From aeced89f46253e0b4813bbbd89362a0c4466f2d7 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 30 May 2026 21:34:13 +0300 Subject: test(rmdir): lock in FS family, args[0] pathname capture, UNCLASSIFIED exit Audit of the rmdir(2) syscall found the tracing implementation already correct and fully consistent with its siblings: rmdir is in the FS family, classified KindPathname with the pathname captured from args[0] (its generated BPF C handler is byte-identical to unlink's), and its exit is a ret_event with UNCLASSIFIED ret_type (rmdir returns int 0/-1, not a byte count). The docs and drift tests, integration tests (unlink-rmdir success and unlink-rmdir-notempty ENOTEMPTY failure), and retclassify coverage all already match. To guard against future drift, add a dedicated rmdir lock-in: - FormatRmdir tracepoint fixture (single const char * pathname at args[0], mirroring the real sys_enter_rmdir format and unlink's shape). - TestGenerateRmdirHandlerCapturesPathFromArgs0: asserts the generated handler reads the path from args[0] (with a negative guard against args[1], since rmdir has no dirfd) and that the exit stays UNCLASSIFIED. - TestRmdirFamilyAndKindMatchSiblings: asserts rmdir shares FamilyFS and KindPathname/pathname with unlink/unlinkat/mkdir. Co-Authored-By: Claude Opus 4.8 --- internal/generate/codegen_test.go | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) (limited to 'internal/generate/codegen_test.go') diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go index baf47d1..3178b1c 100644 --- a/internal/generate/codegen_test.go +++ b/internal/generate/codegen_test.go @@ -729,6 +729,53 @@ func TestMkdiratFamilyAndKindMatchSiblings(t *testing.T) { } } +// TestGenerateRmdirHandlerCapturesPathFromArgs0 locks in that rmdir(2) is a +// KindPathname event whose real filesystem path is read from args[0]. rmdir is +// "int rmdir(const char *pathname)" with a single pathname argument (no dirfd), +// so the path lives at args[0] — exactly like its single-pathname sibling +// unlink(2) and unlike the dirfd-relative mkdirat/unlinkat which read args[1]. +// A regression that dropped rmdir's path capture (while unlink/mkdir keep theirs) +// or read the wrong arg would surface here. The exit returns int 0/-1 (a status +// code, not a byte count), so the exit handler must stay UNCLASSIFIED. +func TestGenerateRmdirHandlerCapturesPathFromArgs0(t *testing.T) { + exitRmdir := strings.Replace(FormatExitRead, "sys_exit_read", "sys_exit_rmdir", 1) + exitRmdir = strings.Replace(exitRmdir, "ID: 843", "ID: 881", 1) + output := generateFromPair(t, FormatRmdir, exitRmdir) + + requireContains(t, output, `SEC("tracepoint/syscalls/sys_enter_rmdir")`) + requireContains(t, output, "struct path_event *ev") + requireContains(t, output, "ev->event_type = ENTER_PATH_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_RMDIR;") + requireContains(t, output, "__builtin_memset(&(ev->pathname), 0, sizeof(ev->pathname));") + requireContains(t, output, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[0]);") + // Negative guard: rmdir has no dirfd, so the path must NOT be read from args[1]. + requireNotContains(t, output, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[1]);") + // Return value is a 0/-1 status code, not a byte count: UNCLASSIFIED. + requireContains(t, output, "ev->ret_type = UNCLASSIFIED;") +} + +// TestRmdirFamilyAndKindMatchSiblings locks in that rmdir shares the same FS +// family and KindPathname classification as its directory/link removal siblings +// unlink/unlinkat/mkdir. A drift here (e.g. rmdir slipping into Misc, or losing +// its pathname capture) would split related path-based syscalls across families +// in the dashboard and drop rmdir's path from the trace. +func TestRmdirFamilyAndKindMatchSiblings(t *testing.T) { + for _, syscall := range []string{"rmdir", "unlink", "unlinkat", "mkdir"} { + if got := ClassifySyscallFamily("sys_enter_" + syscall); got != FamilyFS { + t.Errorf("%s family = %q, want %q", syscall, got, FamilyFS) + } + } + + rmdir := mustParseOne(t, FormatRmdir) + if r := ClassifyFormat(&rmdir); r.Kind != KindPathname || r.PathnameField != "pathname" { + t.Errorf("rmdir classified as kind=%d field=%q, want KindPathname/pathname", r.Kind, r.PathnameField) + } + // rmdir has no dirfd, so the pathname is the first real argument: args[0]. + if n := rmdir.FieldNumber("pathname"); n != 0 { + t.Errorf("rmdir FieldNumber(pathname) = %d, want 0", n) + } +} + func TestGenerateExecHandler(t *testing.T) { output := generateFromPair(t, FormatExecveat, FormatExitExecveat) -- cgit v1.2.3