summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-29 22:56:49 +0300
committerPaul Buetow <paul@buetow.org>2026-05-29 22:56:49 +0300
commit3cd431e7aac28fa7bacc37a7e751a9082287251d (patch)
tree390e929878878a34a98de35e1a3d485e0c721f50
parent9789578fe1363ec2f183cffb48f22bd6b656fd1a (diff)
test(mknod): lock in mknod/mknodat path-arg classification
Audit of mknod(2) found the tracing implementation already correct: sys_enter_mknod captures the real pathname from args[0] (no dirfd), while the sibling sys_enter_mknodat captures it from args[1] (after dirfd). Both are FamilyFS path_events; both exits are ret_event UNCLASSIFIED (int 0/-1). No code or doc changes were needed. Add lock-in tests guarding this behavior against regressions: - TestGenerateMknodMknodatHandlers asserts the generated BPF C reads the path from args[0] for mknod and args[1] for mknodat. - FormatMknodat/FormatExitMknodat testdata mirroring the real tracepoint layout (dfd pushes filename to args[1]). - mknodat rows added to the classify kind (KindPathname) and family (FamilyFS) test tables, matching the existing mknod coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--internal/generate/classify_test.go2
-rw-r--r--internal/generate/codegen_test.go29
-rw-r--r--internal/generate/testdata.go38
3 files changed, 69 insertions, 0 deletions
diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go
index 25c7068..5b233c2 100644
--- a/internal/generate/classify_test.go
+++ b/internal/generate/classify_test.go
@@ -1692,6 +1692,7 @@ func TestClassifySyscallPairAccepted(t *testing.T) {
{"pread64", FormatPread64, FormatExitPread64, KindFd},
{"symlink", FormatSymlink, FormatExitSymlink, KindName},
{"mknod", FormatMknod, FormatExitMknod, KindPathname},
+ {"mknodat", FormatMknodat, FormatExitMknodat, KindPathname},
{"execve", FormatExecve, FormatExitExecve, KindExec},
{"execveat", FormatExecveat, FormatExitExecveat, KindExec},
{"accept", FormatAccept, FormatExitAccept, KindAccept},
@@ -1806,6 +1807,7 @@ func TestClassifySyscallPairEmitsAllFamilies(t *testing.T) {
family SyscallFamily
}{
{"mknod", FormatMknod, FormatExitMknod, FamilyFS},
+ {"mknodat", FormatMknodat, FormatExitMknodat, FamilyFS},
{"execve", FormatExecve, FormatExitExecve, FamilyProcess},
{"execveat", FormatExecveat, FormatExitExecveat, FamilyProcess},
{"accept", FormatAccept, FormatExitAccept, FamilyNetwork},
diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go
index 5179fee..7e0e122 100644
--- a/internal/generate/codegen_test.go
+++ b/internal/generate/codegen_test.go
@@ -631,6 +631,35 @@ func TestGenerateAccessFaccessatHandlers(t *testing.T) {
requireContains(t, faccessatOut, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[1]);")
}
+// TestGenerateMknodMknodatHandlers locks in the generated BPF C for mknod(2)
+// and its dirfd-relative sibling mknodat(2). Both create a filesystem node and
+// capture a real path into a path_event's pathname member, but from DIFFERENT
+// argument slots: mknod(2) has no dirfd so its path is at args[0], whereas
+// mknodat(2) takes dfd at args[0] and the path at args[1]. This guards against
+// a regression that would read the wrong arg (e.g. capturing mknodat's dirfd
+// as a path, or dropping mknod's path entirely). The exit side is a ret_event
+// (int 0/-1, UNCLASSIFIED) — verified via the shared ret_event handler shape.
+func TestGenerateMknodMknodatHandlers(t *testing.T) {
+ exitMknod := strings.Replace(FormatExitRead, "sys_exit_read", "sys_exit_mknod", 1)
+ exitMknod = strings.Replace(exitMknod, "ID: 843", "ID: 893", 1)
+ mknodOut := generateFromPair(t, FormatMknod, exitMknod)
+ requireContains(t, mknodOut, `SEC("tracepoint/syscalls/sys_enter_mknod")`)
+ requireContains(t, mknodOut, "struct path_event *ev")
+ requireContains(t, mknodOut, "ev->event_type = ENTER_PATH_EVENT;")
+ requireContains(t, mknodOut, "ev->trace_id = SYS_ENTER_MKNOD;")
+ // mknod(2): path (filename) is at args[0] — no dirfd precedes it.
+ requireContains(t, mknodOut, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[0]);")
+
+ exitMknodat := strings.Replace(FormatExitRead, "sys_exit_read", "sys_exit_mknodat", 1)
+ exitMknodat = strings.Replace(exitMknodat, "ID: 843", "ID: 895", 1)
+ mknodatOut := generateFromPair(t, FormatMknodat, exitMknodat)
+ requireContains(t, mknodatOut, `SEC("tracepoint/syscalls/sys_enter_mknodat")`)
+ requireContains(t, mknodatOut, "struct path_event *ev")
+ requireContains(t, mknodatOut, "ev->trace_id = SYS_ENTER_MKNODAT;")
+ // mknodat(2): dfd is at args[0], so the path (filename) is at args[1].
+ requireContains(t, mknodatOut, "bpf_probe_read_user_str(ev->pathname, sizeof(ev->pathname), (void*)ctx->args[1]);")
+}
+
func TestGenerateFcntlHandler(t *testing.T) {
output := generateFromPair(t, FormatFcntl, FormatExitFcntl)
diff --git a/internal/generate/testdata.go b/internal/generate/testdata.go
index b8f8f17..25392b1 100644
--- a/internal/generate/testdata.go
+++ b/internal/generate/testdata.go
@@ -1228,6 +1228,44 @@ format:
print fmt: "filename: 0x%08lx, mode: 0x%08lx, dev: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->mode)), ((unsigned long)(REC->dev))
`
+// FormatMknodat mirrors the real sys_enter_mknodat tracepoint format. Unlike
+// mknod(2), mknodat(2) takes a directory fd (dfd) as its first argument, which
+// pushes the filename (the real path) to args[1]. The classifier must capture
+// the path from args[1] here, not args[0] (which is the dirfd).
+const FormatMknodat = `name: sys_enter_mknodat
+ID: 896
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:int dfd; offset:16; size:8; signed:0;
+ field:const char * filename; offset:24; size:8; signed:0;
+ field:umode_t mode; offset:32; size:8; signed:0;
+ field:unsigned dev; offset:40; size:8; signed:0;
+
+print fmt: "dfd: 0x%08lx, filename: 0x%08lx, mode: 0x%08lx, dev: 0x%08lx", ((unsigned long)(REC->dfd)), ((unsigned long)(REC->filename)), ((unsigned long)(REC->mode)), ((unsigned long)(REC->dev))
+`
+
+// FormatExitMknodat mirrors the real sys_exit_mknodat tracepoint format. Like
+// mknod, mknodat returns a plain int (0 on success, -1 on error) and is
+// therefore classified as a ret_event (UNCLASSIFIED return value).
+const FormatExitMknodat = `name: sys_exit_mknodat
+ID: 895
+format:
+ field:unsigned short common_type; offset:0; size:2; signed:0;
+ field:unsigned char common_flags; offset:2; size:1; signed:0;
+ field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
+ field:int common_pid; offset:4; size:4; signed:1;
+
+ field:int __syscall_nr; offset:8; size:4; signed:1;
+ field:long ret; offset:16; size:8; signed:1;
+
+print fmt: "0x%lx", REC->ret
+`
+
const FormatExitMknod = `name: sys_exit_mknod
ID: 893
format: