Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,67 @@ fn main() -> i32 {
| **Scoping** | Shared or local | Always shared | Always shared | Always shared |
| **Persistence** | No | Yes (filesystem) | Optional (if pinned) | No |

#### 3.3.7 Sysctl Variables

The `@sysctl` attribute turns a userspace global into a typed handle for a `/proc/sys/...` knob. Reading the variable opens and parses the corresponding `/proc/sys` file; writing it formats the value and writes the file. Userspace code controls when each access happens — there is no auto-apply or auto-restore.

**Syntax:**

```kernelscript
@sysctl("net.core.somaxconn") var somaxconn: u32
@sysctl("net.ipv4.ip_forward") var ip_forward: bool
@sysctl("kernel.hostname") var hostname: str(64)
```

The attribute argument is the dotted path under `/proc/sys`. The declared type is the wire type after parsing the file's text contents.

**Constraints (enforced at compile time):**

- Allowed types: `u8/u16/u32/u64`, `i8/i16/i32/i64`, `bool` (rendered as `0`/`1`), `str(N)`. Struct, array, and map types are rejected.
- The path must be a non-empty dotted string with no `/` and no `..`.
- No initializer — values come from the kernel.
- Cannot be combined with `pin` or `local`.
- **Userspace only.** A sysctl handle referenced from `@xdp`, `@tc`, `@probe`, `@tracepoint`, `@helper`, or `@kfunc` is a compile-time error. Those contexts have no filesystem access.

**Semantics:**

- Reads happen on every access; writes happen on every assignment. There is no caching.
- Failures (`EACCES`, `EINVAL`, `ENOENT`, ...) are reported via the standard error path.
- The eBPF and kernel-module outputs do not contain sysctl globals — they exist only in the userspace binary.

**Examples:**

Tuning a knob the eBPF program needs:

```kernelscript
@sysctl("net.core.bpf_jit_enable") var bpf_jit: bool

@xdp fn filter(ctx: *xdp_md) -> xdp_action { return XDP_PASS }

fn main() -> i32 {
if (!bpf_jit) {
bpf_jit = true
}
var prog = load(filter)
attach(prog, "eth0", 0)
return 0
}
```

Save and restore around an experiment:

```kernelscript
@sysctl("net.core.somaxconn") var somaxconn: u32

fn main() -> i32 {
var saved = somaxconn
somaxconn = 65535
run_experiment()
somaxconn = saved
return 0
}
```

### 3.4 Kernel-Userspace Scoping Model

KernelScript uses a simple and intuitive scoping model:
Expand Down
13 changes: 13 additions & 0 deletions examples/sysctl_demo.ks
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@sysctl("kernel.ostype") var ostype: str(32)
@sysctl("net.core.somaxconn") var somaxconn: u32

@xdp fn passthrough(ctx: *xdp_md) -> xdp_action {
return 2
}

fn main() -> i32 {
var was: u32 = somaxconn
print("ostype=", ostype, " somaxconn=", was)
somaxconn = 4096
return 0
}
8 changes: 6 additions & 2 deletions src/ast.ml
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ type global_variable_declaration = {
global_var_pos: position;
is_local: bool; (* true if declared with 'local' keyword *)
is_pinned: bool; (* true if declared with 'pin' keyword *)
global_var_attributes: attribute list;
}

(** Impl block for struct_ops - Option 1 from proposal *)
Expand Down Expand Up @@ -585,13 +586,14 @@ let make_config_declaration name fields pos = {
config_pos = pos;
}

let make_global_var_decl name typ init pos ?(is_local=false) ?(is_pinned=false) () = {
let make_global_var_decl name typ init pos ?(is_local=false) ?(is_pinned=false) ?(attributes=[]) () = {
global_var_name = name;
global_var_type = typ;
global_var_init = init;
global_var_pos = pos;
is_local;
is_pinned;
global_var_attributes = attributes;
}

let make_impl_block name attributes items pos = {
Expand Down Expand Up @@ -948,6 +950,8 @@ let string_of_declaration = function
) struct_def.struct_fields) in
Printf.sprintf "%sstruct %s {\n %s\n}" attrs_str struct_def.struct_name fields_str
| GlobalVarDecl decl ->
let attrs_str = if decl.global_var_attributes = [] then "" else
(String.concat " " (List.map string_of_attribute decl.global_var_attributes)) ^ "\n" in
let pin_str = if decl.is_pinned then "pin " else "" in
let local_str = if decl.is_local then "local " else "" in
let type_str = match decl.global_var_type with
Expand All @@ -958,7 +962,7 @@ let string_of_declaration = function
| None -> ""
| Some expr -> " = " ^ string_of_expr expr
in
Printf.sprintf "%s%svar %s%s%s;" pin_str local_str decl.global_var_name type_str init_str
Printf.sprintf "%s%s%svar %s%s%s;" attrs_str pin_str local_str decl.global_var_name type_str init_str
| ImplBlock impl_block ->
let attrs_str = String.concat " " (List.map string_of_attribute impl_block.impl_attributes) in
let items_str = String.concat "\n " (List.map (function
Expand Down
4 changes: 3 additions & 1 deletion src/ebpf_c_codegen.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2974,7 +2974,9 @@ let generate_declarations_in_source_order_unified ctx ir_multi_prog ~_btf_path _

| Ir.IRDeclGlobalVarDef global_var ->
(* Skip variables that shadow map definitions *)
if not (List.mem global_var.global_var_name map_names) then (
(* Skip sysctl globals — they are userspace-only, never emitted in eBPF *)
if global_var.sysctl_path = None
&& not (List.mem global_var.global_var_name map_names) then (
(* Emit __hidden macro once before the first local variable *)
if global_var.is_local && not !hidden_macro_emitted then (
hidden_macro_emitted := true;
Expand Down
4 changes: 3 additions & 1 deletion src/ir.ml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ and ir_global_variable = {
global_var_pos: ir_position;
is_local: bool; (* true if declared with 'local' keyword *)
is_pinned: bool; (* true if declared with 'pin' keyword *)
sysctl_path: string option; (* Some "net.core.somaxconn" for @sysctl globals *)
}

(** Source-ordered declaration for preserving original order *)
Expand Down Expand Up @@ -609,13 +610,14 @@ let make_ir_config_management loads updates sync = {
runtime_config_sync = sync;
}

let make_ir_global_variable name var_type init pos ?(is_local=false) ?(is_pinned=false) () = {
let make_ir_global_variable name var_type init pos ?(is_local=false) ?(is_pinned=false) ?(sysctl_path=None) () = {
global_var_name = name;
global_var_type = var_type;
global_var_init = init;
global_var_pos = pos;
is_local;
is_pinned;
sysctl_path;
}

(** Extraction helpers: extract typed lists from source_declarations *)
Expand Down
6 changes: 6 additions & 0 deletions src/ir_generator.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2633,13 +2633,19 @@ let lower_global_variable_declaration symbol_table (global_var_decl : Ast.global
| _ -> None))
| None -> None
in
let sysctl_path =
List.find_map (function
| Ast.AttributeWithArg ("sysctl", p) -> Some p
| _ -> None) global_var_decl.global_var_attributes
in
make_ir_global_variable
global_var_decl.global_var_name
ir_type
ir_init
global_var_decl.global_var_pos
~is_local:global_var_decl.is_local
~is_pinned:global_var_decl.is_pinned
~sysctl_path
()


Expand Down
18 changes: 18 additions & 0 deletions src/parser.mly
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,24 @@ global_variable_declaration:
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_local:true ~is_pinned:true () }
| PIN LOCAL VAR IDENTIFIER ASSIGN expression
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_local:true ~is_pinned:true () }
| attribute_list VAR IDENTIFIER COLON bpf_type ASSIGN expression
{ make_global_var_decl $3 (Some $5) (Some $7) (make_pos ()) ~attributes:$1 () }
| attribute_list VAR IDENTIFIER COLON bpf_type
{ make_global_var_decl $3 (Some $5) None (make_pos ()) ~attributes:$1 () }
| attribute_list VAR IDENTIFIER ASSIGN expression
{ make_global_var_decl $3 None (Some $5) (make_pos ()) ~attributes:$1 () }
| attribute_list PIN VAR IDENTIFIER COLON bpf_type ASSIGN expression
{ make_global_var_decl $4 (Some $6) (Some $8) (make_pos ()) ~is_pinned:true ~attributes:$1 () }
| attribute_list PIN VAR IDENTIFIER COLON bpf_type
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_pinned:true ~attributes:$1 () }
| attribute_list PIN VAR IDENTIFIER ASSIGN expression
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_pinned:true ~attributes:$1 () }
| attribute_list LOCAL VAR IDENTIFIER COLON bpf_type ASSIGN expression
{ make_global_var_decl $4 (Some $6) (Some $8) (make_pos ()) ~is_local:true ~attributes:$1 () }
| attribute_list LOCAL VAR IDENTIFIER COLON bpf_type
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_local:true ~attributes:$1 () }
| attribute_list LOCAL VAR IDENTIFIER ASSIGN expression
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_local:true ~attributes:$1 () }

/* Match expressions: match (expr) { pattern: expr, ... } */
match_expression:
Expand Down
98 changes: 93 additions & 5 deletions src/type_checker.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type context = {
function_scopes: (string, Ast.function_scope) Hashtbl.t;
helper_functions: (string, unit) Hashtbl.t; (* Track @helper functions *)
test_functions: (string, unit) Hashtbl.t; (* Track @test functions *)
sysctl_globals: (string, unit) Hashtbl.t; (* Track @sysctl global vars by name *)
maps: (string, Ir.ir_map_def) Hashtbl.t;
configs: (string, Ast.config_declaration) Hashtbl.t;
attributed_functions: (string, unit) Hashtbl.t; (* Track attributed functions that cannot be called directly *)
Expand Down Expand Up @@ -140,6 +141,7 @@ let create_context symbol_table ast =
let function_scopes = Hashtbl.create 16 in
let helper_functions = Hashtbl.create 16 in
let test_functions = Hashtbl.create 16 in
let sysctl_globals = Hashtbl.create 8 in
let attributed_functions = Hashtbl.create 16 in
let types = Hashtbl.create 16 in
let maps = Hashtbl.create 16 in
Expand Down Expand Up @@ -179,6 +181,7 @@ let create_context symbol_table ast =
function_scopes = function_scopes;
helper_functions = helper_functions;
test_functions = test_functions;
sysctl_globals = sysctl_globals;
attributed_functions = attributed_functions;
types = types;
maps = maps;
Expand Down Expand Up @@ -377,6 +380,71 @@ let validate_ringbuf_object ctx _name ringbuf_type pos =
type_error ("Ring buffer size must not exceed 128MB, got: " ^ string_of_int size) pos
| _ -> () (* Not a ring buffer, no validation needed *)

(** Validate a @sysctl global variable declaration *)
let validate_sysctl_decl gv =
let path =
List.find_map (function
| AttributeWithArg ("sysctl", p) -> Some p
| _ -> None) gv.global_var_attributes
in
match path with
| None -> ()
| Some path ->
if path = ""
|| String.contains path '/'
|| (try ignore (Str.search_forward (Str.regexp_string "..") path 0); true
with Not_found -> false)
then type_error
("Invalid sysctl path '" ^ path ^ "': must be a non-empty dotted string with no '/' or '..'")
gv.global_var_pos;

let type_ok = match gv.global_var_type with
| Some t ->
(match t with
| U8 | U16 | U32 | U64
| I8 | I16 | I32 | I64
| Bool
| Str _ -> true
| _ -> false)
| None -> false
in
if not type_ok then
type_error
("sysctl variable '" ^ gv.global_var_name ^
"' must be an integer, bool, or str(N) (no struct/array/map types)")
gv.global_var_pos;

if gv.global_var_init <> None then
type_error
("sysctl variable '" ^ gv.global_var_name ^
"' cannot have an initializer; values come from /proc/sys")
gv.global_var_pos;

if gv.is_pinned then
type_error
("sysctl variable '" ^ gv.global_var_name ^
"' cannot also be 'pin'")
gv.global_var_pos

(** Reject access to a @sysctl global from eBPF or kernel-scope (kfunc/helper) contexts.
sysctl handles are userspace-only because they perform /proc/sys file I/O. *)
let check_sysctl_context_access ctx name pos =
if Hashtbl.mem ctx.sysctl_globals name then begin
let in_ebpf = ctx.current_program_type <> None in
let in_kernel_fn = match ctx.current_function with
| Some f ->
(match Hashtbl.find_opt ctx.function_scopes f with
| Some Ast.Kernel -> true
| _ -> false)
| None -> false
in
if in_ebpf || in_kernel_fn then
type_error
("sysctl variable '" ^ name ^
"' can only be accessed from userspace functions, not from eBPF or kfunc contexts")
pos
end

(** Check if we can assign from_type to to_type (for variable declarations) *)
let can_assign to_type from_type =
match unify_types to_type from_type with
Expand Down Expand Up @@ -548,6 +616,7 @@ let type_check_identifier ctx name pos =
else
try
let typ = Hashtbl.find ctx.variables name in
check_sysctl_context_access ctx name pos;
{ texpr_desc = TIdentifier name; texpr_type = typ; texpr_pos = pos }
with Not_found ->
(* Check if it's a function that could be used as a reference *)
Expand Down Expand Up @@ -1561,6 +1630,8 @@ and type_check_statement ctx stmt =
(match typed_expr.texpr_type with
| NoneType -> type_error ("'none' cannot be assigned to variables. It can only be used in comparisons with map lookup results.") stmt.stmt_pos
| _ -> ());
(* Reject sysctl writes from eBPF/kernel contexts *)
check_sysctl_context_access ctx name stmt.stmt_pos;
(* Check if the variable is const by looking it up in the symbol table *)
(match Symbol_table.lookup_symbol ctx.symbol_table name with
| Some symbol when Symbol_table.is_const_variable symbol ->
Expand All @@ -1581,6 +1652,7 @@ and type_check_statement ctx stmt =

| CompoundAssignment (name, op, expr) ->
let typed_expr = type_check_expression ctx expr in
check_sysctl_context_access ctx name stmt.stmt_pos;
(* Check if the variable is const by looking it up in the symbol table *)
(match Symbol_table.lookup_symbol ctx.symbol_table name with
| Some symbol when Symbol_table.is_const_variable symbol ->
Expand Down Expand Up @@ -2391,13 +2463,21 @@ let type_check_ast ?symbol_table:(provided_symbol_table=None) ast =
in
Hashtbl.replace ctx.maps map_decl.name ir_map_def
| GlobalVarDecl global_var_decl ->
(* Validate @sysctl declarations *)
validate_sysctl_decl global_var_decl;
(* Register sysctl globals for usage-site context checks *)
if List.exists (function
| AttributeWithArg ("sysctl", _) -> true
| _ -> false) global_var_decl.global_var_attributes
then
Hashtbl.replace ctx.sysctl_globals global_var_decl.global_var_name ();
(* Validate pinning rules: cannot pin local variables *)
if global_var_decl.is_pinned && global_var_decl.is_local then
type_error "Cannot pin local variables - only shared variables can be pinned" global_var_decl.global_var_pos;

(* Add global variable to type checker context *)
let var_type = match global_var_decl.global_var_type with
| Some t ->
| Some t ->
let resolved_type = resolve_user_type ctx t in
(* Validate ring buffer objects *)
validate_ringbuf_object ctx global_var_decl.global_var_name resolved_type global_var_decl.global_var_pos;
Expand All @@ -2407,7 +2487,7 @@ let type_check_ast ?symbol_table:(provided_symbol_table=None) ast =
Hashtbl.replace ctx.variables global_var_decl.global_var_name var_type
| _ -> ()
) ast;

(* Second pass: First register ALL function signatures (global and attributed) *)
List.iter (function
| GlobalFunction func ->
Expand Down Expand Up @@ -2744,13 +2824,21 @@ let rec type_check_and_annotate_ast ?symbol_table:(provided_symbol_table=None) ?
| ConfigDecl config_decl ->
Hashtbl.replace ctx.configs config_decl.config_name config_decl
| GlobalVarDecl global_var_decl ->
(* Validate @sysctl declarations *)
validate_sysctl_decl global_var_decl;
(* Register sysctl globals for usage-site context checks *)
if List.exists (function
| AttributeWithArg ("sysctl", _) -> true
| _ -> false) global_var_decl.global_var_attributes
then
Hashtbl.replace ctx.sysctl_globals global_var_decl.global_var_name ();
(* Validate pinning rules: cannot pin local variables *)
if global_var_decl.is_pinned && global_var_decl.is_local then
type_error "Cannot pin local variables - only shared variables can be pinned" global_var_decl.global_var_pos;

(* Add global variable to type checker context *)
let var_type = match global_var_decl.global_var_type with
| Some t ->
| Some t ->
let resolved_type = resolve_user_type ctx t in
(* Validate ring buffer objects *)
validate_ringbuf_object ctx global_var_decl.global_var_name resolved_type global_var_decl.global_var_pos;
Expand Down
Loading
Loading