From 85be7ffe493f2a5245f213fa3728607ce09900ff Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 16:19:21 -0700 Subject: [PATCH 01/14] feat(sysctl): parse @sysctl attribute on global var declarations Signed-off-by: Cong Wang --- src/ast.ml | 4 +++- src/parser.mly | 6 ++++++ tests/dune | 14 ++++++++++++-- tests/test_definition_order.ml | 1 + tests/test_sysctl.ml | 27 +++++++++++++++++++++++++++ 5 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/test_sysctl.ml diff --git a/src/ast.ml b/src/ast.ml index 3ff6ae4..1c28920 100644 --- a/src/ast.ml +++ b/src/ast.ml @@ -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 *) @@ -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) ?(attrs=[]) () = { global_var_name = name; global_var_type = typ; global_var_init = init; global_var_pos = pos; is_local; is_pinned; + global_var_attributes = attrs; } let make_impl_block name attributes items pos = { diff --git a/src/parser.mly b/src/parser.mly index 1f90250..4f0b35f 100644 --- a/src/parser.mly +++ b/src/parser.mly @@ -654,6 +654,12 @@ 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 ()) ~attrs:$1 () } + | attribute_list VAR IDENTIFIER COLON bpf_type + { make_global_var_decl $3 (Some $5) None (make_pos ()) ~attrs:$1 () } + | attribute_list VAR IDENTIFIER ASSIGN expression + { make_global_var_decl $3 None (Some $5) (make_pos ()) ~attrs:$1 () } /* Match expressions: match (expr) { pattern: expr, ... } */ match_expression: diff --git a/tests/dune b/tests/dune index 25142e2..d5e394d 100644 --- a/tests/dune +++ b/tests/dune @@ -431,6 +431,11 @@ (modules test_definition_order) (libraries kernelscript alcotest)) +(executable + (name test_sysctl) + (modules test_sysctl) + (libraries kernelscript alcotest str)) + ; Top-level alias to build all tests @@ -519,7 +524,8 @@ test_tc.exe test_exec.exe test_void_functions.exe - test_definition_order.exe)) + test_definition_order.exe + test_sysctl.exe)) ; Runtest rules to actually execute the tests (rule @@ -852,4 +858,8 @@ (rule (alias runtest) - (action (run ./test_definition_order.exe))) \ No newline at end of file + (action (run ./test_definition_order.exe))) + +(rule + (alias runtest) + (action (run ./test_sysctl.exe))) \ No newline at end of file diff --git a/tests/test_definition_order.ml b/tests/test_definition_order.ml index b72df8d..4240b2d 100644 --- a/tests/test_definition_order.ml +++ b/tests/test_definition_order.ml @@ -54,6 +54,7 @@ let make_test_global_var name var_type line = global_var_pos = make_test_position line 1; is_local = false; is_pinned = false; + global_var_attributes = []; } let make_test_function name params return_type body line = diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml new file mode 100644 index 0000000..0291bfd --- /dev/null +++ b/tests/test_sysctl.ml @@ -0,0 +1,27 @@ +open Kernelscript.Ast +open Kernelscript.Parse + +let test_parse_sysctl_attribute () = + let src = {| +@sysctl("net.core.somaxconn") +var somaxconn: u32 + +fn main() -> i32 { return 0 } +|} in + let ast = parse_string src in + let found = List.exists (function + | GlobalVarDecl gv -> + gv.global_var_name = "somaxconn" + && List.exists (function + | AttributeWithArg ("sysctl", "net.core.somaxconn") -> true + | _ -> false) + gv.global_var_attributes + | _ -> false) ast in + Alcotest.(check bool) "sysctl attribute parsed" true found + +let () = + Alcotest.run "sysctl" [ + "parse", [ + Alcotest.test_case "attribute on global var" `Quick test_parse_sysctl_attribute; + ]; + ] From 10e824a0431562eb0b48e6b31110ec8a950c2a3a Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 16:57:50 -0700 Subject: [PATCH 02/14] fix(sysctl): align make_global_var_decl naming and round-trip attributes Signed-off-by: Cong Wang --- src/ast.ml | 8 ++++--- src/parser.mly | 6 +++--- tests/dune | 2 +- tests/test_sysctl.ml | 51 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/ast.ml b/src/ast.ml index 1c28920..8e90540 100644 --- a/src/ast.ml +++ b/src/ast.ml @@ -586,14 +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) ?(attrs=[]) () = { +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 = attrs; + global_var_attributes = attributes; } let make_impl_block name attributes items pos = { @@ -950,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 @@ -960,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 diff --git a/src/parser.mly b/src/parser.mly index 4f0b35f..a4b8a9b 100644 --- a/src/parser.mly +++ b/src/parser.mly @@ -655,11 +655,11 @@ global_variable_declaration: | 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 ()) ~attrs:$1 () } + { 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 ()) ~attrs:$1 () } + { 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 ()) ~attrs:$1 () } + { make_global_var_decl $3 None (Some $5) (make_pos ()) ~attributes:$1 () } /* Match expressions: match (expr) { pattern: expr, ... } */ match_expression: diff --git a/tests/dune b/tests/dune index d5e394d..c9835b9 100644 --- a/tests/dune +++ b/tests/dune @@ -862,4 +862,4 @@ (rule (alias runtest) - (action (run ./test_sysctl.exe))) \ No newline at end of file + (action (run ./test_sysctl.exe))) diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 0291bfd..625905a 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -1,3 +1,19 @@ +(* + * Copyright 2025 Multikernel Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *) + open Kernelscript.Ast open Kernelscript.Parse @@ -19,9 +35,44 @@ fn main() -> i32 { return 0 } | _ -> false) ast in Alcotest.(check bool) "sysctl attribute parsed" true found +let test_parse_simple_attribute () = + let src = {| +@some_simple_attr +var x: u32 + +fn main() -> i32 { return 0 } +|} in + let ast = parse_string src in + let found = List.exists (function + | GlobalVarDecl gv -> + gv.global_var_name = "x" + && List.exists (function + | SimpleAttribute "some_simple_attr" -> true + | _ -> false) + gv.global_var_attributes + | _ -> false) ast in + Alcotest.(check bool) "simple attribute parsed" true found + +let test_parse_multiple_attributes () = + let src = {| +@first @sysctl("net.core.somaxconn") +var x: u32 + +fn main() -> i32 { return 0 } +|} in + let ast = parse_string src in + let count = List.fold_left (fun acc d -> + match d with + | GlobalVarDecl gv when gv.global_var_name = "x" -> + acc + List.length gv.global_var_attributes + | _ -> acc) 0 ast in + Alcotest.(check int) "two attributes accumulated" 2 count + let () = Alcotest.run "sysctl" [ "parse", [ Alcotest.test_case "attribute on global var" `Quick test_parse_sysctl_attribute; + Alcotest.test_case "simple attribute on global var" `Quick test_parse_simple_attribute; + Alcotest.test_case "multiple attributes on global var" `Quick test_parse_multiple_attributes; ]; ] From b2eb68ff586ea8a0f38ff01e8b9de806016259a0 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 17:44:05 -0700 Subject: [PATCH 03/14] feat(sysctl): validate @sysctl declarations (path, type, modifiers) Signed-off-by: Cong Wang --- src/parser.mly | 12 +++++++ src/type_checker.ml | 60 ++++++++++++++++++++++++++++++++--- tests/test_sysctl.ml | 75 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/src/parser.mly b/src/parser.mly index a4b8a9b..861e596 100644 --- a/src/parser.mly +++ b/src/parser.mly @@ -660,6 +660,18 @@ global_variable_declaration: { 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: diff --git a/src/type_checker.ml b/src/type_checker.ml index 8a95a99..fadfc23 100644 --- a/src/type_checker.ml +++ b/src/type_checker.ml @@ -377,6 +377,52 @@ 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 + (** 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 @@ -2391,13 +2437,15 @@ 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; (* 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; @@ -2407,7 +2455,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 -> @@ -2744,13 +2792,15 @@ 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; (* 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; diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 625905a..3cc23c5 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -68,6 +68,73 @@ fn main() -> i32 { return 0 } | _ -> acc) 0 ast in Alcotest.(check int) "two attributes accumulated" 2 count +let typecheck_string src = + let ast = Kernelscript.Parse.parse_string src in + Kernelscript.Type_checker.type_check_ast ast + +let expect_typecheck_error ~fragment src = + let got = + try Ok (typecheck_string src) with + | Kernelscript.Type_checker.Type_error (m, _) -> Error m + in + match got with + | Error m -> + let contains hay needle = + try ignore (Str.search_forward (Str.regexp_string needle) hay 0); true + with Not_found -> false + in + Alcotest.(check bool) ("error contains '" ^ fragment ^ "'") true (contains m fragment) + | Ok _ -> + Alcotest.failf "expected type error containing '%s', got success" fragment + +let test_reject_unsupported_type () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") +var somaxconn: hash(1) +fn main() -> i32 { return 0 } +|} + +let test_reject_bad_path_double_dot () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net..core") +var x: u32 +fn main() -> i32 { return 0 } +|} + +let test_reject_bad_path_absolute () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("/proc/sys/net/core/somaxconn") +var x: u32 +fn main() -> i32 { return 0 } +|} + +let test_reject_initializer () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") +var x: u32 = 100 +fn main() -> i32 { return 0 } +|} + +let test_reject_pin_combination () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") +pin var x: u32 +fn main() -> i32 { return 0 } +|} + +let test_accept_int_bool_str () = + ignore (typecheck_string {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@sysctl("net.ipv4.ip_forward") var ip_forward: bool +@sysctl("kernel.hostname") var hostname: str(64) +fn main() -> i32 { return 0 } +|}) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -75,4 +142,12 @@ let () = Alcotest.test_case "simple attribute on global var" `Quick test_parse_simple_attribute; Alcotest.test_case "multiple attributes on global var" `Quick test_parse_multiple_attributes; ]; + "typecheck", [ + Alcotest.test_case "reject unsupported type" `Quick test_reject_unsupported_type; + Alcotest.test_case "reject bad path (double dot)" `Quick test_reject_bad_path_double_dot; + Alcotest.test_case "reject bad path (absolute)" `Quick test_reject_bad_path_absolute; + Alcotest.test_case "reject initializer" `Quick test_reject_initializer; + Alcotest.test_case "reject pin combination" `Quick test_reject_pin_combination; + Alcotest.test_case "accept int/bool/str" `Quick test_accept_int_bool_str; + ]; ] From 9d32d3a40a27e4ee11c6a34e8057aa631547eb2a Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 17:50:10 -0700 Subject: [PATCH 04/14] feat(sysctl): reject sysctl handle access from eBPF and kfunc contexts Signed-off-by: Cong Wang --- src/type_checker.ml | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_sysctl.ml | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/type_checker.ml b/src/type_checker.ml index fadfc23..1118ab9 100644 --- a/src/type_checker.ml +++ b/src/type_checker.ml @@ -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 *) @@ -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 @@ -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; @@ -423,6 +426,25 @@ let validate_sysctl_decl gv = "' 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 @@ -594,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 *) @@ -1607,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 -> @@ -1627,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 -> @@ -2439,6 +2465,12 @@ let type_check_ast ?symbol_table:(provided_symbol_table=None) ast = | 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; @@ -2794,6 +2826,12 @@ let rec type_check_and_annotate_ast ?symbol_table:(provided_symbol_table=None) ? | 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; diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 3cc23c5..a80eb57 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -135,6 +135,44 @@ let test_accept_int_bool_str () = fn main() -> i32 { return 0 } |}) +let test_reject_sysctl_in_xdp () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn f(ctx: *xdp_md) -> xdp_action { + var x = somaxconn + return 2 +} +fn main() -> i32 { return 0 } +|} + +let test_reject_sysctl_in_helper () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@helper fn h() -> u32 { return somaxconn } +@xdp fn f(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} + +let test_reject_sysctl_in_kfunc () = + expect_typecheck_error ~fragment:"sysctl" + {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@kfunc fn k() -> u32 { return somaxconn } +fn main() -> i32 { return 0 } +|} + +let test_allow_sysctl_in_userspace () = + ignore (typecheck_string {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +fn read_it() -> u32 { return somaxconn } +fn main() -> i32 { + somaxconn = 4096 + return 0 +} +|}) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -149,5 +187,9 @@ let () = Alcotest.test_case "reject initializer" `Quick test_reject_initializer; Alcotest.test_case "reject pin combination" `Quick test_reject_pin_combination; Alcotest.test_case "accept int/bool/str" `Quick test_accept_int_bool_str; + Alcotest.test_case "reject access from @xdp" `Quick test_reject_sysctl_in_xdp; + Alcotest.test_case "reject access from @helper" `Quick test_reject_sysctl_in_helper; + Alcotest.test_case "reject access from @kfunc" `Quick test_reject_sysctl_in_kfunc; + Alcotest.test_case "allow access from userspace" `Quick test_allow_sysctl_in_userspace; ]; ] From 08402c3924ef1be9589e3a400c0e0faef1349d07 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 17:54:39 -0700 Subject: [PATCH 05/14] feat(sysctl): thread sysctl path through IR Signed-off-by: Cong Wang --- src/ir.ml | 4 ++- src/ir_generator.ml | 6 ++++ tests/test_sysctl.ml | 39 +++++++++++++++++++++++++ tests/test_userspace_skeleton_header.ml | 1 + 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/ir.ml b/src/ir.ml index d3f041f..a93fb84 100644 --- a/src/ir.ml +++ b/src/ir.ml @@ -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 *) @@ -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 *) diff --git a/src/ir_generator.ml b/src/ir_generator.ml index 5e71e4b..274acd5 100644 --- a/src/ir_generator.ml +++ b/src/ir_generator.ml @@ -2633,6 +2633,11 @@ 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 @@ -2640,6 +2645,7 @@ let lower_global_variable_declaration symbol_table (global_var_decl : Ast.global global_var_decl.global_var_pos ~is_local:global_var_decl.is_local ~is_pinned:global_var_decl.is_pinned + ~sysctl_path () diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index a80eb57..9ea9f93 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -173,6 +173,41 @@ fn main() -> i32 { } |}) +let ir_of src = + let ast = Kernelscript.Parse.parse_string src in + let symbol_table = Kernelscript.Symbol_table.build_symbol_table ast in + let (typed_ast, _) = + Kernelscript.Type_checker.type_check_and_annotate_ast ~symbol_table:(Some symbol_table) ast in + Kernelscript.Ir_generator.generate_ir typed_ast symbol_table "test" + +let test_ir_carries_sysctl_path () = + let ir = ir_of {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} in + let globals = Kernelscript.Ir.get_global_variables ir in + let found = + List.exists (fun gv -> + gv.Kernelscript.Ir.global_var_name = "somaxconn" + && gv.Kernelscript.Ir.sysctl_path = Some "net.core.somaxconn") + globals in + Alcotest.(check bool) "IR records sysctl path" true found + +let test_ir_no_path_for_plain_global () = + let ir = ir_of {| +var plain: u32 +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} in + let globals = Kernelscript.Ir.get_global_variables ir in + let found = + List.exists (fun gv -> + gv.Kernelscript.Ir.global_var_name = "plain" + && gv.Kernelscript.Ir.sysctl_path = None) + globals in + Alcotest.(check bool) "plain global has sysctl_path = None" true found + let () = Alcotest.run "sysctl" [ "parse", [ @@ -192,4 +227,8 @@ let () = Alcotest.test_case "reject access from @kfunc" `Quick test_reject_sysctl_in_kfunc; Alcotest.test_case "allow access from userspace" `Quick test_allow_sysctl_in_userspace; ]; + "ir", [ + Alcotest.test_case "IR carries sysctl path" `Quick test_ir_carries_sysctl_path; + Alcotest.test_case "plain global has no sysctl path" `Quick test_ir_no_path_for_plain_global; + ]; ] diff --git a/tests/test_userspace_skeleton_header.ml b/tests/test_userspace_skeleton_header.ml index 1703f3c..8b25c8e 100644 --- a/tests/test_userspace_skeleton_header.ml +++ b/tests/test_userspace_skeleton_header.ml @@ -86,6 +86,7 @@ let test_skeleton_header_included_with_global_variables () = global_var_pos = test_pos; is_local = false; is_pinned = false; + sysctl_path = None; } in let printf_call = make_ir_instruction (IRCall (DirectCall "printf", [make_ir_value (IRLiteral (StringLit "Hello World")) (IRStr 20) test_pos], None)) test_pos in From fbd803c355f1f52cd6d19296067975f5404f5aa9 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 17:57:41 -0700 Subject: [PATCH 06/14] feat(sysctl): exclude sysctl globals from eBPF codegen Signed-off-by: Cong Wang --- src/ebpf_c_codegen.ml | 4 +++- tests/test_sysctl.ml | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/ebpf_c_codegen.ml b/src/ebpf_c_codegen.ml index 5747f60..367b48a 100644 --- a/src/ebpf_c_codegen.ml +++ b/src/ebpf_c_codegen.ml @@ -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; diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 9ea9f93..8771ac7 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -208,6 +208,23 @@ fn main() -> i32 { return 0 } globals in Alcotest.(check bool) "plain global has sysctl_path = None" true found +let ebpf_c_of src = + let ir = ir_of src in + Kernelscript.Ebpf_c_codegen.generate_c_multi_program ir + +let mentions s c = + try ignore (Str.search_forward (Str.regexp_string s) c 0); true + with Not_found -> false + +let test_ebpf_codegen_omits_sysctl_globals () = + let c = ebpf_c_of {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn f(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} in + Alcotest.(check bool) "no sysctl global in eBPF" false (mentions "somaxconn" c); + Alcotest.(check bool) "no /proc/sys reference" false (mentions "/proc/sys" c) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -231,4 +248,7 @@ let () = Alcotest.test_case "IR carries sysctl path" `Quick test_ir_carries_sysctl_path; Alcotest.test_case "plain global has no sysctl path" `Quick test_ir_no_path_for_plain_global; ]; + "codegen", [ + Alcotest.test_case "eBPF codegen omits sysctl globals" `Quick test_ebpf_codegen_omits_sysctl_globals; + ]; ] From f2420b89a2ab62762391441e9dba9ac4e3f87593 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:03:21 -0700 Subject: [PATCH 07/14] feat(sysctl): emit /proc/sys read/write accessors in userspace codegen Signed-off-by: Cong Wang --- src/userspace_codegen.ml | 100 +++++++++++++++++++++++++++++++++++++-- tests/test_sysctl.ml | 24 ++++++++++ 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 0c07f08..6cf8fec 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -1154,6 +1154,97 @@ let generate_type_alias_definitions_userspace_from_ast type_aliases = "/* Type alias definitions */\n" ^ (String.concat "\n" type_alias_defs) ^ "\n\n" ) else "" +(** Generate /proc/sys path constant and read/write accessors for a @sysctl global. *) +let generate_sysctl_accessors_userspace (gv : ir_global_variable) = + match gv.sysctl_path with + | None -> None + | Some dot_path -> + let name = gv.global_var_name in + let proc_path = + "/proc/sys/" ^ + String.map (fun c -> if c = '.' then '/' else c) dot_path + in + let path_const = + sprintf "static const char __ks_sysctl_%s_path[] = \"%s\";" name proc_path + in + let body = match gv.global_var_type with + | IRStr n -> + sprintf {|static inline void __ks_sysctl_%s_read(char out[%d]) { + int __fd = open(__ks_sysctl_%s_path, O_RDONLY); + if (__fd < 0) { + fprintf(stderr, "sysctl read %%s: %%s\n", __ks_sysctl_%s_path, strerror(errno)); + out[0] = 0; return; + } + ssize_t __n = read(__fd, out, %d - 1); + int __e = errno; close(__fd); + if (__n < 0) { + fprintf(stderr, "sysctl read %%s: %%s\n", __ks_sysctl_%s_path, strerror(__e)); + out[0] = 0; return; + } + out[__n] = 0; + if (__n > 0 && out[__n - 1] == '\n') out[__n - 1] = 0; +} + +static inline void __ks_sysctl_%s_write(const char *v) { + int __fd = open(__ks_sysctl_%s_path, O_WRONLY); + if (__fd < 0) { + fprintf(stderr, "sysctl write %%s: %%s\n", __ks_sysctl_%s_path, strerror(errno)); + return; + } + size_t __l = strlen(v); + ssize_t __w = write(__fd, v, __l); + int __e = errno; close(__fd); + if (__w < 0) + fprintf(stderr, "sysctl write %%s: %%s\n", __ks_sysctl_%s_path, strerror(__e)); +}|} + name n name name n name name name name name + | t -> + let c_type, fmt = match t with + | IRU8 | IRU16 | IRU32 -> "uint32_t", "%u" + | IRU64 -> "uint64_t", "%llu" + | IRI8 | IRI16 | IRI32 -> "int32_t", "%d" + | IRI64 -> "int64_t", "%lld" + | IRBool -> "int", "%d" + | _ -> + failwith + (sprintf "sysctl variable '%s' has unsupported IR type" name) + in + sprintf {|static inline %s __ks_sysctl_%s_read(void) { + int __fd = open(__ks_sysctl_%s_path, O_RDONLY); + if (__fd < 0) { + fprintf(stderr, "sysctl read %%s: %%s\n", __ks_sysctl_%s_path, strerror(errno)); + return 0; + } + char __buf[64]; + ssize_t __n = read(__fd, __buf, sizeof(__buf) - 1); + int __e = errno; close(__fd); + if (__n < 0) { + fprintf(stderr, "sysctl read %%s: %%s\n", __ks_sysctl_%s_path, strerror(__e)); + return 0; + } + __buf[__n] = 0; + %s __v = 0; + sscanf(__buf, "%s", &__v); + return __v; +} + +static inline void __ks_sysctl_%s_write(%s v) { + int __fd = open(__ks_sysctl_%s_path, O_WRONLY); + if (__fd < 0) { + fprintf(stderr, "sysctl write %%s: %%s\n", __ks_sysctl_%s_path, strerror(errno)); + return; + } + char __buf[64]; + int __n = snprintf(__buf, sizeof(__buf), "%s", v); + ssize_t __w = write(__fd, __buf, __n); + int __e = errno; close(__fd); + if (__w < 0) + fprintf(stderr, "sysctl write %%s: %%s\n", __ks_sysctl_%s_path, strerror(__e)); +}|} + c_type name name name name c_type fmt name c_type name name fmt name + in + Some (path_const ^ "\n\n" ^ body) + (** Generate ALL declarations in original source order for userspace - complete implementation *) let generate_declarations_in_source_order_userspace ir_multi_prog = let declarations = ref [] in @@ -1182,9 +1273,12 @@ let generate_declarations_in_source_order_userspace ir_multi_prog = (* Skip configs in userspace - they're handled separately *) () - | Ir.IRDeclGlobalVarDef _global_var -> - (* Skip global variables in userspace - they're handled separately *) - () + | Ir.IRDeclGlobalVarDef global_var -> + (* Sysctl globals get inline accessors emitted here. + Other globals are handled by the eBPF skeleton infrastructure. *) + (match generate_sysctl_accessors_userspace global_var with + | Some accessors -> declarations := accessors :: !declarations + | None -> ()) | Ir.IRDeclFunctionDef _func_def -> (* Skip functions in userspace - they're handled separately *) diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 8771ac7..c7cd83f 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -225,6 +225,29 @@ fn main() -> i32 { return 0 } Alcotest.(check bool) "no sysctl global in eBPF" false (mentions "somaxconn" c); Alcotest.(check bool) "no /proc/sys reference" false (mentions "/proc/sys" c) +let user_c_of src = + let ir = ir_of src in + let tmp = Filename.temp_file "ks_user_" "" in + Sys.remove tmp; Unix.mkdir tmp 0o755; + Kernelscript.Userspace_codegen.generate_userspace_code_from_ir + ir ~output_dir:tmp "test.ks"; + let path = Filename.concat tmp "test.c" in + let ic = open_in path in + let s = really_input_string ic (in_channel_length ic) in + close_in ic; + s + +let test_userspace_emits_accessors () = + let c = user_c_of {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} in + Alcotest.(check bool) "path constant" true (mentions "__ks_sysctl_somaxconn_path" c); + Alcotest.(check bool) "proc path" true (mentions "/proc/sys/net/core/somaxconn" c); + Alcotest.(check bool) "read accessor" true (mentions "__ks_sysctl_somaxconn_read" c); + Alcotest.(check bool) "write accessor" true (mentions "__ks_sysctl_somaxconn_write" c) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -250,5 +273,6 @@ let () = ]; "codegen", [ Alcotest.test_case "eBPF codegen omits sysctl globals" `Quick test_ebpf_codegen_omits_sysctl_globals; + Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; ]; ] From ec663cdf88664a71a919b36670a1da2996510fe6 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:05:44 -0700 Subject: [PATCH 08/14] feat(sysctl): rewrite userspace handle reads/writes to accessor calls Signed-off-by: Cong Wang --- src/userspace_codegen.ml | 18 +++++++++++++++--- tests/test_sysctl.ml | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 6cf8fec..8a66c87 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -1401,13 +1401,23 @@ let rec generate_c_value_from_ir ?(auto_deref_map_access=false) ctx ir_value = | Ast.ArrayLit _ -> "{...}" (* nested arrays simplified *) ) elems in sprintf "{%s}" (String.concat ", " elem_strs)) - | IRVariable name -> + | IRVariable name -> (* Check if this is a global variable that should be accessed through skeleton *) let is_global = List.exists (fun gv -> gv.global_var_name = name) ctx.global_variables in if is_global then (* Access global variable through skeleton *) let global_var = List.find (fun gv -> gv.global_var_name = name) ctx.global_variables in - if global_var.is_local then + if global_var.sysctl_path <> None then + (* sysctl reads call the typed accessor. + For str(N) we wrap in a stmt-expr backed by a static buffer + so the load expression has a usable lifetime. *) + (match global_var.global_var_type with + | IRStr n -> + sprintf "({ static char __ks_sb_%s[%d]; __ks_sysctl_%s_read(__ks_sb_%s); __ks_sb_%s; })" + name n name name name + | _ -> + sprintf "__ks_sysctl_%s_read()" name) + else if global_var.is_local then (* Local global variables are not accessible from userspace *) failwith (Printf.sprintf "Local global variable '%s' is not accessible from userspace" name) else if global_var.is_pinned then @@ -1784,7 +1794,9 @@ let generate_variable_assignment ctx dest src is_const = if is_global then (* Global variable assignment - add null check to prevent segfault *) let global_var = List.find (fun gv -> gv.global_var_name = name) ctx.global_variables in - if global_var.is_local then + if global_var.sysctl_path <> None then + sprintf "%s__ks_sysctl_%s_write(%s);" assignment_prefix name src_str + else if global_var.is_local then (* Local global variables are not accessible from userspace *) failwith (Printf.sprintf "Local global variable '%s' is not accessible from userspace" name) else if global_var.is_pinned then diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index c7cd83f..4605ecb 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -248,6 +248,19 @@ fn main() -> i32 { return 0 } Alcotest.(check bool) "read accessor" true (mentions "__ks_sysctl_somaxconn_read" c); Alcotest.(check bool) "write accessor" true (mentions "__ks_sysctl_somaxconn_write" c) +let test_userspace_rewrites_load_store () = + let c = user_c_of {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { + var was = somaxconn + somaxconn = 4096 + return 0 +} +|} in + Alcotest.(check bool) "load → read call" true (mentions "__ks_sysctl_somaxconn_read(" c); + Alcotest.(check bool) "store → write call" true (mentions "__ks_sysctl_somaxconn_write(" c) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -274,5 +287,6 @@ let () = "codegen", [ Alcotest.test_case "eBPF codegen omits sysctl globals" `Quick test_ebpf_codegen_omits_sysctl_globals; Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; + Alcotest.test_case "userspace rewrites load/store" `Quick test_userspace_rewrites_load_store; ]; ] From b4e9bfb20668ae10ddb99e2317fc315c4a200739 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:07:07 -0700 Subject: [PATCH 09/14] test(sysctl): add end-to-end compile test on examples/sysctl_demo.ks Signed-off-by: Cong Wang --- examples/sysctl_demo.ks | 13 +++++++++++++ tests/test_sysctl.ml | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 examples/sysctl_demo.ks diff --git a/examples/sysctl_demo.ks b/examples/sysctl_demo.ks new file mode 100644 index 0000000..1fc2672 --- /dev/null +++ b/examples/sysctl_demo.ks @@ -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 +} diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 4605ecb..3943c5d 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -261,6 +261,23 @@ fn main() -> i32 { Alcotest.(check bool) "load → read call" true (mentions "__ks_sysctl_somaxconn_read(" c); Alcotest.(check bool) "store → write call" true (mentions "__ks_sysctl_somaxconn_write(" c) +let read_file p = + let ic = open_in p in + let s = really_input_string ic (in_channel_length ic) in + close_in ic; s + +let test_e2e_compiles_example () = + let example = "examples/sysctl_demo.ks" in + if not (Sys.file_exists example) then Alcotest.skip () + else + let src = read_file example in + let c = user_c_of src in + Alcotest.(check bool) "ostype path" true (mentions "/proc/sys/kernel/ostype" c); + Alcotest.(check bool) "ostype read" true (mentions "__ks_sysctl_ostype_read" c); + Alcotest.(check bool) "somaxconn path" true (mentions "/proc/sys/net/core/somaxconn" c); + Alcotest.(check bool) "somaxconn read" true (mentions "__ks_sysctl_somaxconn_read" c); + Alcotest.(check bool) "somaxconn write" true (mentions "__ks_sysctl_somaxconn_write" c) + let () = Alcotest.run "sysctl" [ "parse", [ @@ -289,4 +306,7 @@ let () = Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; Alcotest.test_case "userspace rewrites load/store" `Quick test_userspace_rewrites_load_store; ]; + "e2e", [ + Alcotest.test_case "example file compiles" `Quick test_e2e_compiles_example; + ]; ] From c0de2b35681f467ad7c81c26ca8b5b358e8d4f55 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:46:21 -0700 Subject: [PATCH 10/14] feat(sysctl): add sysctl_read_str/sysctl_write_str escape-hatch builtins Signed-off-by: Cong Wang --- src/stdlib.ml | 22 +++++++++++++++++++++ src/userspace_codegen.ml | 41 +++++++++++++++++++++++++++++++++++++++- tests/test_sysctl.ml | 12 ++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/stdlib.ml b/src/stdlib.ml index 2e84eb0..b847f52 100644 --- a/src/stdlib.ml +++ b/src/stdlib.ml @@ -210,6 +210,28 @@ let builtin_functions = [ kernel_impl = ""; (* Not available in kernel context *) validate = Some validate_exec_function; }; + { + name = "sysctl_read_str"; + param_types = [Str 256; Pointer U8; U32]; + return_type = I32; + description = "Read a sysctl as a raw string. Returns 0 or negative errno (userspace only)."; + is_variadic = false; + ebpf_impl = ""; (* Not available in eBPF context *) + userspace_impl = "__ks_sysctl_read_str"; + kernel_impl = ""; (* Not available in kernel context *) + validate = None; + }; + { + name = "sysctl_write_str"; + param_types = [Str 256; Str 256]; + return_type = I32; + description = "Write a string value to a sysctl. Returns 0 or negative errno (userspace only)."; + is_variadic = false; + ebpf_impl = ""; (* Not available in eBPF context *) + userspace_impl = "__ks_sysctl_write_str"; + kernel_impl = ""; (* Not available in kernel context *) + validate = None; + }; ] diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 8a66c87..9d34638 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -1154,6 +1154,39 @@ let generate_type_alias_definitions_userspace_from_ast type_aliases = "/* Type alias definitions */\n" ^ (String.concat "\n" type_alias_defs) ^ "\n\n" ) else "" +(** Static inline helpers for the sysctl_read_str / sysctl_write_str escape hatch. + Emitted once when any @sysctl global is present. *) +let sysctl_stdlib_helpers_c = {|/* sysctl stdlib helpers */ +static inline int __ks_sysctl_read_str(const char *dot_path, char *out, uint32_t out_len) { + char p[256]; size_t i = 0, j = 0; + j += snprintf(p + j, sizeof(p) - j, "/proc/sys/"); + while (dot_path[i] && j < sizeof(p) - 1) { + p[j++] = dot_path[i] == '.' ? '/' : dot_path[i]; i++; + } + p[j] = 0; + int fd = open(p, O_RDONLY); if (fd < 0) return -errno; + ssize_t n = read(fd, out, out_len - 1); int e = errno; close(fd); + if (n < 0) return -e; + if ((uint32_t)n == out_len - 1) return -ERANGE; + out[n] = 0; + if (n > 0 && out[n - 1] == '\n') out[n - 1] = 0; + return 0; +} + +static inline int __ks_sysctl_write_str(const char *dot_path, const char *value) { + char p[256]; size_t i = 0, j = 0; + j += snprintf(p + j, sizeof(p) - j, "/proc/sys/"); + while (dot_path[i] && j < sizeof(p) - 1) { + p[j++] = dot_path[i] == '.' ? '/' : dot_path[i]; i++; + } + p[j] = 0; + int fd = open(p, O_WRONLY); if (fd < 0) return -errno; + size_t l = strlen(value); + ssize_t w = write(fd, value, l); int e = errno; close(fd); + if (w < 0) return -e; + return 0; +}|} + (** Generate /proc/sys path constant and read/write accessors for a @sysctl global. *) let generate_sysctl_accessors_userspace (gv : ir_global_variable) = match gv.sysctl_path with @@ -1248,6 +1281,7 @@ static inline void __ks_sysctl_%s_write(%s v) { (** Generate ALL declarations in original source order for userspace - complete implementation *) let generate_declarations_in_source_order_userspace ir_multi_prog = let declarations = ref [] in + let sysctl_helpers_emitted = ref false in (* Process source declarations in their original order - handle ALL declaration types *) List.iter (fun source_decl -> @@ -1277,7 +1311,12 @@ let generate_declarations_in_source_order_userspace ir_multi_prog = (* Sysctl globals get inline accessors emitted here. Other globals are handled by the eBPF skeleton infrastructure. *) (match generate_sysctl_accessors_userspace global_var with - | Some accessors -> declarations := accessors :: !declarations + | Some accessors -> + if not !sysctl_helpers_emitted then begin + sysctl_helpers_emitted := true; + declarations := sysctl_stdlib_helpers_c :: !declarations + end; + declarations := accessors :: !declarations | None -> ()) | Ir.IRDeclFunctionDef _func_def -> diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 3943c5d..5e1ad63 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -266,6 +266,15 @@ let read_file p = let s = really_input_string ic (in_channel_length ic) in close_in ic; s +let test_stdlib_helpers_emitted () = + let c = user_c_of {| +@sysctl("net.core.somaxconn") var somaxconn: u32 +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { return 0 } +|} in + Alcotest.(check bool) "read helper defined" true (mentions "__ks_sysctl_read_str(" c); + Alcotest.(check bool) "write helper defined" true (mentions "__ks_sysctl_write_str(" c) + let test_e2e_compiles_example () = let example = "examples/sysctl_demo.ks" in if not (Sys.file_exists example) then Alcotest.skip () @@ -306,6 +315,9 @@ let () = Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; Alcotest.test_case "userspace rewrites load/store" `Quick test_userspace_rewrites_load_store; ]; + "stdlib", [ + Alcotest.test_case "stdlib helpers emitted" `Quick test_stdlib_helpers_emitted; + ]; "e2e", [ Alcotest.test_case "example file compiles" `Quick test_e2e_compiles_example; ]; From e996dcdfd426f774d2f5560dc0cffe24323404fd Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:47:49 -0700 Subject: [PATCH 11/14] docs(sysctl): document @sysctl attribute and stdlib helpers Signed-off-by: Cong Wang --- BUILTINS.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ SPEC.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/BUILTINS.md b/BUILTINS.md index 78c3d41..8bbed2b 100644 --- a/BUILTINS.md +++ b/BUILTINS.md @@ -334,6 +334,66 @@ fn main() -> i32 { --- +### 7. Sysctl Access (Escape Hatch) + +These two helpers exist for cases that don't fit the typed `@sysctl` global handle: dynamic paths chosen at runtime, or sysctls that hold multi-valued strings (e.g. `kernel.printk` = `"4 4 1 7"`). For static, single-valued sysctls prefer the typed handle (see `SPEC.md` §3.3.7) — it's safer. + +#### `sysctl_read_str(path, out, out_len)` +**Signature:** `sysctl_read_str(path: str(256), out: *u8, out_len: u32) -> i32` +**Variadic:** No +**Context:** Userspace only + +**Description:** Read a sysctl as a raw string. The path is given in dotted form (e.g. `"net.core.somaxconn"`); the helper rewrites it to `/proc/sys/...` internally. The trailing newline that `/proc/sys` files include is stripped. + +**Parameters:** +- `path`: dotted sysctl path +- `out`: destination byte buffer +- `out_len`: capacity of `out` (must be > 1 to leave room for the trailing NUL) + +**Return Value:** +- `0` on success +- Negative errno on failure (`-EACCES`, `-ENOENT`, `-ERANGE` if the buffer was too small, ...) + +**Example:** +```kernelscript +fn main() -> i32 { + var buf: u8[64] + var rc = sysctl_read_str("kernel.ostype", &buf, 64) + if (rc == 0) print("ostype=", buf) + return rc +} +``` + +#### `sysctl_write_str(path, value)` +**Signature:** `sysctl_write_str(path: str(256), value: str(256)) -> i32` +**Variadic:** No +**Context:** Userspace only + +**Description:** Write a string value to a sysctl. The path is given in dotted form. Useful for multi-valued sysctls or when the path is dynamic. + +**Parameters:** +- `path`: dotted sysctl path +- `value`: string value to write (newline not required) + +**Return Value:** +- `0` on success +- Negative errno on failure (`-EACCES`, `-EINVAL`, ...) + +**Example:** +```kernelscript +fn main() -> i32 { + var rc = sysctl_write_str("kernel.printk", "4 4 1 7") + return rc +} +``` + +**Context-specific implementations (both helpers):** +- **eBPF:** Not available +- **Userspace:** Generated as `static inline` C functions that perform `open` / `read` / `write` / `close` against `/proc/sys/...` +- **Kernel Module:** Not available + +--- + ## Context Availability Summary | Function | eBPF | Userspace | Kernel Module | Notes | @@ -347,6 +407,8 @@ fn main() -> i32 { | `dispatch()` | ❌ | ✅ | ❌ | Event processing only | | `daemon()` | ❌ | ✅ | ❌ | Process management only | | `exec()` | ❌ | ✅ | ❌ | Process replacement only | +| `sysctl_read_str()` | ❌ | ✅ | ❌ | Sysctl escape hatch | +| `sysctl_write_str()` | ❌ | ✅ | ❌ | Sysctl escape hatch | ## Related Concepts diff --git a/SPEC.md b/SPEC.md index 8e1e2cf..a3d10e7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -748,6 +748,69 @@ 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 +} +``` + +**Escape hatch:** for dynamic paths or sysctls that don't fit the typed-handle shape (multi-valued sysctls like `kernel.printk`), use the stdlib helpers `sysctl_read_str` and `sysctl_write_str` documented in `BUILTINS.md`. + ### 3.4 Kernel-Userspace Scoping Model KernelScript uses a simple and intuitive scoping model: From dd9ad4c9f58eaaa35a945814e5e0a53fab30ce2d Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:50:35 -0700 Subject: [PATCH 12/14] revert(sysctl): remove sysctl_read_str/sysctl_write_str stdlib helpers igned-off-by: Cong Wang --- BUILTINS.md | 62 ---------------------------------------- SPEC.md | 2 -- src/stdlib.ml | 22 -------------- src/userspace_codegen.ml | 41 +------------------------- tests/test_sysctl.ml | 12 -------- 5 files changed, 1 insertion(+), 138 deletions(-) diff --git a/BUILTINS.md b/BUILTINS.md index 8bbed2b..78c3d41 100644 --- a/BUILTINS.md +++ b/BUILTINS.md @@ -334,66 +334,6 @@ fn main() -> i32 { --- -### 7. Sysctl Access (Escape Hatch) - -These two helpers exist for cases that don't fit the typed `@sysctl` global handle: dynamic paths chosen at runtime, or sysctls that hold multi-valued strings (e.g. `kernel.printk` = `"4 4 1 7"`). For static, single-valued sysctls prefer the typed handle (see `SPEC.md` §3.3.7) — it's safer. - -#### `sysctl_read_str(path, out, out_len)` -**Signature:** `sysctl_read_str(path: str(256), out: *u8, out_len: u32) -> i32` -**Variadic:** No -**Context:** Userspace only - -**Description:** Read a sysctl as a raw string. The path is given in dotted form (e.g. `"net.core.somaxconn"`); the helper rewrites it to `/proc/sys/...` internally. The trailing newline that `/proc/sys` files include is stripped. - -**Parameters:** -- `path`: dotted sysctl path -- `out`: destination byte buffer -- `out_len`: capacity of `out` (must be > 1 to leave room for the trailing NUL) - -**Return Value:** -- `0` on success -- Negative errno on failure (`-EACCES`, `-ENOENT`, `-ERANGE` if the buffer was too small, ...) - -**Example:** -```kernelscript -fn main() -> i32 { - var buf: u8[64] - var rc = sysctl_read_str("kernel.ostype", &buf, 64) - if (rc == 0) print("ostype=", buf) - return rc -} -``` - -#### `sysctl_write_str(path, value)` -**Signature:** `sysctl_write_str(path: str(256), value: str(256)) -> i32` -**Variadic:** No -**Context:** Userspace only - -**Description:** Write a string value to a sysctl. The path is given in dotted form. Useful for multi-valued sysctls or when the path is dynamic. - -**Parameters:** -- `path`: dotted sysctl path -- `value`: string value to write (newline not required) - -**Return Value:** -- `0` on success -- Negative errno on failure (`-EACCES`, `-EINVAL`, ...) - -**Example:** -```kernelscript -fn main() -> i32 { - var rc = sysctl_write_str("kernel.printk", "4 4 1 7") - return rc -} -``` - -**Context-specific implementations (both helpers):** -- **eBPF:** Not available -- **Userspace:** Generated as `static inline` C functions that perform `open` / `read` / `write` / `close` against `/proc/sys/...` -- **Kernel Module:** Not available - ---- - ## Context Availability Summary | Function | eBPF | Userspace | Kernel Module | Notes | @@ -407,8 +347,6 @@ fn main() -> i32 { | `dispatch()` | ❌ | ✅ | ❌ | Event processing only | | `daemon()` | ❌ | ✅ | ❌ | Process management only | | `exec()` | ❌ | ✅ | ❌ | Process replacement only | -| `sysctl_read_str()` | ❌ | ✅ | ❌ | Sysctl escape hatch | -| `sysctl_write_str()` | ❌ | ✅ | ❌ | Sysctl escape hatch | ## Related Concepts diff --git a/SPEC.md b/SPEC.md index a3d10e7..92bc571 100644 --- a/SPEC.md +++ b/SPEC.md @@ -809,8 +809,6 @@ fn main() -> i32 { } ``` -**Escape hatch:** for dynamic paths or sysctls that don't fit the typed-handle shape (multi-valued sysctls like `kernel.printk`), use the stdlib helpers `sysctl_read_str` and `sysctl_write_str` documented in `BUILTINS.md`. - ### 3.4 Kernel-Userspace Scoping Model KernelScript uses a simple and intuitive scoping model: diff --git a/src/stdlib.ml b/src/stdlib.ml index b847f52..2e84eb0 100644 --- a/src/stdlib.ml +++ b/src/stdlib.ml @@ -210,28 +210,6 @@ let builtin_functions = [ kernel_impl = ""; (* Not available in kernel context *) validate = Some validate_exec_function; }; - { - name = "sysctl_read_str"; - param_types = [Str 256; Pointer U8; U32]; - return_type = I32; - description = "Read a sysctl as a raw string. Returns 0 or negative errno (userspace only)."; - is_variadic = false; - ebpf_impl = ""; (* Not available in eBPF context *) - userspace_impl = "__ks_sysctl_read_str"; - kernel_impl = ""; (* Not available in kernel context *) - validate = None; - }; - { - name = "sysctl_write_str"; - param_types = [Str 256; Str 256]; - return_type = I32; - description = "Write a string value to a sysctl. Returns 0 or negative errno (userspace only)."; - is_variadic = false; - ebpf_impl = ""; (* Not available in eBPF context *) - userspace_impl = "__ks_sysctl_write_str"; - kernel_impl = ""; (* Not available in kernel context *) - validate = None; - }; ] diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 9d34638..8a66c87 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -1154,39 +1154,6 @@ let generate_type_alias_definitions_userspace_from_ast type_aliases = "/* Type alias definitions */\n" ^ (String.concat "\n" type_alias_defs) ^ "\n\n" ) else "" -(** Static inline helpers for the sysctl_read_str / sysctl_write_str escape hatch. - Emitted once when any @sysctl global is present. *) -let sysctl_stdlib_helpers_c = {|/* sysctl stdlib helpers */ -static inline int __ks_sysctl_read_str(const char *dot_path, char *out, uint32_t out_len) { - char p[256]; size_t i = 0, j = 0; - j += snprintf(p + j, sizeof(p) - j, "/proc/sys/"); - while (dot_path[i] && j < sizeof(p) - 1) { - p[j++] = dot_path[i] == '.' ? '/' : dot_path[i]; i++; - } - p[j] = 0; - int fd = open(p, O_RDONLY); if (fd < 0) return -errno; - ssize_t n = read(fd, out, out_len - 1); int e = errno; close(fd); - if (n < 0) return -e; - if ((uint32_t)n == out_len - 1) return -ERANGE; - out[n] = 0; - if (n > 0 && out[n - 1] == '\n') out[n - 1] = 0; - return 0; -} - -static inline int __ks_sysctl_write_str(const char *dot_path, const char *value) { - char p[256]; size_t i = 0, j = 0; - j += snprintf(p + j, sizeof(p) - j, "/proc/sys/"); - while (dot_path[i] && j < sizeof(p) - 1) { - p[j++] = dot_path[i] == '.' ? '/' : dot_path[i]; i++; - } - p[j] = 0; - int fd = open(p, O_WRONLY); if (fd < 0) return -errno; - size_t l = strlen(value); - ssize_t w = write(fd, value, l); int e = errno; close(fd); - if (w < 0) return -e; - return 0; -}|} - (** Generate /proc/sys path constant and read/write accessors for a @sysctl global. *) let generate_sysctl_accessors_userspace (gv : ir_global_variable) = match gv.sysctl_path with @@ -1281,7 +1248,6 @@ static inline void __ks_sysctl_%s_write(%s v) { (** Generate ALL declarations in original source order for userspace - complete implementation *) let generate_declarations_in_source_order_userspace ir_multi_prog = let declarations = ref [] in - let sysctl_helpers_emitted = ref false in (* Process source declarations in their original order - handle ALL declaration types *) List.iter (fun source_decl -> @@ -1311,12 +1277,7 @@ let generate_declarations_in_source_order_userspace ir_multi_prog = (* Sysctl globals get inline accessors emitted here. Other globals are handled by the eBPF skeleton infrastructure. *) (match generate_sysctl_accessors_userspace global_var with - | Some accessors -> - if not !sysctl_helpers_emitted then begin - sysctl_helpers_emitted := true; - declarations := sysctl_stdlib_helpers_c :: !declarations - end; - declarations := accessors :: !declarations + | Some accessors -> declarations := accessors :: !declarations | None -> ()) | Ir.IRDeclFunctionDef _func_def -> diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 5e1ad63..3943c5d 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -266,15 +266,6 @@ let read_file p = let s = really_input_string ic (in_channel_length ic) in close_in ic; s -let test_stdlib_helpers_emitted () = - let c = user_c_of {| -@sysctl("net.core.somaxconn") var somaxconn: u32 -@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } -fn main() -> i32 { return 0 } -|} in - Alcotest.(check bool) "read helper defined" true (mentions "__ks_sysctl_read_str(" c); - Alcotest.(check bool) "write helper defined" true (mentions "__ks_sysctl_write_str(" c) - let test_e2e_compiles_example () = let example = "examples/sysctl_demo.ks" in if not (Sys.file_exists example) then Alcotest.skip () @@ -315,9 +306,6 @@ let () = Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; Alcotest.test_case "userspace rewrites load/store" `Quick test_userspace_rewrites_load_store; ]; - "stdlib", [ - Alcotest.test_case "stdlib helpers emitted" `Quick test_stdlib_helpers_emitted; - ]; "e2e", [ Alcotest.test_case "example file compiles" `Quick test_e2e_compiles_example; ]; From 3b029d3ed70c05bfb9df8c28f35e8ee7f386c46e Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:55:38 -0700 Subject: [PATCH 13/14] fix(sysctl): evaluate str-assignment source once Signed-off-by: Cong Wang --- src/userspace_codegen.ml | 8 ++++---- tests/test_sysctl.ml | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 8a66c87..3ab95c3 100644 --- a/src/userspace_codegen.ml +++ b/src/userspace_codegen.ml @@ -1812,7 +1812,7 @@ let generate_variable_assignment ctx dest src is_const = (* For string assignments, use safer approach to avoid truncation warnings *) let result = (match dest.val_type with | IRStr size -> - sprintf "%s{ size_t __src_len = strlen(%s); if (__src_len < %d) { strcpy(%s, %s); } else { strncpy(%s, %s, %d - 1); %s[%d - 1] = '\\0'; } }" assignment_prefix src_str size dest_str src_str dest_str src_str size dest_str size + sprintf "%s{ const char *__src = %s; size_t __src_len = strlen(__src); if (__src_len < %d) { strcpy(%s, __src); } else { strncpy(%s, __src, %d - 1); %s[%d - 1] = '\\0'; } }" assignment_prefix src_str size dest_str dest_str size dest_str size | _ -> sprintf "%s%s = %s;" assignment_prefix dest_str src_str) in @@ -1833,7 +1833,7 @@ let generate_variable_assignment ctx dest src is_const = (* For string assignments, use safer approach to avoid truncation warnings *) let result = (match dest.val_type with | IRStr size -> - sprintf "%s{ size_t __src_len = strlen(%s); if (__src_len < %d) { strcpy(%s, %s); } else { strncpy(%s, %s, %d - 1); %s[%d - 1] = '\\0'; } }" assignment_prefix src_str size dest_str src_str dest_str src_str size dest_str size + sprintf "%s{ const char *__src = %s; size_t __src_len = strlen(__src); if (__src_len < %d) { strcpy(%s, __src); } else { strncpy(%s, __src, %d - 1); %s[%d - 1] = '\\0'; } }" assignment_prefix src_str size dest_str dest_str size dest_str size | _ -> sprintf "%s%s = %s;" assignment_prefix dest_str src_str) in @@ -1902,10 +1902,10 @@ let rec generate_c_instruction_from_ir ctx instruction = (match init_expr.expr_desc with | IRValue (ir_val) when (match ir_val.value_desc with IRLiteral (StringLit _) -> true | _ -> false) -> (* Simple string literal - use safe initialization with length checking *) - sprintf "%s;\n { size_t __src_len = strlen(%s); if (__src_len < %d) { strcpy(%s, %s); } else { strncpy(%s, %s, %d - 1); %s[%d - 1] = '\\0'; } }" string_decl init_str size c_var_name init_str c_var_name init_str size c_var_name size + sprintf "%s;\n { const char *__src = %s; size_t __src_len = strlen(__src); if (__src_len < %d) { strcpy(%s, __src); } else { strncpy(%s, __src, %d - 1); %s[%d - 1] = '\\0'; } }" string_decl init_str size c_var_name c_var_name size c_var_name size | _ -> (* Complex expression (function call, concatenation, etc.) - use safe strcpy with length checking *) - sprintf "%s;\n { size_t __src_len = strlen(%s); if (__src_len < %d) { strcpy(%s, %s); } else { strncpy(%s, %s, %d - 1); %s[%d - 1] = '\\0'; } }" string_decl init_str size c_var_name init_str c_var_name init_str size c_var_name size) + sprintf "%s;\n { const char *__src = %s; size_t __src_len = strlen(__src); if (__src_len < %d) { strcpy(%s, __src); } else { strncpy(%s, __src, %d - 1); %s[%d - 1] = '\\0'; } }" string_decl init_str size c_var_name c_var_name size c_var_name size) | None -> sprintf "%s;" string_decl) | IRArray (element_type, size, _) -> diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml index 3943c5d..dd2477e 100644 --- a/tests/test_sysctl.ml +++ b/tests/test_sysctl.ml @@ -261,6 +261,34 @@ fn main() -> i32 { Alcotest.(check bool) "load → read call" true (mentions "__ks_sysctl_somaxconn_read(" c); Alcotest.(check bool) "store → write call" true (mentions "__ks_sysctl_somaxconn_write(" c) +(* Count how many times a substring appears in a string. *) +let count_occurrences needle haystack = + let nlen = String.length needle in + let rec loop i acc = + if i > String.length haystack - nlen then acc + else if String.sub haystack i nlen = needle then loop (i + nlen) (acc + 1) + else loop (i + 1) acc + in + loop 0 0 + +let test_str_sysctl_load_store () = + let c = user_c_of {| +@sysctl("kernel.hostname") var hostname: str(64) +@xdp fn p(ctx: *xdp_md) -> xdp_action { return 2 } +fn main() -> i32 { + var current: str(64) = hostname + hostname = "edge-01" + return 0 +} +|} in + Alcotest.(check bool) "str load → read call" true (mentions "__ks_sysctl_hostname_read(" c); + Alcotest.(check bool) "str store → write call" true (mentions "__ks_sysctl_hostname_write(\"edge-01\")" c); + (* Reading hostname into a local must not re-invoke the accessor multiple + times. The call count for the whole file is 1 (the load) plus 1 (the + accessor's own definition reference). *) + let calls = count_occurrences "__ks_sysctl_hostname_read(__ks_sb_hostname)" c in + Alcotest.(check int) "read called once per load" 1 calls + let read_file p = let ic = open_in p in let s = really_input_string ic (in_channel_length ic) in @@ -305,6 +333,7 @@ let () = Alcotest.test_case "eBPF codegen omits sysctl globals" `Quick test_ebpf_codegen_omits_sysctl_globals; Alcotest.test_case "userspace emits sysctl accessors" `Quick test_userspace_emits_accessors; Alcotest.test_case "userspace rewrites load/store" `Quick test_userspace_rewrites_load_store; + Alcotest.test_case "str sysctl load/store" `Quick test_str_sysctl_load_store; ]; "e2e", [ Alcotest.test_case "example file compiles" `Quick test_e2e_compiles_example; From a0574c0dfb4d6364f596a6cdb85d0fd5bb4e5e09 Mon Sep 17 00:00:00 2001 From: Cong Wang Date: Tue, 5 May 2026 18:58:15 -0700 Subject: [PATCH 14/14] test(strings): match new __src binding in assignment template Signed-off-by: Cong Wang --- tests/test_string_codegen.ml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_string_codegen.ml b/tests/test_string_codegen.ml index c4e3d18..fe15990 100644 --- a/tests/test_string_codegen.ml +++ b/tests/test_string_codegen.ml @@ -74,7 +74,8 @@ fn main() -> i32 { (* Should generate runtime length checking to avoid truncation warnings *) check bool "has strlen check" true (contains_pattern result "strlen.*__src_len"); - check bool "has strcpy for safe case" true (contains_pattern result "strcpy.*var_.*\"Hello\""); + check bool "binds source literal" true (contains_pattern result "__src = \"Hello\""); + check bool "has strcpy for safe case" true (contains_pattern result "strcpy.*var_.*__src"); check bool "has strncpy for truncation case" true (contains_pattern result "strncpy.*var_"); check bool "has explicit null termination" true (contains_pattern result "\\[.*\\].*=.*'\\\\0'"); @@ -166,8 +167,8 @@ fn main() -> i32 { check bool "uses comparison variable in if" true (contains_pattern result "if.*(__binop_"); (* Should have proper string assignments *) - check bool "has Alice assignment" true (contains_pattern result "strcpy.*var_.*\"Alice\""); - check bool "has Bob assignment" true (contains_pattern result "strcpy.*var_.*\"Bob\""); + check bool "has Alice assignment" true (contains_pattern result "__src = \"Alice\""); + check bool "has Bob assignment" true (contains_pattern result "__src = \"Bob\""); ) else ( failwith "Failed to generate userspace code file" ) @@ -222,9 +223,9 @@ fn main() -> i32 { let result = generate_userspace_code_from_program program_text "test_string_truncation" in (* Should handle all cases safely *) - check bool "has strlen checks" true (contains_pattern result "strlen.*\"toolong\""); - check bool "has safe strcpy path" true (contains_pattern result "strcpy.*var_.*\"exact\""); - check bool "has truncation path" true (contains_pattern result "strncpy.*var_.*\"toolong\".*[0-9]+.*-.*1"); + check bool "has strlen checks" true (contains_pattern result "__src = \"toolong\""); + check bool "has safe strcpy path" true (contains_pattern result "__src = \"exact\""); + check bool "has truncation path" true (contains_pattern result "strncpy.*var_.*__src.*[0-9]+.*-.*1"); check bool "explicit null termination" true (contains_pattern result "var_.*\\[.*-.*1\\].*=.*'\\\\0'"); (* Should have proper size checking - the runtime checks use the declared buffer size *) @@ -291,13 +292,13 @@ fn main() -> i32 { let result = generate_userspace_code_from_program program_text "test_edge_strings" in (* Should handle small strings safely *) - check bool "handles single char" true (contains_pattern result "strlen.*\"A\""); - check bool "handles empty string" true (contains_pattern result "strlen.*\"\""); + check bool "handles single char" true (contains_pattern result "__src = \"A\""); + check bool "handles empty string" true (contains_pattern result "__src = \"\""); check bool "size check for single" true (contains_pattern result "__src_len.*<.*2"); check bool "size check for empty buffer" true (contains_pattern result "__src_len.*<.*1"); (* Should still use safe string handling *) - check bool "safe assignment for single" true (contains_pattern result "strcpy.*var_.*\"A\""); + check bool "safe assignment for single" true (contains_pattern result "__src = \"A\""); with | exn -> fail ("Empty and single char strings test failed: " ^ Printexc.to_string exn)