summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-29 21:57:42 +0300
committerPaul Buetow <paul@buetow.org>2026-05-29 21:57:42 +0300
commit8e88f743dfcdd2b347b24dcbfab3a04fbe3c43f1 (patch)
tree7a43d605d2ecaa035c16d2366dbccf6166ec0b29
parentb86b817594ac8a4dc7fe2b80649df2c7a62f1b59 (diff)
test(generate): lock in clone3 KindProc/Process/UNCLASSIFIED classification
Audit of clone3(2): long clone3(struct clone_args *cl_args, size_t size). Neither arg is an fd or filesystem path (cl_args is a userspace control block, size is its byte length), and the return value is a pid_t (child PID in the parent, 0 in the child, -1 on error) — not a byte count. clone3 was already correctly classified as KindProc in FamilyProcess with an UNCLASSIFIED exit, identical to its clone/fork/vfork siblings; the generated BPF handlers emit a null_event on enter (no arg capture) and a ret_event tagged UNCLASSIFIED on exit. No code, classification, or doc changes were needed. Add TestGenerateClone3Handler to pin this down against future drift: the name-only table maps all four siblings to KindProc even when fed the real (struct clone_args *, size_t) args, the family is Process, the ret is UNCLASSIFIED, the enter handler captures no ctx->args[], and the exit handler reports ret_type=UNCLASSIFIED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--internal/generate/codegen_test.go73
1 files changed, 73 insertions, 0 deletions
diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go
index 2c69cd9..62f718b 100644
--- a/internal/generate/codegen_test.go
+++ b/internal/generate/codegen_test.go
@@ -149,6 +149,79 @@ func TestGenerateRtSigpendingHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
+// TestGenerateClone3Handler locks in how clone3(2) is generated. Per the man
+// page:
+//
+// long clone3(struct clone_args *cl_args, size_t size)
+//
+// clone3 is the modern superset of clone/fork/vfork: it creates a new process
+// or thread. args[0] is a userspace pointer to a struct clone_args (a control
+// block, not an fd or filesystem path) and args[1] is its byte size. The return
+// value is a pid_t: the child's PID in the parent, 0 in the child, or -1 on
+// error — never a byte count. ior therefore classifies clone3 as KindProc in
+// FamilyProcess, identical to its siblings clone/fork/vfork. Consequently:
+// - The enter handler emits a struct null_event and must NOT capture args[0]
+// (the clone_args pointer) or args[1] (size) as an fd/path/addr — neither is
+// a traced I/O resource.
+// - The exit handler reports the raw pid/0/-1 status as UNCLASSIFIED; it is not
+// a byte count, so it must never be tagged READ/WRITE/TRANSFER.
+//
+// This guards against a misclassification to KindNull (which would still emit a
+// null_event but break sibling/dimension consistency) or to any fd/path kind
+// (which would wrongly treat the clone_args pointer as a resource).
+func TestGenerateClone3Handler(t *testing.T) {
+ // Classification consistency with the clone/fork/vfork siblings. clone3's
+ // real tracepoint args are (struct clone_args *, size_t); we feed a generic
+ // pointer arg here to prove the name-only table — not a field heuristic —
+ // pins all four siblings to KindProc.
+ for _, name := range []string{"sys_enter_clone3", "sys_enter_clone", "sys_enter_fork", "sys_enter_vfork"} {
+ r := ClassifyFormat(&Format{
+ Name: name,
+ ExternalFields: []Field{
+ {Type: "long", Name: "__syscall_nr"},
+ {Type: "struct clone_args *", Name: "uargs"},
+ {Type: "unsigned long", Name: "size"},
+ },
+ })
+ if r.Kind != KindProc {
+ t.Errorf("%s kind = %v, want KindProc", name, r.Kind)
+ }
+ }
+ if got := ClassifySyscallFamily("sys_enter_clone3"); got != FamilyProcess {
+ t.Errorf("clone3 family = %q, want %q", got, FamilyProcess)
+ }
+ if got := ClassifyRet("sys_exit_clone3"); got != Unclassified {
+ t.Errorf("clone3 ret classification = %q, want %q (pid, not a byte count)", got, Unclassified)
+ }
+
+ output := GenerateTracepointsC(mustParseAll(t, syntheticPair("clone3")))
+
+ enterSec := `SEC("tracepoint/syscalls/sys_enter_clone3")`
+ exitSec := `SEC("tracepoint/syscalls/sys_exit_clone3")`
+ requireContains(t, output, enterSec)
+ requireContains(t, output, "struct null_event *ev")
+ requireContains(t, output, "ev->event_type = ENTER_NULL_EVENT;")
+ requireContains(t, output, "ev->trace_id = SYS_ENTER_CLONE3;")
+
+ // The KindProc enter handler must not wire args[0] (clone_args ptr) or
+ // args[1] (size) as an fd/path/addr — neither is a traced I/O resource.
+ // Scope to the enter handler body (from the enter SEC up to the exit SEC).
+ enterStart := strings.Index(output, enterSec)
+ exitStart := strings.Index(output, exitSec)
+ if enterStart < 0 || exitStart < 0 || exitStart <= enterStart {
+ t.Fatalf("clone3: handlers not found in expected order")
+ }
+ enterBody := output[enterStart:exitStart]
+ if strings.Contains(enterBody, "ctx->args[") {
+ t.Error("clone3 must be KindProc: enter handler must not capture any arg")
+ }
+
+ // The exit handler reports the raw pid/0/-1 status as UNCLASSIFIED.
+ requireContains(t, output, exitSec)
+ requireContains(t, output, "ev->ret = ctx->ret;")
+ requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
+}
+
// TestGenerateSigaltstackHandler locks in how sigaltstack(2) is generated. Per
// the man page:
//