Skip to content

Commit d9ededa

Browse files
committed
Enforce return type constraint for eBPF probe functions to i32 across the codebase.
Signed-off-by: Cong Wang <cwang@multikernel.io>
1 parent 5c7dc67 commit d9ededa

File tree

5 files changed

+54
-19
lines changed

5 files changed

+54
-19
lines changed

SPEC.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,12 @@ fn arbitrary_address() -> i32 {
243243
- **Intelligent Probe Selection**: Automatically chooses fprobe for function entrance (better performance) or kprobe for arbitrary addresses
244244
- **Type Safety**: Function entrance probes have correct types extracted from kernel BTF information
245245

246+
**Return Type Constraint:**
247+
- **All probe functions must return `i32`** due to eBPF's `BPF_PROG()` macro constraint
248+
- The return value controls execution flow: `0` = continue normally, non-zero = may alter behavior
249+
- This applies regardless of the target kernel function's actual return type (which may be `void`, pointers, etc.)
250+
- BTF function signature extraction automatically converts all return types to `i32` for consistency
251+
246252
#### 3.1.2 Traffic Control (TC) Programs with Direction Support
247253

248254
TC programs must specify traffic direction for proper kernel attachment point selection.

examples/probe_do_exit.ks

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@
55
// We print the exit code parameter to see why processes are exiting.
66

77
// Target kernel function signature:
8-
// do_exit(code: i64) -> void
8+
// do_exit(code: i64) -> void (in kernel)
99
//
1010
// The 'code' parameter contains the exit status/signal that caused
1111
// the process to exit. In the kernel, it's declared as 'long' (signed 64-bit).
12+
//
13+
// Note: eBPF probe functions must return i32 due to BPF_PROG() constraint,
14+
// regardless of the target kernel function's return type.
1215

1316

1417
@probe("do_exit")
15-
fn do_exit(code: i64) -> void {
18+
fn do_exit(code: i64) -> i32 {
1619
// Print the exit code parameter
1720
// This will show us the exit status/signal for the exiting process
1821
print("Process exiting with code: %ld", code)
19-
return 0
22+
return 0 // Continue normal execution
2023
}
2124

2225
fn main() -> i32 {

src/btf_stubs.c

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,24 @@ static char* resolve_type_to_string(struct btf *btf, int type_id) {
311311
}
312312

313313
/* Helper function to format function prototype */
314-
static char* format_function_prototype(struct btf *btf, const struct btf_type *func_proto) {
314+
static char* format_function_prototype(struct btf *btf, const struct btf_type *func_proto, int force_i32_return) {
315315
char result[1024];
316316
int ret_type_id = func_proto->type;
317317
int param_count = btf_vlen(func_proto);
318318

319319
/* Get return type */
320-
char *ret_type = resolve_type_to_string(btf, ret_type_id);
320+
char *original_ret_type = resolve_type_to_string(btf, ret_type_id);
321+
322+
/* Use original return type unless forced to i32 for eBPF probe/kfunc functions */
323+
const char *ret_type;
324+
if (force_i32_return) {
325+
/* eBPF probe/kfunc functions must return i32 due to BPF_PROG() constraint,
326+
* regardless of the kernel function's actual return type */
327+
ret_type = "i32";
328+
} else {
329+
/* Preserve original return type for struct_ops and other contexts */
330+
ret_type = original_ret_type;
331+
}
321332

322333
/* Start building the function signature */
323334
snprintf(result, sizeof(result), "fn(");
@@ -353,7 +364,7 @@ static char* format_function_prototype(struct btf *btf, const struct btf_type *f
353364
snprintf(closing, sizeof(closing), ") -> %s", ret_type);
354365
strncat(result, closing, sizeof(result) - strlen(result) - 1);
355366

356-
free(ret_type);
367+
free(original_ret_type);
357368
return strdup(result);
358369
}
359370

@@ -384,7 +395,7 @@ value btf_resolve_type_stub(value btf_handle, value type_id) {
384395
/* Check if this points to a function prototype */
385396
const struct btf_type *target = btf__type_by_id(btf, t->type);
386397
if (target && btf_kind(target) == BTF_KIND_FUNC_PROTO) {
387-
char *func_sig = format_function_prototype(btf, target);
398+
char *func_sig = format_function_prototype(btf, target, 0); /* Don't force i32 for general BTF resolution */
388399
value result = caml_copy_string(func_sig);
389400
free(func_sig);
390401
CAMLreturn(result);
@@ -474,7 +485,7 @@ value btf_resolve_type_stub(value btf_handle, value type_id) {
474485
CAMLreturn(caml_copy_string("fwd"));
475486
}
476487
case BTF_KIND_FUNC_PROTO: {
477-
char *func_sig = format_function_prototype(btf, t);
488+
char *func_sig = format_function_prototype(btf, t, 0); /* Don't force i32 for general BTF resolution */
478489
value result = caml_copy_string(func_sig);
479490
free(func_sig);
480491
CAMLreturn(result);
@@ -573,8 +584,8 @@ value btf_extract_function_signatures_stub(value btf_handle, value function_name
573584
/* Get the function prototype */
574585
const struct btf_type *func_proto = btf__type_by_id(btf, t->type);
575586
if (func_proto && btf_kind(func_proto) == BTF_KIND_FUNC_PROTO) {
576-
/* Extract function signature */
577-
char *signature = format_function_prototype(btf, func_proto);
587+
/* Extract function signature - force i32 return for probe functions */
588+
char *signature = format_function_prototype(btf, func_proto, 1);
578589

579590
/* Create tuple (function_name, signature) */
580591
tuple = caml_alloc_tuple(2);
@@ -665,8 +676,7 @@ value btf_extract_kfuncs_stub(value btf_handle) {
665676
/* Get the function prototype */
666677
const struct btf_type *func_proto = btf__type_by_id(btf, target_func->type);
667678
if (func_proto && btf_kind(func_proto) == BTF_KIND_FUNC_PROTO) {
668-
/* Extract function signature */
669-
char *signature = format_function_prototype(btf, func_proto);
679+
char *signature = format_function_prototype(btf, func_proto, 0);
670680

671681
/* Create tuple (function_name, signature) */
672682
tuple = caml_alloc_tuple(2);

src/type_checker.ml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3054,16 +3054,14 @@ let rec type_check_and_annotate_ast ?symbol_table:(provided_symbol_table=None) ?
30543054
failwith (sprintf "Internal error: %s without target function should have been rejected earlier" probe_type_name)
30553055
);
30563056

3057-
(* Allow both i32 (standard eBPF probe return) and void (matching kernel function signature) *)
3057+
(* Require i32 return type for eBPF probe functions - BPF_PROG() always returns int *)
30583058
let valid_return_type = match resolved_return_type with
30593059
| Some I32 -> true (* Standard eBPF probe return type *)
3060-
| Some Void -> true (* Allow void to match kernel function signatures *)
3061-
| Some U32 -> true (* Allow u32 as alternative to i32 *)
30623060
| _ -> false
30633061
in
30643062

30653063
if not valid_return_type then
3066-
type_error (sprintf "@%s attributed function must return i32, u32, or void" probe_type_name) attr_func.attr_pos
3064+
type_error (sprintf "@%s attributed function must return i32" probe_type_name) attr_func.attr_pos
30673065
| Some _ -> () (* Other program types - validation can be added later *)
30683066
| None -> type_error ("Invalid or unsupported attribute") attr_func.attr_pos);
30693067

tests/test_probe.ml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,9 @@ fn sys_read_handler(fd: u32, buf: *u8, count: usize) -> i32 {
172172
| _ -> fail "Expected AttributedFunction"
173173

174174
let test_probe_return_type_validation _ =
175-
(* Test valid return types for kprobe *)
175+
(* Test valid return types for kprobe - only i32 allowed due to BPF_PROG() constraint *)
176176
let test_cases = [
177177
("i32", "fn handler() -> i32 { return 0 }");
178-
("void", "fn handler() -> void { }");
179-
("u32", "fn handler() -> u32 { return 0 }");
180178
] in
181179

182180
List.iter (fun (ret_type, func_def) ->
@@ -186,6 +184,25 @@ let test_probe_return_type_validation _ =
186184
check int (Printf.sprintf "Type checking should succeed for %s return type" ret_type) 1 (List.length typed_ast)
187185
) test_cases
188186

187+
let test_probe_invalid_return_types _ =
188+
(* Test that void, u32, and other invalid return types are rejected for probe functions *)
189+
let invalid_cases = [
190+
("void", "fn handler() -> void { }");
191+
("u32", "fn handler() -> u32 { return 0 }");
192+
("str", "fn handler() -> str(32) { return \"test\" }");
193+
("bool", "fn handler() -> bool { return true }");
194+
] in
195+
196+
List.iter (fun (ret_type, func_def) ->
197+
let source = "@probe(\"sys_read\")\n" ^ func_def in
198+
try
199+
let ast = parse_string source in
200+
let _ = type_check_ast ast in
201+
fail (Printf.sprintf "Should have rejected %s return type for probe function" ret_type)
202+
with
203+
| _ -> check bool (Printf.sprintf "Correctly rejected %s return type" ret_type) true true
204+
) invalid_cases
205+
189206
let test_probe_too_many_parameters _ =
190207
(* Test rejection of functions with more than 6 parameters *)
191208
let source = "@probe(\"invalid_function\")
@@ -579,6 +596,7 @@ let type_checking_tests = [
579596
"probe type checking", `Quick, test_probe_type_checking;
580597
"probe parameter validation", `Quick, test_probe_parameter_validation;
581598
"probe return type validation", `Quick, test_probe_return_type_validation;
599+
"probe invalid return types", `Quick, test_probe_invalid_return_types;
582600
"probe too many parameters", `Quick, test_probe_too_many_parameters;
583601
"probe pt_regs rejection", `Quick, test_probe_pt_regs_rejection;
584602
]

0 commit comments

Comments
 (0)