diff --git a/SPEC.md b/SPEC.md index 8e1e2cf..92bc571 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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: 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/src/ast.ml b/src/ast.ml index 3ff6ae4..8e90540 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) ?(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 = { @@ -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 @@ -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 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/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/src/parser.mly b/src/parser.mly index 1f90250..861e596 100644 --- a/src/parser.mly +++ b/src/parser.mly @@ -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: diff --git a/src/type_checker.ml b/src/type_checker.ml index 8a95a99..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; @@ -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 @@ -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 *) @@ -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 -> @@ -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 -> @@ -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; @@ -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 -> @@ -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; diff --git a/src/userspace_codegen.ml b/src/userspace_codegen.ml index 0c07f08..3ab95c3 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 *) @@ -1307,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 @@ -1690,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 @@ -1706,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 @@ -1727,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 @@ -1796,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/dune b/tests/dune index 25142e2..c9835b9 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))) 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_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) diff --git a/tests/test_sysctl.ml b/tests/test_sysctl.ml new file mode 100644 index 0000000..dd2477e --- /dev/null +++ b/tests/test_sysctl.ml @@ -0,0 +1,341 @@ +(* + * 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 + +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 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 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 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 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 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 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 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) + +(* 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 + 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", [ + 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; + ]; + "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; + 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; + ]; + "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; + ]; + "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; + 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; + ]; + ] 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