Skip to content

Commit 888f9aa

Browse files
authored
Merge pull request #20 from multikernel/feature/sysctl-support
feat(sysctl): typed @sysctl global variables
2 parents 17a520a + 3a6fcd0 commit 888f9aa

14 files changed

Lines changed: 667 additions & 30 deletions

SPEC.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,67 @@ fn main() -> i32 {
877877
| **Scoping** | Shared or local | Always shared | Always shared | Always shared |
878878
| **Persistence** | No | Yes (filesystem) | Optional (if pinned) | No |
879879

880+
#### 3.3.7 Sysctl Variables
881+
882+
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.
883+
884+
**Syntax:**
885+
886+
```kernelscript
887+
@sysctl("net.core.somaxconn") var somaxconn: u32
888+
@sysctl("net.ipv4.ip_forward") var ip_forward: bool
889+
@sysctl("kernel.hostname") var hostname: str(64)
890+
```
891+
892+
The attribute argument is the dotted path under `/proc/sys`. The declared type is the wire type after parsing the file's text contents.
893+
894+
**Constraints (enforced at compile time):**
895+
896+
- Allowed types: `u8/u16/u32/u64`, `i8/i16/i32/i64`, `bool` (rendered as `0`/`1`), `str(N)`. Struct, array, and map types are rejected.
897+
- The path must be a non-empty dotted string with no `/` and no `..`.
898+
- No initializer — values come from the kernel.
899+
- Cannot be combined with `pin` or `local`.
900+
- **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.
901+
902+
**Semantics:**
903+
904+
- Reads happen on every access; writes happen on every assignment. There is no caching.
905+
- Failures (`EACCES`, `EINVAL`, `ENOENT`, ...) are reported via the standard error path.
906+
- The eBPF and kernel-module outputs do not contain sysctl globals — they exist only in the userspace binary.
907+
908+
**Examples:**
909+
910+
Tuning a knob the eBPF program needs:
911+
912+
```kernelscript
913+
@sysctl("net.core.bpf_jit_enable") var bpf_jit: bool
914+
915+
@xdp fn filter(ctx: *xdp_md) -> xdp_action { return XDP_PASS }
916+
917+
fn main() -> i32 {
918+
if (!bpf_jit) {
919+
bpf_jit = true
920+
}
921+
var prog = load(filter)
922+
attach(prog, "eth0", 0)
923+
return 0
924+
}
925+
```
926+
927+
Save and restore around an experiment:
928+
929+
```kernelscript
930+
@sysctl("net.core.somaxconn") var somaxconn: u32
931+
932+
fn main() -> i32 {
933+
var saved = somaxconn
934+
somaxconn = 65535
935+
run_experiment()
936+
somaxconn = saved
937+
return 0
938+
}
939+
```
940+
880941
### 3.4 Kernel-Userspace Scoping Model
881942

882943
KernelScript uses a simple and intuitive scoping model:

examples/sysctl_demo.ks

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@sysctl("kernel.ostype") var ostype: str(32)
2+
@sysctl("net.core.somaxconn") var somaxconn: u32
3+
4+
@xdp fn passthrough(ctx: *xdp_md) -> xdp_action {
5+
return 2
6+
}
7+
8+
fn main() -> i32 {
9+
var was: u32 = somaxconn
10+
print("ostype=", ostype, " somaxconn=", was)
11+
somaxconn = 4096
12+
return 0
13+
}

src/ast.ml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ type global_variable_declaration = {
389389
global_var_pos: position;
390390
is_local: bool; (* true if declared with 'local' keyword *)
391391
is_pinned: bool; (* true if declared with 'pin' keyword *)
392+
global_var_attributes: attribute list;
392393
}
393394

394395
(** Impl block for struct_ops - Option 1 from proposal *)
@@ -588,13 +589,14 @@ let make_config_declaration name fields pos = {
588589
config_pos = pos;
589590
}
590591

591-
let make_global_var_decl name typ init pos ?(is_local=false) ?(is_pinned=false) () = {
592+
let make_global_var_decl name typ init pos ?(is_local=false) ?(is_pinned=false) ?(attributes=[]) () = {
592593
global_var_name = name;
593594
global_var_type = typ;
594595
global_var_init = init;
595596
global_var_pos = pos;
596597
is_local;
597598
is_pinned;
599+
global_var_attributes = attributes;
598600
}
599601

600602
let make_impl_block name attributes items pos = {
@@ -962,6 +964,8 @@ let string_of_declaration = function
962964
) struct_def.struct_fields) in
963965
Printf.sprintf "%sstruct %s {\n %s\n}" attrs_str struct_def.struct_name fields_str
964966
| GlobalVarDecl decl ->
967+
let attrs_str = if decl.global_var_attributes = [] then "" else
968+
(String.concat " " (List.map string_of_attribute decl.global_var_attributes)) ^ "\n" in
965969
let pin_str = if decl.is_pinned then "pin " else "" in
966970
let local_str = if decl.is_local then "local " else "" in
967971
let type_str = match decl.global_var_type with
@@ -972,7 +976,7 @@ let string_of_declaration = function
972976
| None -> ""
973977
| Some expr -> " = " ^ string_of_expr expr
974978
in
975-
Printf.sprintf "%s%svar %s%s%s;" pin_str local_str decl.global_var_name type_str init_str
979+
Printf.sprintf "%s%s%svar %s%s%s;" attrs_str pin_str local_str decl.global_var_name type_str init_str
976980
| ImplBlock impl_block ->
977981
let attrs_str = String.concat " " (List.map string_of_attribute impl_block.impl_attributes) in
978982
let items_str = String.concat "\n " (List.map (function

src/ebpf_c_codegen.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3056,7 +3056,9 @@ let generate_declarations_in_source_order_unified ctx ir_multi_prog ~_btf_path _
30563056

30573057
| Ir.IRDeclGlobalVarDef global_var ->
30583058
(* Skip variables that shadow map definitions *)
3059-
if not (List.mem global_var.global_var_name map_names) then (
3059+
(* Skip sysctl globals — they are userspace-only, never emitted in eBPF *)
3060+
if global_var.sysctl_path = None
3061+
&& not (List.mem global_var.global_var_name map_names) then (
30603062
(* Emit __hidden macro once before the first local variable *)
30613063
if global_var.is_local && not !hidden_macro_emitted then (
30623064
hidden_macro_emitted := true;

src/ir.ml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ and ir_global_variable = {
379379
global_var_pos: ir_position;
380380
is_local: bool; (* true if declared with 'local' keyword *)
381381
is_pinned: bool; (* true if declared with 'pin' keyword *)
382+
sysctl_path: string option; (* Some "net.core.somaxconn" for @sysctl globals *)
382383
}
383384

384385
(** Source-ordered declaration for preserving original order *)
@@ -631,13 +632,14 @@ let make_ir_config_management loads updates sync = {
631632
runtime_config_sync = sync;
632633
}
633634

634-
let make_ir_global_variable name var_type init pos ?(is_local=false) ?(is_pinned=false) () = {
635+
let make_ir_global_variable name var_type init pos ?(is_local=false) ?(is_pinned=false) ?(sysctl_path=None) () = {
635636
global_var_name = name;
636637
global_var_type = var_type;
637638
global_var_init = init;
638639
global_var_pos = pos;
639640
is_local;
640641
is_pinned;
642+
sysctl_path;
641643
}
642644

643645
(** Extraction helpers: extract typed lists from source_declarations *)

src/ir_generator.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,13 +2751,19 @@ let lower_global_variable_declaration symbol_table (global_var_decl : Ast.global
27512751
| _ -> None))
27522752
| None -> None
27532753
in
2754+
let sysctl_path =
2755+
List.find_map (function
2756+
| Ast.AttributeWithArg ("sysctl", p) -> Some p
2757+
| _ -> None) global_var_decl.global_var_attributes
2758+
in
27542759
make_ir_global_variable
27552760
global_var_decl.global_var_name
27562761
ir_type
27572762
ir_init
27582763
global_var_decl.global_var_pos
27592764
~is_local:global_var_decl.is_local
27602765
~is_pinned:global_var_decl.is_pinned
2766+
~sysctl_path
27612767
()
27622768

27632769

src/parser.mly

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,24 @@ global_variable_declaration:
673673
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_local:true ~is_pinned:true () }
674674
| PIN LOCAL VAR IDENTIFIER ASSIGN expression
675675
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_local:true ~is_pinned:true () }
676+
| attribute_list VAR IDENTIFIER COLON bpf_type ASSIGN expression
677+
{ make_global_var_decl $3 (Some $5) (Some $7) (make_pos ()) ~attributes:$1 () }
678+
| attribute_list VAR IDENTIFIER COLON bpf_type
679+
{ make_global_var_decl $3 (Some $5) None (make_pos ()) ~attributes:$1 () }
680+
| attribute_list VAR IDENTIFIER ASSIGN expression
681+
{ make_global_var_decl $3 None (Some $5) (make_pos ()) ~attributes:$1 () }
682+
| attribute_list PIN VAR IDENTIFIER COLON bpf_type ASSIGN expression
683+
{ make_global_var_decl $4 (Some $6) (Some $8) (make_pos ()) ~is_pinned:true ~attributes:$1 () }
684+
| attribute_list PIN VAR IDENTIFIER COLON bpf_type
685+
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_pinned:true ~attributes:$1 () }
686+
| attribute_list PIN VAR IDENTIFIER ASSIGN expression
687+
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_pinned:true ~attributes:$1 () }
688+
| attribute_list LOCAL VAR IDENTIFIER COLON bpf_type ASSIGN expression
689+
{ make_global_var_decl $4 (Some $6) (Some $8) (make_pos ()) ~is_local:true ~attributes:$1 () }
690+
| attribute_list LOCAL VAR IDENTIFIER COLON bpf_type
691+
{ make_global_var_decl $4 (Some $6) None (make_pos ()) ~is_local:true ~attributes:$1 () }
692+
| attribute_list LOCAL VAR IDENTIFIER ASSIGN expression
693+
{ make_global_var_decl $4 None (Some $6) (make_pos ()) ~is_local:true ~attributes:$1 () }
676694

677695
/* Match expressions: match (expr) { pattern: expr, ... } */
678696
match_expression:

src/type_checker.ml

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type context = {
3535
function_scopes: (string, Ast.function_scope) Hashtbl.t;
3636
helper_functions: (string, unit) Hashtbl.t; (* Track @helper functions *)
3737
test_functions: (string, unit) Hashtbl.t; (* Track @test functions *)
38+
sysctl_globals: (string, unit) Hashtbl.t; (* Track @sysctl global vars by name *)
3839
maps: (string, Ir.ir_map_def) Hashtbl.t;
3940
configs: (string, Ast.config_declaration) Hashtbl.t;
4041
attributed_functions: (string, unit) Hashtbl.t; (* Track attributed functions that cannot be called directly *)
@@ -144,6 +145,7 @@ let create_context symbol_table ast =
144145
let function_scopes = Hashtbl.create 16 in
145146
let helper_functions = Hashtbl.create 16 in
146147
let test_functions = Hashtbl.create 16 in
148+
let sysctl_globals = Hashtbl.create 8 in
147149
let attributed_functions = Hashtbl.create 16 in
148150
let types = Hashtbl.create 16 in
149151
let maps = Hashtbl.create 16 in
@@ -183,6 +185,7 @@ let create_context symbol_table ast =
183185
function_scopes = function_scopes;
184186
helper_functions = helper_functions;
185187
test_functions = test_functions;
188+
sysctl_globals = sysctl_globals;
186189
attributed_functions = attributed_functions;
187190
types = types;
188191
maps = maps;
@@ -386,6 +389,71 @@ let validate_ringbuf_object ctx _name ringbuf_type pos =
386389
type_error ("Ring buffer size must not exceed 128MB, got: " ^ string_of_int size) pos
387390
| _ -> () (* Not a ring buffer, no validation needed *)
388391

392+
(** Validate a @sysctl global variable declaration *)
393+
let validate_sysctl_decl gv =
394+
let path =
395+
List.find_map (function
396+
| AttributeWithArg ("sysctl", p) -> Some p
397+
| _ -> None) gv.global_var_attributes
398+
in
399+
match path with
400+
| None -> ()
401+
| Some path ->
402+
if path = ""
403+
|| String.contains path '/'
404+
|| (try ignore (Str.search_forward (Str.regexp_string "..") path 0); true
405+
with Not_found -> false)
406+
then type_error
407+
("Invalid sysctl path '" ^ path ^ "': must be a non-empty dotted string with no '/' or '..'")
408+
gv.global_var_pos;
409+
410+
let type_ok = match gv.global_var_type with
411+
| Some t ->
412+
(match t with
413+
| U8 | U16 | U32 | U64
414+
| I8 | I16 | I32 | I64
415+
| Bool
416+
| Str _ -> true
417+
| _ -> false)
418+
| None -> false
419+
in
420+
if not type_ok then
421+
type_error
422+
("sysctl variable '" ^ gv.global_var_name ^
423+
"' must be an integer, bool, or str(N) (no struct/array/map types)")
424+
gv.global_var_pos;
425+
426+
if gv.global_var_init <> None then
427+
type_error
428+
("sysctl variable '" ^ gv.global_var_name ^
429+
"' cannot have an initializer; values come from /proc/sys")
430+
gv.global_var_pos;
431+
432+
if gv.is_pinned then
433+
type_error
434+
("sysctl variable '" ^ gv.global_var_name ^
435+
"' cannot also be 'pin'")
436+
gv.global_var_pos
437+
438+
(** Reject access to a @sysctl global from eBPF or kernel-scope (kfunc/helper) contexts.
439+
sysctl handles are userspace-only because they perform /proc/sys file I/O. *)
440+
let check_sysctl_context_access ctx name pos =
441+
if Hashtbl.mem ctx.sysctl_globals name then begin
442+
let in_ebpf = ctx.current_program_type <> None in
443+
let in_kernel_fn = match ctx.current_function with
444+
| Some f ->
445+
(match Hashtbl.find_opt ctx.function_scopes f with
446+
| Some Ast.Kernel -> true
447+
| _ -> false)
448+
| None -> false
449+
in
450+
if in_ebpf || in_kernel_fn then
451+
type_error
452+
("sysctl variable '" ^ name ^
453+
"' can only be accessed from userspace functions, not from eBPF or kfunc contexts")
454+
pos
455+
end
456+
389457
(** Check if we can assign from_type to to_type (for variable declarations) *)
390458
let can_assign to_type from_type =
391459
match unify_types to_type from_type with
@@ -565,6 +633,7 @@ let type_check_identifier ctx name pos =
565633
else
566634
try
567635
let typ = Hashtbl.find ctx.variables name in
636+
check_sysctl_context_access ctx name pos;
568637
{ texpr_desc = TIdentifier name; texpr_type = typ; texpr_pos = pos }
569638
with Not_found ->
570639
(* Check if it's a function that could be used as a reference *)
@@ -1574,6 +1643,8 @@ and type_check_statement ctx stmt =
15741643

15751644
| Assignment (name, expr) ->
15761645
let typed_expr = type_check_expression ctx expr in
1646+
(* Reject sysctl writes from eBPF/kernel contexts *)
1647+
check_sysctl_context_access ctx name stmt.stmt_pos;
15771648
(* Check if the variable is const by looking it up in the symbol table *)
15781649
(match Symbol_table.lookup_symbol ctx.symbol_table name with
15791650
| Some symbol when Symbol_table.is_const_variable symbol ->
@@ -1594,6 +1665,7 @@ and type_check_statement ctx stmt =
15941665

15951666
| CompoundAssignment (name, op, expr) ->
15961667
let typed_expr = type_check_expression ctx expr in
1668+
check_sysctl_context_access ctx name stmt.stmt_pos;
15971669
(* Check if the variable is const by looking it up in the symbol table *)
15981670
(match Symbol_table.lookup_symbol ctx.symbol_table name with
15991671
| Some symbol when Symbol_table.is_const_variable symbol ->
@@ -2486,13 +2558,21 @@ let type_check_ast ?symbol_table:(provided_symbol_table=None) ast =
24862558
in
24872559
Hashtbl.replace ctx.maps map_decl.name ir_map_def
24882560
| GlobalVarDecl global_var_decl ->
2561+
(* Validate @sysctl declarations *)
2562+
validate_sysctl_decl global_var_decl;
2563+
(* Register sysctl globals for usage-site context checks *)
2564+
if List.exists (function
2565+
| AttributeWithArg ("sysctl", _) -> true
2566+
| _ -> false) global_var_decl.global_var_attributes
2567+
then
2568+
Hashtbl.replace ctx.sysctl_globals global_var_decl.global_var_name ();
24892569
(* Validate pinning rules: cannot pin local variables *)
24902570
if global_var_decl.is_pinned && global_var_decl.is_local then
24912571
type_error "Cannot pin local variables - only shared variables can be pinned" global_var_decl.global_var_pos;
2492-
2572+
24932573
(* Add global variable to type checker context *)
24942574
let var_type = match global_var_decl.global_var_type with
2495-
| Some t ->
2575+
| Some t ->
24962576
let resolved_type = resolve_user_type ctx t in
24972577
(* Validate ring buffer objects *)
24982578
validate_ringbuf_object ctx global_var_decl.global_var_name resolved_type global_var_decl.global_var_pos;
@@ -2502,7 +2582,7 @@ let type_check_ast ?symbol_table:(provided_symbol_table=None) ast =
25022582
Hashtbl.replace ctx.variables global_var_decl.global_var_name var_type
25032583
| _ -> ()
25042584
) ast;
2505-
2585+
25062586
(* Second pass: First register ALL function signatures (global and attributed) *)
25072587
List.iter (function
25082588
| GlobalFunction func ->
@@ -2847,13 +2927,21 @@ let rec type_check_and_annotate_ast ?symbol_table:(provided_symbol_table=None) ?
28472927
| ConfigDecl config_decl ->
28482928
Hashtbl.replace ctx.configs config_decl.config_name config_decl
28492929
| GlobalVarDecl global_var_decl ->
2930+
(* Validate @sysctl declarations *)
2931+
validate_sysctl_decl global_var_decl;
2932+
(* Register sysctl globals for usage-site context checks *)
2933+
if List.exists (function
2934+
| AttributeWithArg ("sysctl", _) -> true
2935+
| _ -> false) global_var_decl.global_var_attributes
2936+
then
2937+
Hashtbl.replace ctx.sysctl_globals global_var_decl.global_var_name ();
28502938
(* Validate pinning rules: cannot pin local variables *)
28512939
if global_var_decl.is_pinned && global_var_decl.is_local then
28522940
type_error "Cannot pin local variables - only shared variables can be pinned" global_var_decl.global_var_pos;
2853-
2941+
28542942
(* Add global variable to type checker context *)
28552943
let var_type = match global_var_decl.global_var_type with
2856-
| Some t ->
2944+
| Some t ->
28572945
let resolved_type = resolve_user_type ctx t in
28582946
(* Validate ring buffer objects *)
28592947
validate_ringbuf_object ctx global_var_decl.global_var_name resolved_type global_var_decl.global_var_pos;

0 commit comments

Comments
 (0)