From 2bb05af8e0b08910c01045d7cd7cd375e6b83613 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 1 Jun 2026 10:07:16 +0300 Subject: test(generate): remove redundant pure-classification unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classification correctness (which family/kind/return-class a syscall maps to) is verified by inspection against the man pages and the classifier rules, not by dedicated unit tests. The tracing-relevant outcome — which fd/path/byte-count the generated BPF C actually captures — is covered by the GenerateTracepointsC codegen tests and the end-to-end integration tests, all of which are retained. Removed: - internal/generate/family_test.go (ClassifySyscallFamily / .Family table) - internal/generate/retclassify_test.go (ClassifyRet read/write/transfer/ unclassified tables) - ~70 pure-classification tests trimmed from classify_test.go, keeping only the GenerateTracepointsC codegen/tracing tests plus the shared helpers (mustParseAll, mqFormats, phaseAFormats, syntheticEnter/Exit, itoa) used by codegen_test.go. - pure-classification funcs interleaved in codegen_test.go (TestClassifyRet*Unclassified, TestClassifyTkillFallsThroughToNull, Test{Mkdirat,Rmdir}FamilyAndKindMatchSiblings). Kept all TestGenerate* handler tests (they assert the generated BPF C captures the correct fd/path/arg-index/return classification), the isNoreturnSyscall tests, docs-drift guards, eventloop dispatch tests, and the integration suite — so every affected syscall still has tracing coverage. No tracing gaps discovered. generate package: go test (incl. -race) green; mage build green. Co-Authored-By: Claude Opus 4.8 --- internal/generate/classify_test.go | 3231 +-------------------------------- internal/generate/codegen_test.go | 166 -- internal/generate/family_test.go | 365 ---- internal/generate/retclassify_test.go | 185 -- 4 files changed, 54 insertions(+), 3893 deletions(-) delete mode 100644 internal/generate/family_test.go delete mode 100644 internal/generate/retclassify_test.go (limited to 'internal') diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go index 2f74e8a..bd03af4 100644 --- a/internal/generate/classify_test.go +++ b/internal/generate/classify_test.go @@ -6,2312 +6,68 @@ import ( "testing" ) -func classifyFromData(t *testing.T, data string) ClassificationResult { - t.Helper() - f := mustParseOne(t, data) - return ClassifyFormat(&f) -} - -func TestClassifyFdRead(t *testing.T) { - r := classifyFromData(t, FormatRead) - if r.Kind != KindFd { - t.Errorf("read: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyFdClose(t *testing.T) { - r := classifyFromData(t, FormatClose) - if r.Kind != KindFd { - t.Errorf("close: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyFdPread64(t *testing.T) { - r := classifyFromData(t, FormatPread64) - if r.Kind != KindFd { - t.Errorf("pread64: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyFdWrite(t *testing.T) { - r := classifyFromData(t, FormatWrite) - if r.Kind != KindFd { - t.Errorf("write: got kind %d, want KindFd", r.Kind) - } -} - -// TestClassifyFdLseek pins lseek(2) as a single-fd KindFd event. lseek's -// tracepoint exposes a generic "fd" field of an fd-like type at args[0], so it -// classifies via classifyByField exactly like read/write — the fd is captured -// from args[0], while the off_t offset and whence args are ignored. The return -// value (resulting file offset) is asserted UNCLASSIFIED separately in -// retclassify_test.go (TestClassifyRetUnclassified) and end-to-end in -// TestClassifyRetExitLseek below. -func TestClassifyFdLseek(t *testing.T) { - r := classifyFromData(t, FormatLseek) - if r.Kind != KindFd { - t.Errorf("lseek: got kind %d, want KindFd", r.Kind) - } -} - -// TestClassifyRetExitLseek locks in that sys_exit_lseek is a plain ret_event -// (KindRet) and that ClassifyRet keeps it UNCLASSIFIED. lseek returns the new -// file OFFSET (bytes-from-start), not a transferred byte count, so it must -// never be classified as READ/WRITE/TRANSFER — doing so would inflate I/O byte -// accounting. -func TestClassifyRetExitLseek(t *testing.T) { - r := classifyFromData(t, FormatExitLseek) - if r.Kind != KindRet { - t.Errorf("lseek exit: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_lseek"); got != Unclassified { - t.Errorf("lseek exit: ClassifyRet = %q, want UNCLASSIFIED", got) - } -} - -func TestClassifyFdPidfdGetfd(t *testing.T) { - r := classifyFromData(t, FormatPidfdGetfd) - if r.Kind != KindFd { - t.Errorf("pidfd_getfd: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyOpenOpenat(t *testing.T) { - r := classifyFromData(t, FormatOpenat) - if r.Kind != KindOpen { - t.Errorf("openat: got kind %d, want KindOpen", r.Kind) - } -} - -func TestClassifyOpenOpen(t *testing.T) { - r := classifyFromData(t, FormatOpen) - if r.Kind != KindOpen { - t.Errorf("open: got kind %d, want KindOpen", r.Kind) - } -} - -func TestClassifyOpenOpenat2(t *testing.T) { - r := classifyFromData(t, FormatOpenat2) - if r.Kind != KindOpen { - t.Errorf("openat2: got kind %d, want KindOpen", r.Kind) - } -} - -func TestClassifyPathnameCreat(t *testing.T) { - r := classifyFromData(t, FormatCreat) - if r.Kind != KindPathname { - t.Errorf("creat: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "pathname" { - t.Errorf("creat: PathnameField = %q, want pathname", r.PathnameField) - } -} - -func TestClassifyPathnameUnlink(t *testing.T) { - r := classifyFromData(t, FormatUnlink) - if r.Kind != KindPathname { - t.Errorf("unlink: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "pathname" { - t.Errorf("unlink: PathnameField = %q, want pathname", r.PathnameField) - } -} - -// TestClassifyPathnameUtime locks in that utime's args[0] "filename" is -// captured as a real path. utime(2) changes a file's access/modification -// times; its filename argument is a genuine filesystem path (not a -// domain/host name string), so it must classify as KindPathname with the -// path wired to the "filename" field — matching siblings utimensat/futimesat. -func TestClassifyPathnameUtime(t *testing.T) { - r := classifyFromData(t, FormatUtime) - if r.Kind != KindPathname { - t.Errorf("utime: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "filename" { - t.Errorf("utime: PathnameField = %q, want filename", r.PathnameField) - } -} - -// TestClassifyPathnameAccess locks in that access(2)'s args[0] argument -// (kernel field "filename") is captured as a real filesystem path. access(2) -// checks the calling process's permissions for a file by path; the path is at -// args[0] (there is no dirfd), so it must classify as KindPathname with the -// path wired to the "filename" field. If this regresses to a non-path kind, -// access's pathname would silently stop being captured. -func TestClassifyPathnameAccess(t *testing.T) { - r := classifyFromData(t, FormatAccess) - if r.Kind != KindPathname { - t.Errorf("access: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "filename" { - t.Errorf("access: PathnameField = %q, want filename", r.PathnameField) - } -} - -// TestClassifyPathnameFaccessat locks in that faccessat(2) — access(2)'s -// dirfd-relative sibling — also classifies as KindPathname with the path wired -// to the "filename" field. The path is at args[1] (args[0] is the dirfd); the -// argument-index difference from access(2) is verified separately in the -// codegen tests (TestGenerateAccessFaccessatHandlers). -func TestClassifyPathnameFaccessat(t *testing.T) { - r := classifyFromData(t, FormatFaccessat) - if r.Kind != KindPathname { - t.Errorf("faccessat: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "filename" { - t.Errorf("faccessat: PathnameField = %q, want filename", r.PathnameField) - } -} - -func TestClassifyNameRename(t *testing.T) { - r := classifyFromData(t, FormatRename) - if r.Kind != KindName { - t.Errorf("rename: got kind %d, want KindName", r.Kind) - } -} - -func TestClassifyNameLinkat(t *testing.T) { - r := classifyFromData(t, FormatLinkat) - if r.Kind != KindName { - t.Errorf("linkat: got kind %d, want KindName", r.Kind) - } -} - -func TestClassifyNameSymlink(t *testing.T) { - r := classifyFromData(t, FormatSymlink) - if r.Kind != KindName { - t.Errorf("symlink: got kind %d, want KindName", r.Kind) - } -} - -func TestClassifyFcntl(t *testing.T) { - r := classifyFromData(t, FormatFcntl) - if r.Kind != KindFcntl { - t.Errorf("fcntl: got kind %d, want KindFcntl", r.Kind) - } -} - -func TestClassifyDup(t *testing.T) { - r := classifyFromData(t, FormatDup) - if r.Kind != KindFd { - t.Errorf("dup: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyDup2(t *testing.T) { - r := classifyFromData(t, FormatDup2) - if r.Kind != KindFd { - t.Errorf("dup2: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyDup3(t *testing.T) { - r := classifyFromData(t, FormatDup3) - if r.Kind != KindDup3 { - t.Errorf("dup3: got kind %d, want KindDup3", r.Kind) - } -} - -func TestClassifyOpenByHandleAt(t *testing.T) { - r := classifyFromData(t, FormatOpenByHandleAt) - if r.Kind != KindOpenByHandleAt { - t.Errorf("open_by_handle_at: got kind %d, want KindOpenByHandleAt", r.Kind) - } -} - -func TestClassifyNameToHandleAt(t *testing.T) { - r := classifyFromData(t, FormatNameToHandleAt) - if r.Kind != KindPathname { - t.Errorf("name_to_handle_at: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "name" { - t.Errorf("name_to_handle_at: PathnameField = %q, want name", r.PathnameField) - } -} - -func TestClassifyNullSync(t *testing.T) { - r := classifyFromData(t, FormatSync) - if r.Kind != KindNull { - t.Errorf("sync: got kind %d, want KindNull", r.Kind) - } -} - -func TestClassifyNullSyslog(t *testing.T) { - r := classifyFromData(t, FormatSyslog) - if r.Kind != KindNull { - t.Errorf("syslog: got kind %d, want KindNull", r.Kind) - } -} - -// TestClassifyNullGetcwd pins getcwd as KindNull at enter. -// -// getcwd's args[0] is `char *buf`, an OUTPUT buffer: the kernel writes the -// absolute cwd path into it and the contents only become valid AFTER the -// syscall returns (sys_exit). Reading buf at enter would capture an empty or -// garbage string, so getcwd must NOT be classified as a path-input syscall. -// KindNull is the correct enter kind; the cwd is resolved at exit from -// /proc//cwd (see eventLoop.handleNullExit). This test locks that in: -// - the enter kind is KindNull (not KindPathname/KindName), and -// - no pathname field is captured from the buffer at enter. -func TestClassifyNullGetcwd(t *testing.T) { - r := classifyFromData(t, FormatGetcwd) - if r.Kind != KindNull { - t.Errorf("getcwd: got kind %d, want KindNull", r.Kind) - } - if r.Kind == KindPathname || r.Kind == KindName { - t.Errorf("getcwd: enter must not capture output buf as a path, got kind %d", r.Kind) - } - if r.PathnameField != "" { - t.Errorf("getcwd: no enter-time pathname field expected, got %q", r.PathnameField) - } -} - -// TestClassifyByFieldGetcwdBufNotPath is a defense-in-depth lock-in: even if -// the name-only KindNull override for getcwd were removed, the generic -// field-based classifier must not treat `char *buf` as a pathname. Only the -// field names pathname/path/filename/newname are path-like; "buf" is not, so -// classifyByField must report no match for getcwd's output buffer. -func TestClassifyByFieldGetcwdBufNotPath(t *testing.T) { - if r, ok := classifyByField("char *", "buf"); ok { - t.Errorf("getcwd buf: char *buf must not classify as a field kind, got %d", r.Kind) - } -} - -func TestClassifyNullIoUring(t *testing.T) { - r := classifyFromData(t, FormatIoUringEnter) - if r.Kind != KindFd { - t.Errorf("io_uring_enter: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyIoUringRegister(t *testing.T) { - r := classifyFromData(t, FormatIoUringRegister) - if r.Kind != KindFd { - t.Errorf("io_uring_register: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyRetExitRead(t *testing.T) { - r := classifyFromData(t, FormatExitRead) - if r.Kind != KindRet { - t.Errorf("exit_read: got kind %d, want KindRet", r.Kind) - } -} - -func TestClassifyRetExitWrite(t *testing.T) { - r := classifyFromData(t, FormatExitWrite) - if r.Kind != KindRet { - t.Errorf("exit_write: got kind %d, want KindRet", r.Kind) - } -} - -func TestClassifyRetExitOpenat(t *testing.T) { - r := classifyFromData(t, FormatExitOpenat) - if r.Kind != KindRet { - t.Errorf("exit_openat: got kind %d, want KindRet", r.Kind) - } -} - -func TestClassifyRetExitPread64(t *testing.T) { - r := classifyFromData(t, FormatExitPread64) - if r.Kind != KindRet { - t.Errorf("exit_pread64: got kind %d, want KindRet", r.Kind) - } -} - -func TestClassifyRetExitSymlink(t *testing.T) { - r := classifyFromData(t, FormatExitSymlink) - if r.Kind != KindRet { - t.Errorf("exit_symlink: got kind %d, want KindRet", r.Kind) - } -} - -func TestClassifyPathnameMknod(t *testing.T) { - r := classifyFromData(t, FormatMknod) - if r.Kind != KindPathname { - t.Errorf("mknod: got kind %d, want KindPathname", r.Kind) - } -} - -func TestClassifyExecExecve(t *testing.T) { - r := classifyFromData(t, FormatExecve) - if r.Kind != KindExec { - t.Errorf("execve: got kind %d, want KindExec", r.Kind) - } -} - -func TestClassifyExecExecveat(t *testing.T) { - r := classifyFromData(t, FormatExecveat) - if r.Kind != KindExec { - t.Errorf("execveat: got kind %d, want KindExec", r.Kind) - } -} - -func TestClassifyAccept(t *testing.T) { - r := classifyFromData(t, FormatAccept) - if r.Kind != KindAccept { - t.Errorf("accept: got kind %d, want KindAccept", r.Kind) - } -} - -func TestClassifyAccept4(t *testing.T) { - r := classifyFromData(t, FormatAccept4) - if r.Kind != KindAccept { - t.Errorf("accept4: got kind %d, want KindAccept", r.Kind) - } -} - -func TestClassifyExitAccept(t *testing.T) { - r := classifyFromData(t, FormatExitAccept) - if r.Kind != KindAccept { - t.Errorf("exit_accept: got kind %d, want KindAccept", r.Kind) - } -} - -func TestClassifyExitAccept4(t *testing.T) { - r := classifyFromData(t, FormatExitAccept4) - if r.Kind != KindAccept { - t.Errorf("exit_accept4: got kind %d, want KindAccept", r.Kind) - } -} - -func TestClassifySocketFdSyscallsByName(t *testing.T) { - tests := []string{ - "bind", - "connect", - "listen", - "shutdown", - "getsockname", - "getpeername", - "getsockopt", - "setsockopt", - } - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_" + name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "sockfd"}, - }, - }) - if r.Kind != KindFd { - t.Errorf("%s: got kind %d, want KindFd", name, r.Kind) - } - }) - } -} - -// TestClassifySyncFamilyFdSyscallsByName locks in that the filesystem-sync -// family (fsync/fdatasync/syncfs/sync_file_range) is classified as KindFd on -// enter. Each of these takes an open file descriptor as args[0]: -// - int fsync(int fd) -// - int fdatasync(int fd) -// - int syncfs(int fd) -// - int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned flags) -// -// so their enter tracepoint carries a leading fd field and must capture -// fd=args[0] into a fd_event (KindFd), matching the generated -// handle_sys_enter_* handlers. (Plain sync() takes no args and is KindNull; -// it is asserted separately in the classification table test.) -func TestClassifySyncFamilyFdSyscallsByName(t *testing.T) { - tests := []string{ - "fsync", - "fdatasync", - "syncfs", - "sync_file_range", - } - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_" + name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "fd"}, - }, - }) - if r.Kind != KindFd { - t.Errorf("%s: got kind %d, want KindFd", name, r.Kind) - } - }) - } -} - -// TestClassifyExitSyncfs locks in that the syncfs exit tracepoint is classified -// as KindRet. syncfs(2) returns int (0 on success, -1 on error) and transfers -// no bytes, so its exit format carries a single "ret" field and must map to a -// plain ret_event (KindRet, Unclassified) — matching the generated -// sys_exit_syncfs handler and its fsync/fdatasync siblings. -func TestClassifyExitSyncfs(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_syncfs", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Errorf("exit_syncfs: got kind %d, want KindRet", r.Kind) - } -} - -// TestClassifyFallocateEnterFd locks in that the fallocate enter tracepoint is -// classified as KindFd with the fd captured at args[0]. -// -// int fallocate(int fd, int mode, off_t offset, off_t len) -// -// fallocate(2) manipulates the allocated disk space for the file referred to -// by fd (args[0]); the remaining mode/offset/len args are NOT captured, exactly -// like its fd-based siblings fadvise64(2)/ftruncate(2)/sync_file_range(2) which -// also carry trailing offset/len/advice args but only record args[0]. The -// leading "fd" external field must select KindFd so the generated -// handle_sys_enter_fallocate emits ev->fd = ctx->args[0] into a fd_event. -func TestClassifyFallocateEnterFd(t *testing.T) { - f := &Format{ - Name: "sys_enter_fallocate", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "fd"}, - {Type: "int", Name: "mode"}, - {Type: "loff_t", Name: "offset"}, - {Type: "loff_t", Name: "len"}, - }, - } - r := ClassifyFormat(f) - if r.Kind != KindFd { - t.Fatalf("enter_fallocate: got kind %d, want KindFd", r.Kind) - } - // fd is the first real argument (args[0]); FieldNumber skips __syscall_nr. - if got := f.FieldNumber("fd"); got != 0 { - t.Errorf("enter_fallocate: fd field number = %d, want 0 (args[0])", got) - } -} - -// TestClassifyExitFallocateUnclassifiedRet locks in that the fallocate exit -// tracepoint is classified as KindRet and Unclassified. fallocate(2) returns -// int (0 on success, -1 on error) — that return is a status code, NOT a -// transferred byte count, so its exit format carries a single "ret" field and -// must map to a plain ret_event (KindRet) whose ret_type stays UNCLASSIFIED. -// Misclassifying it as a READ/WRITE/TRANSFER byte count would be a real bug, -// since fallocate allocates space but reports no transferred bytes. -func TestClassifyExitFallocateUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_fallocate", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_fallocate: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_fallocate"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_fallocate) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifySetuidNullEnter locks in that the setuid enter tracepoint is -// classified as KindNull. setuid(2) is "int setuid(uid_t uid)" — its single -// argument is a numeric user ID, NOT a file descriptor or a path. It must -// therefore map to a null_event (no argument capture); misclassifying it as an -// fd-bearing kind would be a real bug, since the uid is not an fd and capturing -// it as one would attribute the credential change to a bogus file. The whole -// credential-setting cluster (setuid/seteuid/setresuid/setreuid/setfsuid and -// the gid analogues) shares this KindNull treatment with the getuid readers. -func TestClassifySetuidNullEnter(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_setuid", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "uid"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("enter_setuid: got kind %d, want KindNull", r.Kind) - } - // The uid argument must never be captured as a file descriptor or path. - if r.PathnameField != "" { - t.Errorf("enter_setuid: unexpected PathnameField %q, want empty", r.PathnameField) - } -} - -// TestClassifyExitSetuidUnclassifiedRet locks in that the setuid exit -// tracepoint is classified as KindRet and Unclassified. setuid(2) returns int -// (0 on success, -1 on error) — that return is a status code, NOT a -// transferred byte count, so its exit format carries a single "ret" field and -// must map to a plain ret_event (KindRet) whose ret_type stays UNCLASSIFIED. -// Misclassifying it as a READ/WRITE/TRANSFER byte count would be a real bug. -func TestClassifyExitSetuidUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_setuid", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_setuid: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_setuid"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_setuid) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifySetpgidNullEnter locks in the setpgid(2) enter classification -// using the syscall's REAL tracepoint fields. setpgid(pid_t pid, pid_t pgid) -// sets the process group ID of a process; both arguments are process/process- -// group identifiers (the kernel tracepoint declares them as field type -// "pid_t"), NOT file descriptors and NOT filesystem paths. The audit concern is -// that args[0] ("pid") could be mistaken for an fd: it must not be. setpgid has -// no fd or path argument, so its enter format must classify as KindNull -// (null_event) — matching its session/process-group siblings setsid/getsid/ -// getpgid/getpgrp and the explicit name-only mapping in classify.go. Using the -// real "pid"/"pgid" pid_t fields here (rather than a synthetic arg0) proves the -// generic field heuristics never capture them: isFdType only matches int/ -// unsigned int/unsigned long (not "pid_t"), and the fd heuristic additionally -// requires the field name be "fd", which neither "pid" nor "pgid" is. -func TestClassifySetpgidNullEnter(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_setpgid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - {Type: "pid_t", Name: "pid"}, - {Type: "pid_t", Name: "pgid"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("enter_setpgid: got kind %d, want KindNull", r.Kind) - } - // Neither pid argument must be captured as a file descriptor or path. - if r.PathnameField != "" { - t.Errorf("enter_setpgid: unexpected PathnameField %q, want empty", r.PathnameField) - } -} - -// TestClassifyExitSetpgidUnclassifiedRet locks in that the setpgid exit -// tracepoint is classified as KindRet and Unclassified. setpgid(2) returns int -// (0 on success, -1 on error) — a status code, NOT a transferred byte count — -// so its exit format carries a single "ret" field and must map to a plain -// ret_event (KindRet) whose ret_type stays UNCLASSIFIED. This matches its -// sibling setsid/getsid (asserted in retclassify_test.go); misclassifying it as -// a READ/WRITE/TRANSFER byte count would be a real bug. -func TestClassifyExitSetpgidUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_setpgid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_setpgid: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_setpgid"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_setpgid) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifyGetgidNullEnter locks in the getgid(2) enter classification using -// the syscall's REAL tracepoint fields. getgid(2) is "gid_t getgid(void)" — it -// takes NO arguments at all, so its enter format carries only the synthetic -// __syscall_nr field and must classify as KindNull (null_event capturing -// nothing). This matches the no-arg id-returning reader cluster -// getuid/geteuid/getegid/getpid/getppid/gettid and the explicit name-only -// mapping in classify.go. With no real argument fields there is nothing the fd -// or path heuristics could latch onto, so PathnameField must stay empty. -func TestClassifyGetgidNullEnter(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_getgid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("enter_getgid: got kind %d, want KindNull", r.Kind) - } - // getgid has no arguments, so nothing must be captured as a path/fd. - if r.PathnameField != "" { - t.Errorf("enter_getgid: unexpected PathnameField %q, want empty", r.PathnameField) - } -} - -// TestClassifyExitGetgidUnclassifiedRet locks in that the getgid exit -// tracepoint is classified as KindRet and Unclassified. getgid(2) returns the -// real group ID (gid_t) of the caller and ALWAYS succeeds — its return is a -// numeric credential identifier, NOT a transferred byte count and never an -// error status. Its exit format carries a single "ret" field and must map to a -// plain ret_event (KindRet) whose ret_type stays UNCLASSIFIED. Misclassifying -// the gid as a READ/WRITE/TRANSFER byte count would be a real bug. This matches -// its no-arg reader siblings getuid/getpid (no byte semantics on their return). -func TestClassifyExitGetgidUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_getgid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_getgid: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_getgid"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_getgid) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifyGettidNullEnter locks in the gettid(2) enter classification using -// the syscall's REAL tracepoint fields. gettid(2) is "pid_t gettid(void)" — it -// takes NO arguments at all, so its enter format carries only the synthetic -// __syscall_nr field and must classify as KindNull (null_event capturing -// nothing). This matches the no-arg id-returning reader cluster -// getuid/geteuid/getegid/getpid/getppid/getgid and the explicit name-only -// mapping in classify.go. With no real argument fields there is nothing the fd -// or path heuristics could latch onto, so PathnameField must stay empty. -func TestClassifyGettidNullEnter(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_gettid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("enter_gettid: got kind %d, want KindNull", r.Kind) - } - // gettid has no arguments, so nothing must be captured as a path/fd. - if r.PathnameField != "" { - t.Errorf("enter_gettid: unexpected PathnameField %q, want empty", r.PathnameField) - } -} - -// TestClassifyExitGettidUnclassifiedRet locks in that the gettid exit -// tracepoint is classified as KindRet and Unclassified. gettid(2) returns the -// caller's thread ID (pid_t) and ALWAYS succeeds — its return is a numeric -// thread identifier, NOT a transferred byte count and never an error status. -// Its exit format carries a single "ret" field and must map to a plain -// ret_event (KindRet) whose ret_type stays UNCLASSIFIED. Misclassifying the tid -// as a READ/WRITE/TRANSFER byte count would be a real bug. This matches its -// no-arg reader siblings getuid/getpid/getgid (no byte semantics on their -// return). -func TestClassifyExitGettidUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_gettid", - ExternalFields: []Field{ - {Type: "int", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_gettid: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_gettid"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_gettid) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifyExitGetpeername locks in that the getpeername exit tracepoint is -// classified as KindRet. getpeername(2) returns int (0 on success, -1 on -// error), so its exit format carries a single "ret" field and must map to a -// plain ret_event, matching the generated sys_exit_getpeername handler. -func TestClassifyExitGetpeername(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_getpeername", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Errorf("exit_getpeername: got kind %d, want KindRet", r.Kind) - } -} - -// TestClassifyExitGetsockname locks in that the getsockname exit tracepoint is -// classified as KindRet. getsockname(2) returns int (0 on success, -1 on -// error), so its exit format carries a single "ret" field and must map to a -// plain ret_event, matching the generated sys_exit_getsockname handler — just -// like its sibling getpeername (see TestClassifyExitGetpeername). -func TestClassifyExitGetsockname(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_getsockname", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Errorf("exit_getsockname: got kind %d, want KindRet", r.Kind) - } -} - -// TestClassifySetsockoptEnterFd locks in that the setsockopt enter tracepoint is -// classified as KindFd with the socket fd captured at args[0]. The signature is: -// -// int setsockopt(int sockfd, int level, int optname, -// const void *optval, socklen_t optlen) -// -// setsockopt(2) sets a socket option on the socket referred to by sockfd -// (args[0]); the remaining level/optname/optval/optlen args are NOT captured. -// optval is a userspace pointer (not a transferred byte buffer we account for), -// so only the leading sockfd matters — exactly like its KindFd network siblings -// bind/connect/getsockname/getpeername/getsockopt and the explicit name-only -// mapping in classify.go. The classification is name-only, so this asserts the -// kind holds even when the enter format carries the real "fd" field. Capturing -// any later arg as the fd, or failing to capture args[0], would be a real bug. -func TestClassifySetsockoptEnterFd(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_setsockopt", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "fd"}, - {Type: "int", Name: "level"}, - {Type: "int", Name: "optname"}, - {Type: "char *", Name: "optval"}, - {Type: "int", Name: "optlen"}, - }, - }) - if r.Kind != KindFd { - t.Fatalf("enter_setsockopt: got kind %d, want KindFd", r.Kind) - } - // optval is a userspace pointer, never a pathname we record. - if r.PathnameField != "" { - t.Errorf("enter_setsockopt: unexpected PathnameField %q, want empty", r.PathnameField) - } -} - -// TestClassifyExitSetsockoptUnclassifiedRet locks in that the setsockopt exit -// tracepoint is classified as KindRet and Unclassified. setsockopt(2) returns -// int (0 on success, -1 on error) — a status code, NOT a transferred byte count -// — so its exit format carries a single "ret" field and must map to a plain -// ret_event (KindRet) whose ret_type stays UNCLASSIFIED, matching the generated -// sys_exit_setsockopt handler and its sibling getsockopt. Misclassifying it as a -// READ/WRITE/TRANSFER byte count would be a real bug. -func TestClassifyExitSetsockoptUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_setsockopt", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_setsockopt: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_setsockopt"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_setsockopt) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifyExitGetsockoptUnclassifiedRet mirrors the setsockopt exit lock-in -// for its read-side sibling getsockopt(2), which likewise returns int (0/-1) and -// must map to a plain ret_event (KindRet, UNCLASSIFIED) — never a READ byte -// count, even though it copies option data into a userspace buffer via a -// userspace pointer rather than returning a transferred byte total. -func TestClassifyExitGetsockoptUnclassifiedRet(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_exit_getsockopt", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "ret"}, - }, - }) - if r.Kind != KindRet { - t.Fatalf("exit_getsockopt: got kind %d, want KindRet", r.Kind) - } - if got := ClassifyRet("sys_exit_getsockopt"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_getsockopt) = %q, want UNCLASSIFIED", got) - } -} - -func TestClassifySocket(t *testing.T) { - r := classifyFromData(t, FormatSocket) - if r.Kind != KindSocket { - t.Errorf("socket: got kind %d, want KindSocket", r.Kind) - } -} - -func TestClassifySocketpair(t *testing.T) { - r := classifyFromData(t, FormatSocketpair) - if r.Kind != KindSocketpair { - t.Errorf("socketpair: got kind %d, want KindSocketpair", r.Kind) - } -} - -func TestClassifyExitSocketpair(t *testing.T) { - r := classifyFromData(t, FormatExitSocketpair) - if r.Kind != KindSocketpair { - t.Errorf("exit_socketpair: got kind %d, want KindSocketpair", r.Kind) - } -} - -// TestClassifySocketpairNotFd is a regression lock-in for the socketpair(2) -// audit (task c00). socketpair(int domain, int type, int protocol, int sv[2]) -// takes the address-family/domain constant as args[0] (named "family" in the -// tracepoint format), NOT a file descriptor. The created fds are written into -// the OUTPUT array sv[2] (args[3]) and are only valid after the call returns. -// socketpair must therefore be KindSocketpair (read sv[2] at exit), never -// KindFd, which would record the domain integer as a bogus fd. Pin that the -// name-based override wins so a future field-shape change cannot make it fall -// through to the generic KindFd path. -func TestClassifySocketpairNotFd(t *testing.T) { - r := classifyFromData(t, FormatSocketpair) - if r.Kind == KindFd { - t.Fatal("socketpair classified as KindFd: args[0] is the domain constant, not an fd") - } - if r.Kind != KindSocketpair { - t.Errorf("socketpair: got kind %d, want KindSocketpair", r.Kind) - } -} - -func TestClassifyPipe(t *testing.T) { - r := classifyFromData(t, FormatPipe) - if r.Kind != KindPipe { - t.Errorf("pipe: got kind %d, want KindPipe", r.Kind) - } -} - -func TestClassifyPipe2(t *testing.T) { - r := classifyFromData(t, FormatPipe2) - if r.Kind != KindPipe { - t.Errorf("pipe2: got kind %d, want KindPipe", r.Kind) - } -} - -func TestClassifyExitPipe(t *testing.T) { - r := classifyFromData(t, FormatExitPipe) - if r.Kind != KindPipe { - t.Errorf("exit_pipe: got kind %d, want KindPipe", r.Kind) - } -} - -func TestClassifyExitPipe2(t *testing.T) { - r := classifyFromData(t, FormatExitPipe2) - if r.Kind != KindPipe { - t.Errorf("exit_pipe2: got kind %d, want KindPipe", r.Kind) - } -} - -// TestClassifyPipeNotFd locks in that pipe(2) is NOT classified as KindFd. -// pipe's args[0] is an OUTPUT pointer to int[2] (the two created fds are written -// there by the kernel and are only valid AFTER the syscall returns), NOT an fd -// argument. Capturing args[0] as an fd would attribute the pipe to a bogus -// descriptor; pipe must use the pipe-specific KindPipe path that reads the fd -// pair from the userspace buffer at exit. Same pitfall as socketpair (task c00). -func TestClassifyPipeNotFd(t *testing.T) { - for _, name := range []string{"pipe", "pipe2"} { - r := classifyFromData(t, map[string]string{ - "pipe": FormatPipe, - "pipe2": FormatPipe2, - }[name]) - if r.Kind == KindFd { - t.Fatalf("%s classified as KindFd: args[0] is an output ptr, not an fd", name) - } - if r.Kind != KindPipe { - t.Errorf("%s: got kind %d, want KindPipe", name, r.Kind) - } - } -} - -// TestClassifyPipeUnclassifiedRet locks in that the pipe and pipe2 exit -// tracepoints stay UNCLASSIFIED. pipe(2)/pipe2(2) return int (0 on success, -// -1 on error) — a status code, NOT a transferred byte count. They must not be -// in retClassifications and must never map to READ/WRITE/TRANSFER, which would -// misreport phantom bytes. The created fds are surfaced via fd0/fd1 in the -// pipe_event, not via the return value. -func TestClassifyPipeUnclassifiedRet(t *testing.T) { - for _, name := range []string{"sys_exit_pipe", "sys_exit_pipe2"} { - if got := ClassifyRet(name); got != Unclassified { - t.Errorf("ClassifyRet(%s) = %q, want UNCLASSIFIED", name, got) - } - } -} - -func TestClassifyEventfd(t *testing.T) { - r := classifyFromData(t, FormatEventfd) - if r.Kind != KindEventfd { - t.Errorf("eventfd: got kind %d, want KindEventfd", r.Kind) - } -} - -func TestClassifyEventfd2(t *testing.T) { - r := classifyFromData(t, FormatEventfd2) - if r.Kind != KindEventfd { - t.Errorf("eventfd2: got kind %d, want KindEventfd", r.Kind) - } -} - -func TestClassifyExitEventfd(t *testing.T) { - r := classifyFromData(t, FormatExitEventfd) - if r.Kind != KindEventfd { - t.Errorf("exit_eventfd: got kind %d, want KindEventfd", r.Kind) - } -} - -func TestClassifyExitEventfd2(t *testing.T) { - r := classifyFromData(t, FormatExitEventfd2) - if r.Kind != KindEventfd { - t.Errorf("exit_eventfd2: got kind %d, want KindEventfd", r.Kind) - } -} - -func TestClassifyEventfdSpecializedFdFromAirSyscalls(t *testing.T) { - tests := []string{ - "sys_enter_memfd_create", - "sys_exit_memfd_create", - "sys_enter_memfd_secret", - "sys_exit_memfd_secret", - "sys_enter_userfaultfd", - "sys_exit_userfaultfd", - "sys_enter_signalfd", - "sys_exit_signalfd", - "sys_enter_signalfd4", - "sys_exit_signalfd4", - "sys_enter_timerfd_create", - "sys_exit_timerfd_create", - } - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r, ok := classifyNameOnly(name) - if !ok { - t.Fatalf("classifyNameOnly(%q) did not match", name) - } - if r.Kind != KindEventfd { - t.Fatalf("classifyNameOnly(%q) kind = %v, want KindEventfd", name, r.Kind) - } - }) - } -} - -func TestClassifyEpollCtl(t *testing.T) { - r := classifyFromData(t, FormatEpollCtl) - if r.Kind != KindEpollCtl { - t.Errorf("epoll_ctl: got kind %d, want KindEpollCtl", r.Kind) - } -} - -func TestClassifyEpollWait(t *testing.T) { - r := classifyFromData(t, FormatEpollWait) - if r.Kind != KindFd { - t.Errorf("epoll_wait: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyEpollPwait(t *testing.T) { - r := classifyFromData(t, FormatEpollPwait) - if r.Kind != KindFd { - t.Errorf("epoll_pwait: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyEpollPwait2(t *testing.T) { - r := classifyFromData(t, FormatEpollPwait2) - if r.Kind != KindFd { - t.Errorf("epoll_pwait2: got kind %d, want KindFd", r.Kind) - } -} - -func TestClassifyPoll(t *testing.T) { - r := classifyFromData(t, FormatPoll) - if r.Kind != KindPoll { - t.Errorf("poll: got kind %d, want KindPoll", r.Kind) - } -} - -func TestClassifyPpoll(t *testing.T) { - r := classifyFromData(t, FormatPpoll) - if r.Kind != KindPoll { - t.Errorf("ppoll: got kind %d, want KindPoll", r.Kind) - } -} - -func TestClassifySelect(t *testing.T) { - r := classifyFromData(t, FormatSelect) - if r.Kind != KindPoll { - t.Errorf("select: got kind %d, want KindPoll", r.Kind) - } -} - -func TestClassifyPselect6(t *testing.T) { - r := classifyFromData(t, FormatPselect6) - if r.Kind != KindPoll { - t.Errorf("pselect6: got kind %d, want KindPoll", r.Kind) - } -} - -func TestClassifyMunmap(t *testing.T) { - r := classifyFromData(t, FormatMunmap) - if r.Kind != KindMem { - t.Errorf("munmap: got kind %d, want KindMem", r.Kind) - } -} - -func TestClassifyMremap(t *testing.T) { - r := classifyFromData(t, FormatMremap) - if r.Kind != KindMem { - t.Errorf("mremap: got kind %d, want KindMem", r.Kind) - } -} - -func TestClassifyNanosleep(t *testing.T) { - r := classifyFromData(t, FormatNanosleep) - if r.Kind != KindSleep { - t.Errorf("nanosleep: got kind %d, want KindSleep", r.Kind) - } -} - -func TestClassifyClockNanosleep(t *testing.T) { - r := classifyFromData(t, FormatClockNanosleep) - if r.Kind != KindSleep { - t.Errorf("clock_nanosleep: got kind %d, want KindSleep", r.Kind) - } -} - -func TestClassifyKeyctl(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_keyctl", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "option"}, - {Type: "key_serial_t", Name: "arg2"}, - }, - }) - if r.Kind != KindKeyctl { - t.Errorf("keyctl: got kind %d, want KindKeyctl", r.Kind) - } -} - -func TestClassifyAddKey(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_add_key", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "const char *", Name: "_type"}, - {Type: "const char *", Name: "_description"}, - {Type: "const void *", Name: "_payload"}, - {Type: "size_t", Name: "plen"}, - {Type: "key_serial_t", Name: "ringid"}, - }, - }) - if r.Kind != KindKeyctl { - t.Errorf("add_key: got kind %d, want KindKeyctl", r.Kind) - } -} - -// TestClassifyRequestKey locks in the request_key(2) classification: -// -// key_serial_t request_key(const char *type, const char *description, -// const char *callout_info, key_serial_t dest_keyring) -// -// type/description/callout_info are key metadata STRINGS (a key type name, a -// free-form description and optional callout payload), NOT filesystem paths, -// so the const char * args must not trip the pathname/open heuristics. The -// name-only table maps request_key to KindKeyctl before any field is -// inspected; the generated handler captures only the numeric dest_keyring -// (args[3]) plus the option=-2 sentinel, and the exit returns a key serial / -// -1 that is not a byte count (UNCLASSIFIED). -func TestClassifyRequestKey(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_request_key", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "const char *", Name: "_type"}, - {Type: "const char *", Name: "_description"}, - {Type: "const char *", Name: "_callout_info"}, - {Type: "key_serial_t", Name: "destringid"}, - }, - }) - if r.Kind != KindKeyctl { - t.Errorf("request_key: got kind %d, want KindKeyctl", r.Kind) - } - // The const char * type/description/callout_info args are key metadata, - // not paths — no path capture must be emitted for them. - if r.PathnameField != "" { - t.Errorf("request_key: got PathnameField %q, want empty (string args are key metadata, not paths)", r.PathnameField) - } - // Family: Security, alongside add_key/keyctl/lsm_*/seccomp siblings. - for _, prefix := range []string{"sys_enter_", "sys_exit_"} { - if fam := ClassifySyscallFamily(prefix + "request_key"); fam != FamilySecurity { - t.Errorf("%srequest_key: got family %s, want FamilySecurity", prefix, fam) - } - } - // Return value is a key serial / -1, never a byte transfer. - if got := ClassifyRet("sys_exit_request_key"); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_request_key) = %q, want UNCLASSIFIED", got) - } -} - -// TestClassifyKeyctlAudit is a lock-in regression test for the keyctl(2) -// audit. The key-management syscalls have these signatures: -// -// long keyctl(int op, unsigned long arg2, arg3, arg4, arg5) -// key_serial_t add_key(const char *type, const char *desc, -// const void *payload, size_t plen, key_serial_t keyring) -// key_serial_t request_key(const char *type, const char *desc, -// const char *callout_info, key_serial_t dest_keyring) -// -// keyctl's op selects a command and the remaining arguments are -// operation-dependent unsigned longs — never an fd or a path. add_key and -// request_key take string TYPE/DESCRIPTION arguments that are key metadata -// (a key type name and a free-form description), NOT filesystem paths, so -// they must not be classified as KindPathname/KindOpen. All three therefore -// classify as KindKeyctl (operation + generic numeric args, captured via the -// keyctl_event without any bpf_probe_read_user path/fd capture), live in the -// FamilySecurity family alongside their *_key/landlock_*/lsm_*/seccomp -// siblings, and return an operation-dependent value or -1 that is NOT a byte -// transfer, so their exits stay UNCLASSIFIED. -func TestClassifyKeyctlAudit(t *testing.T) { - for _, name := range []string{"keyctl", "add_key", "request_key"} { - // Family: Security on both enter and exit tracepoint names. - for _, prefix := range []string{"sys_enter_", "sys_exit_"} { - if fam := ClassifySyscallFamily(prefix + name); fam != FamilySecurity { - t.Errorf("%s%s: got family %s, want FamilySecurity", prefix, name, fam) - } - } - - // Returns: UNCLASSIFIED (key serial / op-dependent value / -1, not - // a byte count), so the exit must NOT be tagged as a read/write/ - // transfer byte transfer. - if got := ClassifyRet("sys_exit_" + name); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_%s) = %q, want UNCLASSIFIED", name, got) - } - } - - // Contrast: add_key/request_key take a const char * "type"/"description" - // first argument, but it is key metadata, not a path. Such a field name - // must NOT trip the generic pathname/open heuristics — the name-only table - // maps these syscalls to KindKeyctl before any field is inspected. - addKey := ClassifyFormat(&Format{ - Name: "sys_enter_add_key", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "const char *", Name: "_type"}, - {Type: "const char *", Name: "_description"}, - {Type: "const void *", Name: "_payload"}, - {Type: "size_t", Name: "plen"}, - {Type: "key_serial_t", Name: "ringid"}, - }, - }) - if addKey.Kind != KindKeyctl { - t.Errorf("add_key: got kind %d, want KindKeyctl (string args are key metadata, not paths)", addKey.Kind) - } - if addKey.PathnameField != "" { - t.Errorf("add_key: got PathnameField %q, want empty (no path capture)", addKey.PathnameField) - } -} - -func TestClassifyPtrace(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_ptrace", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "request"}, - {Type: "long", Name: "pid"}, - }, - }) - if r.Kind != KindPtrace { - t.Errorf("ptrace: got kind %d, want KindPtrace", r.Kind) - } -} - -func TestClassifyPerfEventOpen(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_perf_event_open", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "struct perf_event_attr *", Name: "attr_uptr"}, - {Type: "pid_t", Name: "pid"}, - {Type: "int", Name: "cpu"}, - {Type: "int", Name: "group_fd"}, - {Type: "unsigned long", Name: "flags"}, - }, - }) - if r.Kind != KindPerfOpen { - t.Errorf("perf_event_open: got kind %d, want KindPerfOpen", r.Kind) - } -} - -func TestClassifyMqOpen(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_mq_open", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "const char *", Name: "u_name"}, - {Type: "int", Name: "oflag"}, - }, - }) - if r.Kind != KindMqOpen { - t.Errorf("mq_open: got kind %d, want KindMqOpen", r.Kind) - } -} - -func TestClassifyMqUnlink(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_mq_unlink", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "const char *", Name: "u_name"}, - }, - }) - if r.Kind != KindPathname { - t.Errorf("mq_unlink: got kind %d, want KindPathname", r.Kind) - } - if r.PathnameField != "u_name" { - t.Errorf("mq_unlink: PathnameField = %q, want u_name", r.PathnameField) - } -} - -func TestClassifyMqFdSyscallsByName(t *testing.T) { - tests := []string{ - "mq_timedsend", - "mq_timedreceive", - "mq_notify", - "mq_getsetattr", - } - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: "sys_enter_" + name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "mqd_t", Name: "mqdes"}, - }, - }) - if r.Kind != KindFd { - t.Errorf("%s: got kind %d, want KindFd", name, r.Kind) - } - }) - } -} - -func TestClassifyN7NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_pidfd_open", KindPidfd}, - {"sys_exit_pidfd_open", KindPidfd}, - {"sys_enter_pidfd_send_signal", KindFd}, - {"sys_enter_kexec_file_load", KindFd}, - {"sys_enter_kcmp", KindTwoFd}, - {"sys_enter_membarrier", KindNull}, - {"sys_enter_rseq", KindNull}, - {"sys_enter_set_robust_list", KindNull}, - {"sys_enter_get_robust_list", KindNull}, - {"sys_enter_mmap2", KindNull}, - {"sys_enter_kexec_load", KindNull}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -// TestClassifySendfile64CapturesOutFd locks in the sendfile64 audit (task az): -// sendfile64(out_fd, in_fd, offset, count) transfers bytes between two file -// descriptors inside the kernel and returns the count written to out_fd. Its -// real tracepoint fields carry no field literally named "fd", so without the -// explicit nameOnlyKindsTable override it would fall through to KindNull and -// capture no descriptor — inconsistent with its sibling copy_file_range (KindFd) -// and the read/write/sendto/recvfrom families. This test pins that sendfile64 is -// a KindFd event capturing out_fd (args[0], the write destination) and that the -// generated C emits exactly that capture, never a null_event. -func TestClassifySendfile64CapturesOutFd(t *testing.T) { - // Realistic enter layout from /sys/kernel/tracing for sys_enter_sendfile64. - enter := &Format{ - Name: "sys_enter_sendfile64", - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "int", Name: "out_fd"}, - {Type: "int", Name: "in_fd"}, - {Type: "off_t *", Name: "offset"}, - {Type: "size_t", Name: "count"}, - }, - } - r := ClassifyFormat(enter) - if r.Kind != KindFd { - t.Fatalf("sendfile64: got kind %d, want KindFd (must not fall back to KindNull)", r.Kind) - } - // Negative guard: out_fd/in_fd must not be mistaken for a two-fd event; the - // audit deliberately keeps sendfile64 single-fd like copy_file_range. - if r.Kind == KindTwoFd || r.Kind == KindNull { - t.Fatalf("sendfile64: kind %d, want single-fd KindFd, not two-fd/null", r.Kind) - } - - // Generated C must capture out_fd at args[0] (the byte-write destination) via - // a struct fd_event, never a struct null_event. - output := GenerateTracepointsC(phaseAFormats("sendfile64", 9500)) - if !strings.Contains(output, "/// sys_enter_sendfile64 is a struct fd_event") { - t.Fatalf("sys_enter_sendfile64 should be a struct fd_event:\n%s", output) - } - if strings.Contains(output, "/// sys_enter_sendfile64 is a struct null_event") { - t.Fatalf("sys_enter_sendfile64 must not be a struct null_event:\n%s", output) - } - if !strings.Contains(output, "ev->fd = (__s32)ctx->args[0];") { - t.Fatalf("sys_enter_sendfile64 should capture out_fd from args[0]:\n%s", output) - } - // Return value stays TransferClassified: sendfile64 moves bytes between two - // fds, consistent with copy_file_range/splice/tee/vmsplice. - if c := ClassifyRet("sys_exit_sendfile64"); c != TransferClassified { - t.Fatalf("sendfile64 ret: got %v, want TransferClassified", c) - } -} - -func TestClassifyG7NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_epoll_create", KindEventfd}, - {"sys_exit_epoll_create", KindEventfd}, - {"sys_enter_epoll_create1", KindEventfd}, - {"sys_exit_epoll_create1", KindEventfd}, - {"sys_enter_inotify_init", KindEventfd}, - {"sys_exit_inotify_init", KindEventfd}, - {"sys_enter_inotify_init1", KindEventfd}, - {"sys_exit_inotify_init1", KindEventfd}, - {"sys_enter_fanotify_init", KindEventfd}, - {"sys_exit_fanotify_init", KindEventfd}, - {"sys_enter_landlock_create_ruleset", KindEventfd}, - {"sys_exit_landlock_create_ruleset", KindEventfd}, - {"sys_enter_fsopen", KindEventfd}, - {"sys_exit_fsopen", KindEventfd}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -func TestClassifyI7NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_mincore", KindMem}, - {"sys_enter_remap_file_pages", KindMem}, - {"sys_enter_mlock", KindMem}, - {"sys_enter_mlock2", KindMem}, - {"sys_enter_munlock", KindMem}, - {"sys_enter_mseal", KindMem}, - {"sys_enter_map_shadow_stack", KindMem}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -func TestClassifyH7NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_mprotect", - "sys_enter_madvise", - "sys_enter_pkey_mprotect", - "sys_enter_brk", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindMem { - t.Fatalf("%s: got kind %d, want KindMem", name, r.Kind) - } - }) - } -} - -func TestClassifyL7NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_pkey_alloc", KindNull}, - {"sys_enter_pkey_free", KindNull}, - {"sys_enter_mbind", KindNull}, - {"sys_enter_set_mempolicy", KindNull}, - {"sys_enter_get_mempolicy", KindNull}, - {"sys_enter_set_mempolicy_home_node", KindNull}, - {"sys_enter_migrate_pages", KindNull}, - {"sys_enter_move_pages", KindNull}, - {"sys_enter_mlockall", KindNull}, - {"sys_enter_munlockall", KindNull}, - {"sys_enter_process_madvise", KindFd}, - {"sys_enter_process_mrelease", KindFd}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -func TestClassifyJ7NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_futex", - "sys_enter_futex_wait", - "sys_enter_futex_wake", - "sys_enter_futex_requeue", - "sys_enter_futex_waitv", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindFutex { - t.Fatalf("%s: got kind %d, want KindFutex", name, r.Kind) - } - }) - } -} - -func TestClassifyK7NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_wait4", KindProc}, - {"sys_enter_waitid", KindProc}, - {"sys_enter_kill", KindNull}, - {"sys_enter_prctl", KindPrctl}, - {"sys_enter_setns", KindFd}, - {"sys_enter_unshare", KindNull}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -func TestClassifyM7NameOnlyKinds(t *testing.T) { - nullKinds := []string{ - "sys_enter_clock_gettime", - "sys_enter_clock_settime", - "sys_enter_clock_getres", - "sys_enter_clock_adjtime", - "sys_enter_gettimeofday", - "sys_enter_settimeofday", - "sys_enter_time", - "sys_enter_times", - "sys_enter_adjtimex", - "sys_enter_alarm", - "sys_enter_getitimer", - "sys_enter_setitimer", - } - for _, name := range nullKinds { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("%s: got kind %d, want KindNull", name, r.Kind) - } - }) - } - - timerObjKinds := []string{ - "sys_enter_timer_create", - "sys_enter_timer_settime", - "sys_enter_timer_gettime", - "sys_enter_timer_getoverrun", - "sys_enter_timer_delete", - } - for _, name := range timerObjKinds { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindTimerObj { - t.Fatalf("%s: got kind %d, want KindTimerObj", name, r.Kind) - } - }) - } -} - -func TestClassifyO7NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_landlock_add_rule", - "sys_enter_landlock_restrict_self", - } - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindFd { - t.Fatalf("%s: got kind %d, want KindFd", name, r.Kind) - } - }) - } -} - -func TestClassify67NameOnlyKinds(t *testing.T) { - tests := []struct { - name string - want TracepointKind - }{ - {"sys_enter_seccomp", KindSeccomp}, - {"sys_exit_seccomp", KindSeccomp}, - {"sys_enter_init_module", KindModule}, - {"sys_exit_init_module", KindModule}, - {"sys_enter_delete_module", KindModule}, - {"sys_exit_delete_module", KindModule}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: tt.name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != tt.want { - t.Fatalf("%s: got kind %d, want %d", tt.name, r.Kind, tt.want) - } - }) - } -} - -// TestClassifyInitModuleVsFinitModule locks in the load-bearing distinction -// between the two module-loading syscalls (man 2 init_module). -// -// init_module(void *module_image, unsigned long len, const char *param_values) -// takes a userspace ELF image pointer and a module-PARAMETER string (not a -// filesystem path), so it must classify as KindModule (null_event) and capture -// neither an fd nor a path — param_values must NOT be mistaken for a path. -// -// finit_module(int fd, const char *param_values, int flags) reads the module -// from a file descriptor, so it must classify as KindFd via field-based -// matching on the leading "fd" field. -func TestClassifyInitModuleVsFinitModule(t *testing.T) { - if r := classifyFromData(t, FormatInitModule); r.Kind != KindModule { - t.Errorf("init_module: got kind %d, want KindModule", r.Kind) - } - if r := classifyFromData(t, FormatFinitModule); r.Kind != KindFd { - t.Errorf("finit_module: got kind %d, want KindFd", r.Kind) - } - - // param_values (uargs) is a parameter string, never a captured path: the - // init_module classification must not select KindPathname/KindName/KindOpen. - if r := classifyFromData(t, FormatInitModule); r.PathnameField != "" { - t.Errorf("init_module: unexpected PathnameField %q, want empty", r.PathnameField) - } - if r := classifyFromData(t, FormatFinitModule); r.PathnameField != "" { - t.Errorf("finit_module: unexpected PathnameField %q, want empty", r.PathnameField) - } - - // Both module-loading syscalls live in FamilySecurity (man 2 init_module: - // loading kernel code is a privileged, security-sensitive operation), and - // both return 0/-1 with no byte count, so their exits are UNCLASSIFIED. - for _, name := range []string{"init_module", "finit_module"} { - if fam := ClassifySyscallFamily("sys_enter_" + name); fam != FamilySecurity { - t.Errorf("%s: got family %s, want FamilySecurity", name, fam) - } - if got := ClassifyRet("sys_exit_" + name); got != Unclassified { - t.Errorf("ClassifyRet(sys_exit_%s) = %q, want UNCLASSIFIED", name, got) - } - } -} - -func TestClassify87NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_rt_sigaction", - "sys_enter_rt_sigprocmask", - "sys_enter_rt_sigpending", - "sys_enter_rt_sigsuspend", - "sys_enter_rt_sigtimedwait", - "sys_enter_rt_sigreturn", - "sys_enter_sigaltstack", - "sys_enter_pause", - "sys_enter_rt_sigqueueinfo", - "sys_enter_rt_tgsigqueueinfo", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("%s: got kind %d, want KindNull", name, r.Kind) - } - }) - } -} - -func TestClassify97NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_getpid", - "sys_enter_gettid", - "sys_enter_getppid", - "sys_enter_getuid", - "sys_enter_geteuid", - "sys_enter_getgid", - "sys_enter_getegid", - "sys_enter_getresuid", - "sys_enter_getresgid", - "sys_enter_getgroups", - "sys_enter_setuid", - "sys_enter_seteuid", - "sys_enter_setgid", - "sys_enter_setegid", - "sys_enter_setresuid", - "sys_enter_setresgid", - "sys_enter_setreuid", - "sys_enter_setregid", - "sys_enter_setfsuid", - "sys_enter_setfsgid", - "sys_enter_setgroups", - "sys_enter_umask", - "sys_enter_setsid", - "sys_enter_getsid", - "sys_enter_setpgid", - "sys_enter_getpgid", - "sys_enter_getpgrp", - "sys_enter_set_tid_address", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("%s: got kind %d, want KindNull", name, r.Kind) - } - }) - } -} - -func TestClassifyA7NameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_sched_yield", - "sys_enter_sched_setaffinity", - "sys_enter_sched_getaffinity", - "sys_enter_sched_setparam", - "sys_enter_sched_getparam", - "sys_enter_sched_setscheduler", - "sys_enter_sched_getscheduler", - "sys_enter_sched_setattr", - "sys_enter_sched_getattr", - "sys_enter_sched_get_priority_max", - "sys_enter_sched_get_priority_min", - "sys_enter_sched_rr_get_interval", - "sys_enter_getcpu", - "sys_enter_getrusage", - "sys_enter_getrlimit", - "sys_enter_setrlimit", - "sys_enter_prlimit64", - "sys_enter_getpriority", - "sys_enter_setpriority", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("%s: got kind %d, want KindNull", name, r.Kind) - } - }) - } -} - -func TestClassifyE7NullNameOnlyKinds(t *testing.T) { - tests := []string{ - "sys_enter_sysinfo", - "sys_enter_sysfs", - "sys_enter_ustat", - "sys_enter_newuname", - "sys_enter_sethostname", - "sys_enter_setdomainname", - "sys_enter_capget", - "sys_enter_capset", - "sys_enter_personality", - "sys_enter_reboot", - "sys_enter_restart_syscall", - "sys_enter_vhangup", - "sys_enter_arch_prctl", - "sys_enter_ioperm", - "sys_enter_iopl", - "sys_enter_modify_ldt", - "sys_enter_lsm_get_self_attr", - "sys_enter_lsm_set_self_attr", - "sys_enter_lsm_list_modules", - } - - for _, name := range tests { - t.Run(name, func(t *testing.T) { - r := ClassifyFormat(&Format{ - Name: name, - ExternalFields: []Field{ - {Type: "long", Name: "__syscall_nr"}, - {Type: "long", Name: "arg0"}, - }, - }) - if r.Kind != KindNull { - t.Fatalf("%s: got kind %d, want KindNull", name, r.Kind) - } - }) - } -} - -// TestClassifyIo