summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/generate/classify_test.go3149
-rw-r--r--internal/generate/codegen_test.go166
-rw-r--r--internal/generate/family_test.go365
-rw-r--r--internal/generate/retclassify_test.go185
4 files changed, 13 insertions, 3852 deletions
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,1315 +6,16 @@ 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/<tid>/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)
- }
- })
- }
-}
+// This file retains only the classifier tests that exercise actual code
+// generation (tracing behavior / argument extraction / event-kind selection),
+// plus the shared test helpers used by codegen_test.go and family-codegen
+// tests. The former pure-classification unit tests — those whose sole assertion
+// was "ClassifyFormat(X).Kind == Y", "ClassifySyscallFamily(X) == Z", or
+// "ClassifyRet(X) == ..." with no tracing component — were removed (task j20)
+// as redundant: classification correctness is verified by inspection against
+// the man pages and the classifier rules, and the tracing-relevant outcome
+// (which fd/path/byte-count the generated BPF C actually captures) is covered by
+// the GenerateTracepointsC tests kept below and in codegen_test.go.
// TestClassifySendfile64CapturesOutFd locks in the sendfile64 audit (task az):
// sendfile64(out_fd, in_fd, offset, count) transfers bytes between two file
@@ -1366,952 +67,7 @@ func TestClassifySendfile64CapturesOutFd(t *testing.T) {
}
}
-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)
- }
- })
- }
-}
-
-// TestClassifyIoplNullEnter locks in the iopl(2) enter classification using the
-// syscall's REAL tracepoint field. iopl(int level) changes the x86 I/O privilege
-// level of the calling thread (the two least significant bits of level select
-// the IOPL, 0-3); level is a plain int status/selector, NOT a file descriptor and
-// NOT a filesystem path. iopl is in nameOnlyKindsTable, so its enter classifies
-// as KindNull by name before any field heuristic runs — but the audit concern is
-// that the single "level" int must never be captured as an fd or a path. Using
-// the real "int level" field here (rather than the synthetic arg0 used by
-// TestClassifyE7NullNameOnlyKinds) proves the heuristics would not capture it
-// even if the name-only mapping were removed: the fd heuristic requires the field
-// be named "fd" (which "level" is not), and no string-pointer path field exists.
-// Siblings ioperm/modify_ldt share this null_event shape (FamilyMisc, asserted in
-// family_test.go).
-func TestClassifyIoplNullEnter(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_enter_iopl",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "level"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("enter_iopl: got kind %d, want KindNull", r.Kind)
- }
- // The "level" argument must not be captured as a file descriptor or path.
- if r.PathnameField != "" {
- t.Errorf("enter_iopl: unexpected PathnameField %q, want empty", r.PathnameField)
- }
-}
-
-// TestClassifyExitIoplUnclassifiedRet locks in that the iopl exit tracepoint is
-// classified as KindRet and Unclassified. iopl(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 handle_sys_exit_iopl).
-// Misclassifying that status as a READ/WRITE/TRANSFER byte count would be a real
-// bug; it shares this shape with its siblings ioperm/modify_ldt.
-func TestClassifyExitIoplUnclassifiedRet(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_exit_iopl",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "ret"},
- },
- })
- if r.Kind != KindRet {
- t.Fatalf("exit_iopl: got kind %d, want KindRet", r.Kind)
- }
- if got := ClassifyRet("sys_exit_iopl"); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_iopl) = %q, want UNCLASSIFIED", got)
- }
-}
-
-// TestClassifyArchPrctlNullEnter locks in the arch_prctl(2) enter classification
-// using the syscall's REAL kernel tracepoint fields. arch_prctl(int op, unsigned
-// long addr) sets/gets x86-64-specific thread state (ARCH_SET_FS, ARCH_GET_FS,
-// ARCH_SET_GS, ARCH_GET_GS, ARCH_SET_CPUID, ARCH_GET_CPUID). The kernel exposes
-// these args as "option" (an int operation code) and "arg2" (an unsigned long
-// that is either a value for the SET ops or a userspace pointer for the GET ops).
-// Neither is a file descriptor and neither is a filesystem path. arch_prctl is in
-// nameOnlyKindsTable, so its enter classifies as KindNull by name before any field
-// heuristic runs — but the audit concern is that "option"/"arg2" must never be
-// captured as an fd or a path. Using the real fields here (rather than the
-// synthetic arg0 used by TestClassifyE7NullNameOnlyKinds) proves the heuristics
-// would not capture them even if the name-only mapping were removed: the fd
-// heuristic requires a field named "fd" (neither "option" nor "arg2" qualifies),
-// and no C-string-pointer path field exists. arch_prctl is deliberately
-// FamilyProcess (asserted in family_test.go), not Misc, unlike its x86 siblings
-// ioperm/iopl/modify_ldt.
-func TestClassifyArchPrctlNullEnter(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_enter_arch_prctl",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "option"},
- {Type: "unsigned long", Name: "arg2"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("enter_arch_prctl: got kind %d, want KindNull", r.Kind)
- }
- // Neither the "option" code nor the "arg2" value/pointer must be captured as a
- // file descriptor or a path.
- if r.PathnameField != "" {
- t.Errorf("enter_arch_prctl: unexpected PathnameField %q, want empty", r.PathnameField)
- }
-}
-
-// TestClassifyExitArchPrctlUnclassifiedRet locks in that the arch_prctl exit
-// tracepoint is classified as KindRet and Unclassified. arch_prctl(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
-// handle_sys_exit_arch_prctl). Misclassifying that status as a READ/WRITE/TRANSFER
-// byte count would be a real bug. (The ARCH_GET_CPUID op returns the flag setting
-// in the return value, but it is still a small status code, not an I/O byte
-// count.)
-func TestClassifyExitArchPrctlUnclassifiedRet(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_exit_arch_prctl",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "ret"},
- },
- })
- if r.Kind != KindRet {
- t.Fatalf("exit_arch_prctl: got kind %d, want KindRet", r.Kind)
- }
- if got := ClassifyRet("sys_exit_arch_prctl"); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_arch_prctl) = %q, want UNCLASSIFIED", got)
- }
-}
-
-// TestClassifyIoprioNullKind locks in the argument-capture classification for
-// ioprio_set/ioprio_get using their real kernel tracepoint fields. Unlike the
-// name-only Misc/null syscalls above, ioprio_* are NOT in nameOnlyKindsTable:
-// they classify by field fallthrough. ioprio_set(which, who, ioprio) and
-// ioprio_get(which, who) carry only int-typed which/who/ioprio fields. None is
-// named "fd"/"pathname"/"path"/"filename", so ClassifyFormat must return
-// KindNone — in particular the "who" argument (a pid/pgid/uid selected by
-// "which", never an fd) must NOT be misclassified as KindFd, and nothing must be
-// captured as a path. classifyEnterForGeneration then promotes the field-bearing
-// enter format to KindNull (the null_event seen in generated_tracepoints.c).
-func TestClassifyIoprioNullKind(t *testing.T) {
- cases := []struct {
- name string
- fields []Field
- }{
- {
- name: "sys_enter_ioprio_set",
- fields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "which"},
- {Type: "int", Name: "who"},
- {Type: "int", Name: "ioprio"},
- },
- },
- {
- name: "sys_enter_ioprio_get",
- fields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "which"},
- {Type: "int", Name: "who"},
- },
- },
- }
-
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- f := &Format{Name: tc.name, ExternalFields: tc.fields}
-
- // No field should match an fd/path/name pattern: raw classification
- // is KindNone, proving "who" is not captured as an fd.
- if r := ClassifyFormat(f); r.Kind != KindNone {
- t.Fatalf("%s: ClassifyFormat kind = %d, want KindNone", tc.name, r.Kind)
- }
-
- // The generator promotes the field-bearing enter format to KindNull.
- if r := classifyEnterForGeneration(f); r.Kind != KindNull {
- t.Fatalf("%s: classifyEnterForGeneration kind = %d, want KindNull", tc.name, r.Kind)
- }
- })
- }
-}
-
-func TestClassifyB7NameOnlyKinds(t *testing.T) {
- tests := []struct {
- name string
- want TracepointKind
- }{
- {"sys_enter_msgget", KindSysVId},
- {"sys_enter_semget", KindSysVId},
- {"sys_enter_shmget", KindSysVId},
- {"sys_enter_msgsnd", KindSysVOp},
- {"sys_enter_msgrcv", KindSysVOp},
- {"sys_enter_msgctl", KindSysVOp},
- {"sys_enter_semop", KindSysVOp},
- {"sys_enter_semtimedop", KindSysVOp},
- {"sys_enter_semctl", KindSysVOp},
- {"sys_enter_shmat", KindSysVOp},
- {"sys_enter_shmdt", KindSysVOp},
- {"sys_enter_shmctl", KindSysVOp},
- }
-
- 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 TestClassify37NameOnlyKinds(t *testing.T) {
- tests := []string{
- "sys_enter_clone",
- "sys_enter_clone3",
- "sys_enter_fork",
- "sys_enter_vfork",
- }
-
- 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 != KindProc {
- t.Fatalf("%s: got kind %d, want KindProc", name, r.Kind)
- }
- })
- }
-}
-
-func TestClassify57NameOnlyKinds(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_enter_bpf",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "int", Name: "cmd"},
- {Type: "union bpf_attr *", Name: "attr"},
- {Type: "unsigned int", Name: "size"},
- },
- })
- if r.Kind != KindBpf {
- t.Fatalf("sys_enter_bpf: got kind %d, want KindBpf", r.Kind)
- }
-}
-
-func TestClassifyAcctPathname(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_enter_acct",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "const char *", Name: "name"},
- },
- })
- if r.Kind != KindPathname {
- t.Fatalf("acct: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "name" {
- t.Fatalf("acct: PathnameField=%q, want name", r.PathnameField)
- }
-}
-
-// TestClassifySetdomainnameNotPath locks in the audit finding for the
-// setdomainname(2) syscall (`int setdomainname(const char *name, size_t len)`).
-// Its first arg is a `const char *name`, but that name is the NIS/YP domain
-// name string — NOT a filesystem path. The name-only table therefore pins it to
-// KindNull so the field-based path heuristic (which would otherwise treat a
-// `const char *name` arg as KindPathname) never fires. The sibling sethostname
-// must classify identically, and both must share a family, so the two stay
-// consistent.
-func TestClassifySetdomainnameNotPath(t *testing.T) {
- // Realistic format including the const char *name arg that the path
- // heuristic would latch onto if the name-only table did not win first.
- setdomainname := ClassifyFormat(&Format{
- Name: "sys_enter_setdomainname",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "const char *", Name: "name"},
- {Type: "size_t", Name: "len"},
- },
- })
- if setdomainname.Kind != KindNull {
- t.Fatalf("setdomainname: got kind %d, want KindNull (domain name is not a path)", setdomainname.Kind)
- }
- if setdomainname.PathnameField != "" {
- t.Fatalf("setdomainname: PathnameField=%q, want empty (must not be captured as a path)", setdomainname.PathnameField)
- }
-
- sethostname := ClassifyFormat(&Format{
- Name: "sys_enter_sethostname",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "const char *", Name: "name"},
- {Type: "size_t", Name: "len"},
- },
- })
- if sethostname.Kind != setdomainname.Kind {
- t.Fatalf("sethostname kind %d != setdomainname kind %d: siblings must agree", sethostname.Kind, setdomainname.Kind)
- }
-
- if got := ClassifySyscallFamily("sys_enter_setdomainname"); got != FamilyMisc {
- t.Fatalf("setdomainname: family=%q, want %q", got, FamilyMisc)
- }
- if got, want := ClassifySyscallFamily("sys_enter_setdomainname"), ClassifySyscallFamily("sys_enter_sethostname"); got != want {
- t.Fatalf("setdomainname family %q != sethostname family %q: siblings must agree", got, want)
- }
-}
-
-func TestClassifyMount(t *testing.T) {
- r := classifyFromData(t, FormatMount)
- if r.Kind != KindPathname {
- t.Errorf("mount: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "dir_name" {
- t.Errorf("mount: PathnameField = %q, want dir_name", r.PathnameField)
- }
-}
-
-func TestClassifyUmount(t *testing.T) {
- r := classifyFromData(t, FormatUmount)
- if r.Kind != KindPathname {
- t.Errorf("umount: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "name" {
- t.Errorf("umount: PathnameField = %q, want name", r.PathnameField)
- }
-}
-
-func TestClassifyMoveMount(t *testing.T) {
- r := classifyFromData(t, FormatMoveMount)
- if r.Kind != KindTwoFd {
- t.Errorf("move_mount: got kind %d, want KindTwoFd", r.Kind)
- }
-}
-
-func TestClassifyFsmount(t *testing.T) {
- r := classifyFromData(t, FormatFsmount)
- if r.Kind != KindEventfd {
- t.Errorf("fsmount: got kind %d, want KindEventfd", r.Kind)
- }
-}
-
-func TestClassifyExitFsmount(t *testing.T) {
- r := classifyFromData(t, FormatExitFsmount)
- if r.Kind != KindEventfd {
- t.Errorf("exit_fsmount: got kind %d, want KindEventfd", r.Kind)
- }
-}
-
-func TestClassifyPivotRoot(t *testing.T) {
- r := classifyFromData(t, FormatPivotRoot)
- if r.Kind != KindPathname {
- t.Errorf("pivot_root: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "new_root" {
- t.Errorf("pivot_root: PathnameField = %q, want new_root", r.PathnameField)
- }
-}
-
-func TestClassifyQuotactl(t *testing.T) {
- r := classifyFromData(t, FormatQuotactl)
- if r.Kind != KindPathname {
- t.Errorf("quotactl: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "special" {
- t.Errorf("quotactl: PathnameField = %q, want special", r.PathnameField)
- }
-}
-
-func TestClassifyStatmount(t *testing.T) {
- r := classifyFromData(t, FormatStatmount)
- if r.Kind != KindNull {
- t.Errorf("statmount: got kind %d, want KindNull", r.Kind)
- }
-}
-
-func TestClassifyListmount(t *testing.T) {
- r := classifyFromData(t, FormatListmount)
- if r.Kind != KindNull {
- t.Errorf("listmount: got kind %d, want KindNull", r.Kind)
- }
-}
-
-func TestClassifyListns(t *testing.T) {
- r := classifyFromData(t, FormatListns)
- if r.Kind != KindNull {
- t.Errorf("listns: got kind %d, want KindNull", r.Kind)
- }
-}
-
-func TestClassifySwapon(t *testing.T) {
- r := classifyFromData(t, FormatSwapon)
- if r.Kind != KindPathname {
- t.Errorf("swapon: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "specialfile" {
- t.Errorf("swapon: PathnameField = %q, want specialfile", r.PathnameField)
- }
-}
-
-func TestClassifySwapoff(t *testing.T) {
- r := classifyFromData(t, FormatSwapoff)
- if r.Kind != KindPathname {
- t.Errorf("swapoff: got kind %d, want KindPathname", r.Kind)
- }
- if r.PathnameField != "specialfile" {
- t.Errorf("swapoff: PathnameField = %q, want specialfile", r.PathnameField)
- }
-}
-
-// TestClassifyKillExplicitNull locks in the kill(2) enter classification using
-// the syscall's REAL tracepoint fields. kill(pid_t pid, int sig) sends signal
-// sig to a process (or process group); both arguments are integers — a process/
-// process-group identifier and a signal number — NOT a file descriptor and NOT
-// a filesystem path. The audit concern is that args[0] ("pid") could be mistaken
-// for an fd: it must not be. kill has no fd or path argument, so its enter format
-// must classify as KindNull (null_event), matching its signal siblings tkill/
-// tgkill/rt_sigqueueinfo and the explicit name-only mapping in classify.go.
-// (Its pidfd-taking sibling pidfd_send_signal differs deliberately: args[0]
-// there is a real pidfd file descriptor, so that one is KindFd/FamilyIPC.)
-func TestClassifyKillExplicitNull(t *testing.T) {
- r := classifyFromData(t, FormatKill)
- if r.Kind != KindNull {
- t.Errorf("kill: got kind %d, want KindNull", r.Kind)
- }
- // Neither the pid nor the sig argument must be captured as a path/fd.
- if r.PathnameField != "" {
- t.Errorf("kill: unexpected PathnameField %q, want empty", r.PathnameField)
- }
-}
-
-// TestClassifyExitKillUnclassifiedRet locks in that the kill exit tracepoint is
-// classified as KindRet and Unclassified. kill(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. This matches its signal siblings
-// (tkill/tgkill/rt_sigqueueinfo); misclassifying it as a READ/WRITE/TRANSFER
-// byte count would be a real bug.
-func TestClassifyExitKillUnclassifiedRet(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: "sys_exit_kill",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "ret"},
- },
- })
- if r.Kind != KindRet {
- t.Fatalf("exit_kill: got kind %d, want KindRet", r.Kind)
- }
- if got := ClassifyRet("sys_exit_kill"); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_kill) = %q, want UNCLASSIFIED", got)
- }
-}
-
-func TestClassifyNullExitByName(t *testing.T) {
- tests := []string{"sys_enter_exit", "sys_enter_exit_group"}
- for _, name := range tests {
- t.Run(name, func(t *testing.T) {
- r := ClassifyFormat(&Format{
- Name: name,
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "int", Name: "error_code"},
- },
- })
- if r.Kind != KindNull {
- t.Errorf("%s: got kind %d, want KindNull", name, r.Kind)
- }
- })
- }
-}
-
-// --- End-to-end classification with enter+exit pairs ---
+// --- End-to-end codegen: enter+exit pairs must be accepted and emit handlers ---
func TestClassifySyscallPairAccepted(t *testing.T) {
tests := []struct {
@@ -2616,6 +372,8 @@ func TestClassifyMqSyscallPairsAcceptedAndClassified(t *testing.T) {
}
}
+// --- Shared test helpers (used here and by codegen_test.go) ---
+
func phaseAFormats(name string, enterID int) []Format {
enterFields := []Field{
{Type: "long", Name: "__syscall_nr"},
@@ -2692,887 +450,6 @@ func itoa(v int) string {
return strconv.Itoa(v)
}
-func TestClassifyFormatNoExternalFields(t *testing.T) {
- f := &Format{
- Name: "sys_enter_test",
- ID: 999,
- ExternalFields: nil,
- }
- r := ClassifyFormat(f)
- if r.Kind != KindNone {
- t.Errorf("ClassifyFormat with empty ExternalFields: got kind %d, want KindNone", r.Kind)
- }
-}
-
-func TestClassifyNameOnlyTables(t *testing.T) {
- tests := []struct {
- name string
- want TracepointKind
- }{
- {"sys_enter_open_by_handle_at", KindOpenByHandleAt},
- {"sys_exit_socketpair", KindSocketpair},
- {"sys_enter_pidfd_open", KindPidfd},
- {"sys_enter_epoll_ctl", KindEpollCtl},
- {"sys_enter_perf_event_open", KindPerfOpen},
- {"sys_enter_futex", KindFutex},
- {"sys_enter_clone", KindProc},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r, ok := classifyNameOnly(tt.name)
- if !ok {
- t.Fatalf("classifyNameOnly(%q) did not match", tt.name)
- }
- if r.Kind != tt.want {
- t.Fatalf("classifyNameOnly(%q) kind = %d, want %d", tt.name, r.Kind, tt.want)
- }
- })
- }
-}
-
-func TestClassifyNameOnlyPrefixKinds(t *testing.T) {
- r, ok := classifyNameOnly("sys_enter_io_destroy")
- if !ok {
- t.Fatal("classifyNameOnly(sys_enter_io_destroy) did not match prefix metadata")
- }
- if r.Kind != KindNull {
- t.Fatalf("classifyNameOnly(sys_enter_io_destroy) kind = %d, want KindNull", r.Kind)
- }
-
- r, ok = classifyNameOnly("sys_enter_io_uring_enter")
- if !ok {
- t.Fatal("classifyNameOnly(sys_enter_io_uring_enter) did not match exact metadata")
- }
- if r.Kind != KindFd {
- t.Fatalf("classifyNameOnly(sys_enter_io_uring_enter) kind = %d, want KindFd", r.Kind)
- }
-}
-
-// TestIoUringRegisterTablePrecedenceOverIoPrefix is an audit lock-in for
-// io_uring_register(2): int io_uring_register(unsigned int fd, unsigned int
-// opcode, void *arg, unsigned int nr_args). The io_uring fd lives at args[0],
-// so the syscall MUST classify as KindFd (a single-fd fd_event) rather than
-// falling through to the generic `sys_enter_io_` KindNull prefix rule shared
-// with io_setup/io_destroy/io_submit/io_getevents. classifyNameOnly consults
-// the exact nameOnlyKindsTable BEFORE the nameOnlyPrefixKinds list, so the
-// explicit KindFd entry wins. This test would fail if that entry were removed
-// (leaving the prefix rule to wrongly produce KindNull and drop the fd) or if
-// table-vs-prefix lookup order were reversed.
-func TestIoUringRegisterTablePrecedenceOverIoPrefix(t *testing.T) {
- r, ok := classifyNameOnly("sys_enter_io_uring_register")
- if !ok {
- t.Fatal("classifyNameOnly(sys_enter_io_uring_register) did not match")
- }
- if r.Kind != KindFd {
- t.Fatalf("io_uring_register kind = %d, want KindFd (fd at args[0]); the "+
- "sys_enter_io_ KindNull prefix rule must not win", r.Kind)
- }
-
- // Sanity check the prefix rule itself still maps the io_* AIO siblings that
- // have no fd argument to KindNull, so the precedence above is meaningful.
- if pr, ok := classifyNameOnly("sys_enter_io_submit"); !ok || pr.Kind != KindNull {
- t.Fatalf("sys_enter_io_submit kind = %d (ok=%v), want KindNull via prefix rule", pr.Kind, ok)
- }
-}
-
-// TestIoUringRegisterReturnUnclassified locks in that io_uring_register's exit
-// is UNCLASSIFIED: on success it returns 0 or a small positive value (e.g. an
-// fd or count specific to the opcode), never a byte-transfer count, so it must
-// not appear in retClassifications as READ/WRITE/TRANSFER. Treating it as a
-// byte count would corrupt throughput accounting. Its io_uring siblings
-// (io_uring_setup/io_uring_enter) are likewise unclassified.
-func TestIoUringRegisterReturnUnclassified(t *testing.T) {
- for _, name := range []string{
- "sys_exit_io_uring_register",
- "sys_exit_io_uring_enter",
- "sys_exit_io_uring_setup",
- } {
- if got := ClassifyRet(name); got != Unclassified {
- t.Errorf("ClassifyRet(%q) = %q, want %q", name, got, Unclassified)
- }
- }
-}
-
-func TestClassifyNameOnlyUnknown(t *testing.T) {
- if r, ok := classifyNameOnly("sys_enter_not_real"); ok {
- t.Fatalf("classifyNameOnly matched unknown syscall with kind %d", r.Kind)
- }
-}
-
-func TestNameOnlyKindMetadataIsValid(t *testing.T) {
- for name, kind := range nameOnlyKindsTable {
- if name == "" {
- t.Fatal("nameOnlyKindsTable contains an empty syscall name")
- }
- if kind == KindNone {
- t.Fatalf("nameOnlyKindsTable[%q] = KindNone", name)
- }
- }
-
- for _, prefixKind := range nameOnlyPrefixKinds {
- if prefixKind.prefix == "" {
- t.Fatal("nameOnlyPrefixKinds contains an empty prefix")
- }
- if prefixKind.kind == KindNone {
- t.Fatalf("nameOnlyPrefixKinds[%q] = KindNone", prefixKind.prefix)
- }
- }
-}
-
-func TestIsFdTypeNonMatch(t *testing.T) {
- nonFdTypes := []string{
- "const char *",
- "long",
- "size_t",
- "pid_t",
- "umode_t",
- "char *",
- "void *",
- }
- for _, typ := range nonFdTypes {
- if isFdType(typ) {
- t.Errorf("isFdType(%q) = true, want false", typ)
- }
- }
-}
-
-// TestClassifySchedGetattrPidNotFd is a lock-in regression test for the
-// sched_getattr audit. The syscall signature is:
-//
-// int sched_getattr(pid_t pid, struct sched_attr *attr,
-// unsigned int size, unsigned int flags)
-//
-// args[0] is a PID (a process/thread id), NOT a file descriptor, and attr is
-// a userspace output pointer. The expected classification is KindNull (no fd,
-// pathname, or other resource handle is extracted on enter) and family Sched.
-//
-// The critical invariant guarded here is that the pid argument must never be
-// misclassified as an fd. ClassifyFormat resolves sched_getattr via the
-// name-only table first, which short-circuits before any field heuristic;
-// this test pins that behavior even when the real kernel field layout (whose
-// first arg is named "pid", not "fd") is supplied.
-func TestClassifySchedGetattrPidNotFd(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_sched_getattr: pid_t pid, struct sched_attr *uattr,
- // unsigned int usize, unsigned int flags.
- r := ClassifyFormat(&Format{
- Name: "sys_enter_sched_getattr",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "pid_t", Name: "pid"},
- {Type: "struct sched_attr *", Name: "uattr"},
- {Type: "unsigned int", Name: "usize"},
- {Type: "unsigned int", Name: "flags"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("sched_getattr: got kind %d, want KindNull (pid arg must not be treated as fd)", r.Kind)
- }
- if r.Kind == KindFd {
- t.Fatalf("sched_getattr: pid arg misclassified as fd")
- }
-
- // Family must match the Sched siblings (sched_setattr, sched_getparam,
- // sched_getscheduler, ...).
- if fam := ClassifySyscallFamily("sys_enter_sched_getattr"); fam != FamilySched {
- t.Fatalf("sched_getattr: got family %s, want FamilySched", fam)
- }
-
- // Sanity-check sibling consistency: the matching setter and the other
- // getters share the same family and KindNull classification.
- siblings := []string{
- "sys_enter_sched_setattr",
- "sys_enter_sched_getparam",
- "sys_enter_sched_getscheduler",
- }
- for _, name := range siblings {
- s := ClassifyFormat(&Format{
- Name: name,
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "arg0"},
- },
- })
- if s.Kind != KindNull {
- t.Errorf("%s: got kind %d, want KindNull", name, s.Kind)
- }
- if fam := ClassifySyscallFamily(name); fam != FamilySched {
- t.Errorf("%s: got family %s, want FamilySched", name, fam)
- }
- }
-}
-
-// TestClassifySchedGetparamPidNotFd is a lock-in regression test for the
-// sched_getparam(2) audit. The syscall signature is:
-//
-// int sched_getparam(pid_t pid, struct sched_param *param)
-//
-// args[0] is a PID (a thread/process id; 0 means the calling thread), NOT a
-// file descriptor, and param is a userspace output pointer to a struct
-// sched_param. No fd or filesystem path is involved, so the enter tracepoint
-// must classify as KindNull (plain null_event; the pid must never be picked up
-// as an fd). On success sched_getparam returns 0 (-1 on error) and transfers no
-// byte count, so its exit stays KindRet / UNCLASSIFIED — exactly like its
-// sibling sched_getattr and the setter sched_setparam.
-func TestClassifySchedGetparamPidNotFd(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_sched_getparam: pid_t pid, struct sched_param *param.
- r := ClassifyFormat(&Format{
- Name: "sys_enter_sched_getparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "pid_t", Name: "pid"},
- {Type: "struct sched_param *", Name: "param"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("sched_getparam: got kind %d, want KindNull (pid arg must not be treated as fd)", r.Kind)
- }
- if r.Kind == KindFd {
- t.Fatalf("sched_getparam: pid arg misclassified as fd")
- }
-
- // Family must match the Sched siblings (sched_setparam, sched_getattr, ...).
- if fam := ClassifySyscallFamily("sys_enter_sched_getparam"); fam != FamilySched {
- t.Fatalf("sched_getparam: got family %s, want FamilySched", fam)
- }
-
- // Exit returns int 0/-1 (a status code, not a transferred byte count), so
- // the return must classify as KindRet / UNCLASSIFIED.
- exit := ClassifyFormat(&Format{
- Name: "sys_exit_sched_getparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "ret"},
- },
- })
- if exit.Kind != KindRet {
- t.Fatalf("exit_sched_getparam: got kind %d, want KindRet", exit.Kind)
- }
- if got := ClassifyRet("sys_exit_sched_getparam"); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_sched_getparam) = %q, want UNCLASSIFIED", got)
- }
-
- // Sibling consistency: the matching setter shares family Sched and KindNull.
- if s := ClassifyFormat(&Format{
- Name: "sys_enter_sched_setparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "pid_t", Name: "pid"},
- {Type: "struct sched_param *", Name: "param"},
- },
- }); s.Kind != KindNull {
- t.Errorf("sched_setparam: got kind %d, want KindNull", s.Kind)
- }
- if fam := ClassifySyscallFamily("sys_enter_sched_setparam"); fam != FamilySched {
- t.Errorf("sched_setparam: got family %s, want FamilySched", fam)
- }
-}
-
-// TestClassifySchedSetparamPidNotFd is a lock-in regression test for the
-// sched_setparam(2) audit. The syscall signature is:
-//
-// int sched_setparam(pid_t pid, const struct sched_param *param)
-//
-// args[0] is a PID (the thread/process whose scheduling parameters are set; 0
-// means the calling thread), NOT a file descriptor, and param is a userspace
-// input pointer to a struct sched_param. No fd or filesystem path is involved,
-// so the enter tracepoint must classify as KindNull (plain null_event; the pid
-// must never be picked up as an fd). On success sched_setparam returns 0 (-1 on
-// error) and transfers no byte count, so its exit stays KindRet / UNCLASSIFIED
-// — exactly like its getter sibling sched_getparam and sched_setscheduler.
-func TestClassifySchedSetparamPidNotFd(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_sched_setparam: pid_t pid, const struct sched_param *param.
- r := ClassifyFormat(&Format{
- Name: "sys_enter_sched_setparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "pid_t", Name: "pid"},
- {Type: "const struct sched_param *", Name: "param"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("sched_setparam: got kind %d, want KindNull (pid arg must not be treated as fd)", r.Kind)
- }
- if r.Kind == KindFd {
- t.Fatalf("sched_setparam: pid arg misclassified as fd")
- }
-
- // Family must match the Sched siblings (sched_getparam, sched_setscheduler, ...).
- if fam := ClassifySyscallFamily("sys_enter_sched_setparam"); fam != FamilySched {
- t.Fatalf("sched_setparam: got family %s, want FamilySched", fam)
- }
-
- // Exit returns int 0/-1 (a status code, not a transferred byte count), so
- // the return must classify as KindRet / UNCLASSIFIED.
- exit := ClassifyFormat(&Format{
- Name: "sys_exit_sched_setparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "long", Name: "ret"},
- },
- })
- if exit.Kind != KindRet {
- t.Fatalf("exit_sched_setparam: got kind %d, want KindRet", exit.Kind)
- }
- if got := ClassifyRet("sys_exit_sched_setparam"); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_sched_setparam) = %q, want UNCLASSIFIED", got)
- }
-
- // Sibling consistency: the matching getter shares family Sched and KindNull.
- if g := ClassifyFormat(&Format{
- Name: "sys_enter_sched_getparam",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "pid_t", Name: "pid"},
- {Type: "struct sched_param *", Name: "param"},
- },
- }); g.Kind != KindNull {
- t.Errorf("sched_getparam: got kind %d, want KindNull", g.Kind)
- }
- if fam := ClassifySyscallFamily("sys_enter_sched_getparam"); fam != FamilySched {
- t.Errorf("sched_getparam: got family %s, want FamilySched", fam)
- }
-}
-
-// TestClassifyGetRobustListPidNotFd is a lock-in regression test for the
-// get_robust_list(2) / set_robust_list(2) audit. The signatures are:
-//
-// long get_robust_list(int pid, struct robust_list_head **head_ptr, size_t *len_ptr)
-// long set_robust_list(struct robust_list_head *head, size_t len)
-//
-// get_robust_list's args[0] is a PID (a thread/process identifier), NOT a file
-// descriptor, and head_ptr/len_ptr are userspace output pointers; set_robust_list
-// takes a userspace head pointer and a length. Neither syscall touches an fd or a
-// filesystem path, so both enter as KindNull (plain null_event). On success both
-// return 0 (-1 on error) and transfer no byte count, so their exits stay
-// UNCLASSIFIED.
-//
-// Invariants pinned here:
-// - enter classifies as KindNull (the pid arg must NOT be picked up as an fd),
-// - family is Misc — grouped with the per-thread sibling rseq, not promoted to
-// IPC like the futex_* shared-memory primitives (see family.go / family_test.go),
-// - return classifies as UNCLASSIFIED (0/-1, no byte transfer).
-//
-// FAMILY DECISION (resolved): get_robust_list/set_robust_list stay FamilyMisc and
-// are NOT moved to FamilyIPC for "futex consistency". The boundary rule (spelled
-// out next to the futex block in family.go) is operation-vs-registration: a
-// syscall is IPC only if it PERFORMS the actual IPC/sync operation (futex
-// wait/wake/requeue, or an op on an IPC object). These two only register/query
-// the per-thread robust-futex list head pointer — per get_robust_list(2) the list
-// is "managed in user space: the kernel knows only about the location of the head"
-// — and never wait, wake, or touch the shared futex word. That is per-thread
-// bookkeeping, structurally identical to rseq, so they share rseq's Misc family.
-// The contrast assertion below (futex == IPC) pins both sides of the boundary.
-func TestClassifyGetRobustListPidNotFd(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_get_robust_list: int pid, struct robust_list_head **head_ptr,
- // size_t *len_ptr.
- r := ClassifyFormat(&Format{
- Name: "sys_enter_get_robust_list",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "int", Name: "pid"},
- {Type: "struct robust_list_head **", Name: "head_ptr"},
- {Type: "size_t *", Name: "len_ptr"},
- },
- })
- if r.Kind != KindNull {
- t.Fatalf("get_robust_list: got kind %d, want KindNull (pid arg must not be treated as fd)", r.Kind)
- }
-
- // set_robust_list takes a userspace head pointer and a length, no fd/path.
- s := ClassifyFormat(&Format{
- Name: "sys_enter_set_robust_list",
- ExternalFields: []Field{
- {Type: "long", Name: "__syscall_nr"},
- {Type: "struct robust_list_head *", Name: "head"},
- {Type: "size_t", Name: "len"},
- },
- })
- if s.Kind != KindNull {
- t.Fatalf("set_robust_list: got kind %d, want KindNull", s.Kind)
- }
-
- // Both syscalls are FamilyMisc, sharing the family with their per-thread
- // sibling rseq (and NOT promoted to FamilyIPC like the futex_* primitives).
- for _, name := range []string{
- "sys_enter_get_robust_list",
- "sys_enter_set_robust_list",
- "sys_enter_rseq",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyMisc {
- t.Errorf("%s: got family %s, want FamilyMisc", name, fam)
- }
- }
-
- // Contrast: the futex_* siblings ARE classified IPC, so the robust-list pair
- // is deliberately NOT lumped in with them at the family level.
- if fam := ClassifySyscallFamily("sys_enter_futex"); fam != FamilyIPC {
- t.Errorf("futex: got family %s, want FamilyIPC (contrast case)", fam)
- }
-
- // Returns: both exits are UNCLASSIFIED (0/-1, no byte transfer).
- for _, name := range []string{"get_robust_list", "set_robust_list"} {
- if got := ClassifyRet("sys_exit_" + name); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_%s) = %q, want UNCLASSIFIED", name, got)
- }
- }
-}
-
-// TestClassifyRecvmsgReadByteCount is a lock-in regression test for the
-// recvmsg(2) audit. The syscall signature is:
-//
-// ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
-//
-// args[0] is a socket file descriptor (the real kernel tracepoint format names
-// the first field "fd"), msg is a userspace output pointer, and flags is an int.
-// On success recvmsg returns the NUMBER OF BYTES RECEIVED, so its exit must be
-// READ_CLASSIFIED — those bytes are counted as read, exactly like the
-// recvfrom/recv/read/readv siblings, and never as a write.
-//
-// The invariants pinned here:
-// - enter classifies as KindFd off the first "fd" field (sockfd is an fd),
-// - family is Network (matches sendmsg/recvfrom/socket siblings),
-// - return classifies as READ_CLASSIFIED (bytes-received → read).
-//
-// Contrast cases guard against the two easy mistakes: sendmsg is the write-side
-// sibling (WRITE_CLASSIFIED, never read), and recvmmsg is the batch variant
-// whose per-message byte counts cannot be derived from its scalar return value
-// (it returns a message count), so it is deliberately deferred to UNCLASSIFIED
-// rather than READ_CLASSIFIED.
-func TestClassifyRecvmsgReadByteCount(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_recvmsg: int fd, struct user_msghdr *msg, unsigned int flags.
- recvmsg := ClassifyFormat(&Format{
- Name: "sys_enter_recvmsg",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "fd"},
- {Type: "struct user_msghdr *", Name: "msg"},
- {Type: "unsigned int", Name: "flags"},
- },
- })
- if recvmsg.Kind != KindFd {
- t.Fatalf("recvmsg: got kind %d, want KindFd (sockfd at args[0] is an fd)", recvmsg.Kind)
- }
-
- if fam := ClassifySyscallFamily("sys_enter_recvmsg"); fam != FamilyNetwork {
- t.Fatalf("recvmsg: got family %s, want FamilyNetwork", fam)
- }
-
- // Return value is a byte count of data received → counted as read.
- if got := ClassifyRet("sys_exit_recvmsg"); got != ReadClassified {
- t.Fatalf("recvmsg: ClassifyRet = %q, want READ_CLASSIFIED (return is bytes received)", got)
- }
-
- // sendmsg is the write-side sibling; it must never be a read.
- if got := ClassifyRet("sys_exit_sendmsg"); got != WriteClassified {
- t.Fatalf("sendmsg: ClassifyRet = %q, want WRITE_CLASSIFIED", got)
- }
-
- // recvmmsg is the batch variant: its scalar return is a message count, not a
- // byte count, so it defers byte classification (UNCLASSIFIED) rather than
- // being mislabeled as a read.
- if got := ClassifyRet("sys_exit_recvmmsg"); got != Unclassified {
- t.Fatalf("recvmmsg: ClassifyRet = %q, want UNCLASSIFIED (batch return is not a byte count)", got)
- }
-
- // The single-message read siblings all count their return as bytes read.
- for _, name := range []string{
- "sys_exit_recvfrom",
- "sys_exit_read",
- "sys_exit_readv",
- } {
- if got := ClassifyRet(name); got != ReadClassified {
- t.Errorf("%s: ClassifyRet = %q, want READ_CLASSIFIED", name, got)
- }
- }
-
- // All read/write message-socket siblings share the Network family.
- for _, name := range []string{
- "sys_enter_sendmsg",
- "sys_enter_recvmmsg",
- "sys_enter_recvfrom",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyNetwork {
- t.Errorf("%s: got family %s, want FamilyNetwork", name, fam)
- }
- }
-}
-
-// TestClassifySendmsgWriteByteCount is a lock-in regression test for the
-// sendmsg(2) audit. The syscall signature is:
-//
-// ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
-//
-// args[0] is a socket file descriptor (the real kernel tracepoint format names
-// the first field "fd"), msg is a userspace input pointer, and flags is an int.
-// On success sendmsg returns the NUMBER OF BYTES SENT, so its exit must be
-// WRITE_CLASSIFIED — those bytes are counted as written, exactly like the
-// send/sendto/write siblings, and never as a read.
-//
-// The invariants pinned here:
-// - enter classifies as KindFd off the first "fd" field (sockfd is an fd),
-// - family is Network (matches sendto/recvfrom/recvmsg/socket siblings),
-// - return classifies as WRITE_CLASSIFIED (bytes-sent → written).
-//
-// Contrast cases guard against the two easy mistakes: recvmsg is the read-side
-// sibling (READ_CLASSIFIED, never write), and sendmmsg is the batch variant
-// whose per-message byte counts cannot be derived from its scalar return value,
-// so it is deliberately deferred to UNCLASSIFIED rather than WRITE_CLASSIFIED.
-func TestClassifySendmsgWriteByteCount(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_sendmsg: int fd, struct user_msghdr *msg, unsigned int flags.
- sendmsg := ClassifyFormat(&Format{
- Name: "sys_enter_sendmsg",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "int", Name: "fd"},
- {Type: "struct user_msghdr *", Name: "msg"},
- {Type: "unsigned int", Name: "flags"},
- },
- })
- if sendmsg.Kind != KindFd {
- t.Fatalf("sendmsg: got kind %d, want KindFd (sockfd at args[0] is an fd)", sendmsg.Kind)
- }
-
- if fam := ClassifySyscallFamily("sys_enter_sendmsg"); fam != FamilyNetwork {
- t.Fatalf("sendmsg: got family %s, want FamilyNetwork", fam)
- }
-
- // Return value is a byte count of data sent → counted as written.
- if got := ClassifyRet("sys_exit_sendmsg"); got != WriteClassified {
- t.Fatalf("sendmsg: ClassifyRet = %q, want WRITE_CLASSIFIED (return is bytes sent)", got)
- }
-
- // recvmsg is the read-side sibling; it must never be a write.
- if got := ClassifyRet("sys_exit_recvmsg"); got != ReadClassified {
- t.Fatalf("recvmsg: ClassifyRet = %q, want READ_CLASSIFIED", got)
- }
-
- // sendmmsg is the batch variant: its scalar return is a message count, not a
- // byte count, so it defers byte classification (UNCLASSIFIED) rather than
- // being mislabeled as a write.
- if got := ClassifyRet("sys_exit_sendmmsg"); got != Unclassified {
- t.Fatalf("sendmmsg: ClassifyRet = %q, want UNCLASSIFIED (batch return is not a byte count)", got)
- }
-
- // All three send/recv message siblings share the Network family.
- for _, name := range []string{
- "sys_enter_recvmsg",
- "sys_enter_sendmmsg",
- "sys_enter_sendto",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyNetwork {
- t.Errorf("%s: got family %s, want FamilyNetwork", name, fam)
- }
- }
-}
-
-// TestClassifyPwritev2WriteByteCount locks in the classification of the
-// pwritev2(2) audit. The syscall signature is:
-//
-// ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt,
-// off_t offset, int flags)
-//
-// args[0] is the file descriptor written to (the real kernel tracepoint format
-// names the first field "fd"), iov/iovcnt describe the gather buffers, offset is
-// the file position, and flags is an int (RWF_*). On success pwritev2 returns
-// the NUMBER OF BYTES WRITTEN, so its exit must be WRITE_CLASSIFIED — those
-// bytes are counted as written, exactly like the pwritev/writev/write/pwrite64
-// siblings, and never as a read.
-//
-// The invariants pinned here:
-// - enter classifies as KindFd off the first "fd" field (fd at args[0]),
-// - family is FS (matches pwritev/writev/pwrite64/preadv2 file-IO siblings),
-// - return classifies as WRITE_CLASSIFIED (bytes-written → written).
-//
-// Contrast case guards the easy off-by-one mistake: preadv2 is the read-side
-// sibling (READ_CLASSIFIED, never write). The whole p/readv/writev family is
-// asserted together so a stray reclassification of any sibling trips the test.
-func TestClassifyPwritev2WriteByteCount(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_pwritev2: int fd, struct iovec *vec, unsigned long vlen,
- // unsigned long pos_l, unsigned long pos_h, rwf_t flags.
- pwritev2 := ClassifyFormat(&Format{
- Name: "sys_enter_pwritev2",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "unsigned long", Name: "fd"},
- {Type: "const struct iovec *", Name: "vec"},
- {Type: "unsigned long", Name: "vlen"},
- {Type: "unsigned long", Name: "pos_l"},
- {Type: "unsigned long", Name: "pos_h"},
- {Type: "rwf_t", Name: "flags"},
- },
- })
- if pwritev2.Kind != KindFd {
- t.Fatalf("pwritev2: got kind %d, want KindFd (fd at args[0])", pwritev2.Kind)
- }
-
- if fam := ClassifySyscallFamily("sys_enter_pwritev2"); fam != FamilyFS {
- t.Fatalf("pwritev2: got family %s, want FamilyFS", fam)
- }
-
- // Return value is a byte count of data written → counted as written.
- if got := ClassifyRet("sys_exit_pwritev2"); got != WriteClassified {
- t.Fatalf("pwritev2: ClassifyRet = %q, want WRITE_CLASSIFIED (return is bytes written)", got)
- }
-
- // preadv2 is the read-side sibling; it must never be a write.
- if got := ClassifyRet("sys_exit_preadv2"); got != ReadClassified {
- t.Fatalf("preadv2: ClassifyRet = %q, want READ_CLASSIFIED", got)
- }
-
- // All write-side vectored/positional siblings count their byte-count return
- // as written; assert the whole group so a stray reclassification trips here.
- for _, name := range []string{
- "sys_exit_pwritev",
- "sys_exit_writev",
- "sys_exit_write",
- "sys_exit_pwrite64",
- } {
- if got := ClassifyRet(name); got != WriteClassified {
- t.Errorf("%s: ClassifyRet = %q, want WRITE_CLASSIFIED", name, got)
- }
- }
-
- // All read-side siblings stay READ_CLASSIFIED (off-by-one guard).
- for _, name := range []string{
- "sys_exit_preadv",
- "sys_exit_readv",
- "sys_exit_read",
- "sys_exit_pread64",
- } {
- if got := ClassifyRet(name); got != ReadClassified {
- t.Errorf("%s: ClassifyRet = %q, want READ_CLASSIFIED", name, got)
- }
- }
-
- // The whole p/readv/writev family shares the FS family.
- for _, name := range []string{
- "sys_enter_pwritev",
- "sys_enter_writev",
- "sys_enter_write",
- "sys_enter_pwrite64",
- "sys_enter_preadv2",
- "sys_enter_preadv",
- "sys_enter_readv",
- "sys_enter_read",
- "sys_enter_pread64",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyFS {
- t.Errorf("%s: got family %s, want FamilyFS", name, fam)
- }
- }
-}
-
-// TestClassifyPwritevWriteByteCount locks in the classification of the
-// pwritev(2) audit. The syscall signature is:
-//
-// ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, off_t offset)
-//
-// pwritev gathers iovcnt buffers and writes them to fd at the absolute file
-// offset (it does NOT advance the file offset, and unlike pwritev2 it takes no
-// flags argument — that flags field is the only structural difference from its
-// pwritev2 sibling). args[0] is the file descriptor written to (the real kernel
-// tracepoint format names the first field "fd"; vec/vlen describe the gather
-// buffers and pos_l/pos_h carry the 64-bit offset). On success pwritev returns
-// the NUMBER OF BYTES WRITTEN, so its exit must be WRITE_CLASSIFIED — those
-// bytes are counted as written, exactly like the write/pwrite64/writev/pwritev2
-// siblings, and never as a read.
-//
-// The invariants pinned here:
-// - enter classifies as KindFd off the first "fd" field (fd at args[0]),
-// - family is FS (matches write/pwrite64/writev/pwritev2 file-IO siblings),
-// - return classifies as WRITE_CLASSIFIED (bytes-written → written).
-//
-// Contrast case guards the easy off-by-one mistake in the read/write tables:
-// preadv is the read-side vectored+positional counterpart (READ_CLASSIFIED,
-// never write). Both directions are asserted together so a stray
-// reclassification of either side trips the test, and the write-side return is
-// explicitly checked not to leak into the transfer/unclassified buckets.
-func TestClassifyPwritevWriteByteCount(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_pwritev: unsigned long fd, const struct iovec *vec,
- // unsigned long vlen, unsigned long pos_l, unsigned long pos_h.
- // (No flags field — that is what distinguishes pwritev from pwritev2.)
- pwritev := ClassifyFormat(&Format{
- Name: "sys_enter_pwritev",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "unsigned long", Name: "fd"},
- {Type: "const struct iovec *", Name: "vec"},
- {Type: "unsigned long", Name: "vlen"},
- {Type: "unsigned long", Name: "pos_l"},
- {Type: "unsigned long", Name: "pos_h"},
- },
- })
- if pwritev.Kind != KindFd {
- t.Fatalf("pwritev: got kind %d, want KindFd (fd at args[0])", pwritev.Kind)
- }
-
- if fam := ClassifySyscallFamily("sys_enter_pwritev"); fam != FamilyFS {
- t.Fatalf("pwritev: got family %s, want FamilyFS", fam)
- }
-
- // Return value is a byte count of data written → counted as written.
- if got := ClassifyRet("sys_exit_pwritev"); got != WriteClassified {
- t.Fatalf("pwritev: ClassifyRet = %q, want WRITE_CLASSIFIED (return is bytes written)", got)
- }
-
- // preadv is the read-side vectored+positional counterpart; it must never be
- // a write (off-by-one guard in the read/write classification tables).
- if got := ClassifyRet("sys_exit_preadv"); got != ReadClassified {
- t.Fatalf("preadv: ClassifyRet = %q, want READ_CLASSIFIED", got)
- }
-
- // pwritev must not leak into the transfer (sendfile/splice style) bucket nor
- // be left unclassified — it is a plain write byte count.
- if got := ClassifyRet("sys_exit_pwritev"); got == TransferClassified || got == Unclassified {
- t.Fatalf("pwritev: ClassifyRet = %q, want WRITE_CLASSIFIED, not transfer/unclassified", got)
- }
-
- // All vectored/positional write siblings count their byte-count return as
- // written; assert the group so a stray reclassification trips here.
- for _, name := range []string{
- "sys_exit_pwritev",
- "sys_exit_pwritev2",
- "sys_exit_writev",
- "sys_exit_write",
- "sys_exit_pwrite64",
- } {
- if got := ClassifyRet(name); got != WriteClassified {
- t.Errorf("%s: ClassifyRet = %q, want WRITE_CLASSIFIED", name, got)
- }
- }
-
- // All read-side siblings stay READ_CLASSIFIED (off-by-one guard).
- for _, name := range []string{
- "sys_exit_preadv",
- "sys_exit_preadv2",
- "sys_exit_readv",
- "sys_exit_read",
- "sys_exit_pread64",
- } {
- if got := ClassifyRet(name); got != ReadClassified {
- t.Errorf("%s: ClassifyRet = %q, want READ_CLASSIFIED", name, got)
- }
- }
-
- // The fd-bearing vectored/positional siblings all share KindFd at args[0]
- // and the FS family; pin both enter sides so a kind/family drift trips here.
- for _, name := range []string{
- "sys_enter_pwritev",
- "sys_enter_pwritev2",
- "sys_enter_writev",
- "sys_enter_write",
- "sys_enter_pwrite64",
- "sys_enter_preadv",
- "sys_enter_preadv2",
- "sys_enter_readv",
- "sys_enter_read",
- "sys_enter_pread64",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyFS {
- t.Errorf("%s: got family %s, want FamilyFS", name, fam)
- }
- }
-}
-
-// TestClassifyPwrite64WriteByteCount locks in the classification of the
-// pwrite64(2) audit. The syscall signature is:
-//
-// ssize_t pwrite64(int fd, const void *buf, size_t count, off_t offset)
-//
-// args[0] is the file descriptor written to (the kernel tracepoint format names
-// the first field "fd"), buf/count describe the source buffer, and offset is the
-// absolute file position (pwrite64 does NOT advance the file offset). On success
-// pwrite64 returns the NUMBER OF BYTES WRITTEN, so its exit must be
-// WRITE_CLASSIFIED — those bytes are counted as written, exactly like the
-// write/pwrite/pwritev/pwritev2 siblings, and never as a read.
-//
-// The invariants pinned here:
-// - enter classifies as KindFd off the first "fd" field (fd at args[0]),
-// - family is FS (matches write/pread64/pwritev file-IO siblings),
-// - return classifies as WRITE_CLASSIFIED (bytes-written → written).
-//
-// Contrast case guards the easy off-by-one mistake in the read/write tables:
-// pread64 is the read-side positional counterpart (READ_CLASSIFIED, never
-// write). Both directions are asserted together so a stray reclassification of
-// either side trips the test.
-func TestClassifyPwrite64WriteByteCount(t *testing.T) {
- // Field layout mirrors the actual kernel tracepoint format for
- // sys_enter_pwrite64: unsigned int fd, const char *buf, size_t count,
- // loff_t pos.
- pwrite64 := ClassifyFormat(&Format{
- Name: "sys_enter_pwrite64",
- ExternalFields: []Field{
- {Type: "int", Name: "__syscall_nr"},
- {Type: "unsigned int", Name: "fd"},
- {Type: "const char *", Name: "buf"},
- {Type: "size_t", Name: "count"},
- {Type: "loff_t", Name: "pos"},
- },
- })
- if pwrite64.Kind != KindFd {
- t.Fatalf("pwrite64: got kind %d, want KindFd (fd at args[0])", pwrite64.Kind)
- }
-
- if fam := ClassifySyscallFamily("sys_enter_pwrite64"); fam != FamilyFS {
- t.Fatalf("pwrite64: got family %s, want FamilyFS", fam)
- }
-
- // Return value is a byte count of data written → counted as written.
- if got := ClassifyRet("sys_exit_pwrite64"); got != WriteClassified {
- t.Fatalf("pwrite64: ClassifyRet = %q, want WRITE_CLASSIFIED (return is bytes written)", got)
- }
-
- // pread64 is the read-side positional counterpart; it must never be a
- // write (off-by-one guard in the read/write classification tables).
- if got := ClassifyRet("sys_exit_pread64"); got != ReadClassified {
- t.Fatalf("pread64: ClassifyRet = %q, want READ_CLASSIFIED", got)
- }
-
- // pwrite64 must not be misclassified as a transfer (sendfile/splice style)
- // nor left unclassified — it is a plain write byte count.
- if got := ClassifyRet("sys_exit_pwrite64"); got == TransferClassified || got == Unclassified {
- t.Fatalf("pwrite64: ClassifyRet = %q, want WRITE_CLASSIFIED, not transfer/unclassified", got)
- }
-
- // All positional/plain write siblings count their byte-count return as
- // written; assert the group so a stray reclassification trips here.
- // (There is no separate sys_exit_pwrite tracepoint on Linux: the glibc
- // pwrite() wrapper dispatches the pwrite64 syscall, so only pwrite64 has a
- // tracepoint to classify.)
- for _, name := range []string{
- "sys_exit_pwrite64",
- "sys_exit_pwritev",
- "sys_exit_pwritev2",
- "sys_exit_write",
- } {
- if got := ClassifyRet(name); got != WriteClassified {
- t.Errorf("%s: ClassifyRet = %q, want WRITE_CLASSIFIED", name, got)
- }
- }
-
- // The fd-bearing positional siblings all share KindFd at args[0] and the
- // FS family; pin both enter sides so a kind/family drift trips here.
- for _, name := range []string{
- "sys_enter_pwrite64",
- "sys_enter_pread64",
- "sys_enter_write",
- "sys_enter_read",
- } {
- if fam := ClassifySyscallFamily(name); fam != FamilyFS {
- t.Errorf("%s: got family %s, want FamilyFS", name, fam)
- }
- }
-}
-
func mustParseAll(t *testing.T, data string) []Format {
t.Helper()
formats, err := ParseFormats(strings.NewReader(data))
diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go
index a3d0500..b62c6a3 100644
--- a/internal/generate/codegen_test.go
+++ b/internal/generate/codegen_test.go
@@ -251,17 +251,6 @@ func TestGenerateProcessMadviseHandlerUsesFirstArgumentAsFd(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetProcessMadviseUnclassified locks in that process_madvise's
-// return value is UNCLASSIFIED. The man page says it returns "the number of
-// bytes advised", but that is advisory accounting, not real I/O: no bytes move
-// between buffers. Classifying it as TRANSFER/READ/WRITE would double-count it as
-// data movement, so it must stay UNCLASSIFIED like madvise(2).
-func TestClassifyRetProcessMadviseUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_process_madvise"); got != Unclassified {
- t.Errorf("process_madvise ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateRtSigpendingHandler locks in how rt_sigpending(2) is generated.
// Per the man page:
//
@@ -355,16 +344,6 @@ func TestGenerateRtTgsigqueueinfoHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetRtTgsigqueueinfoUnclassified locks in that rt_tgsigqueueinfo's
-// return value is UNCLASSIFIED. The syscall returns an int status (0 on success,
-// -1 on error) — never a byte count — so it must never be tagged as a
-// READ/WRITE/TRANSFER transfer size.
-func TestClassifyRetRtTgsigqueueinfoUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_rt_tgsigqueueinfo"); got != Unclassified {
- t.Errorf("rt_tgsigqueueinfo ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateMsgctlHandler locks in how msgctl(2) is generated. Per the man
// page:
//
@@ -432,16 +411,6 @@ func TestGenerateMsgctlHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetMsgctlUnclassified locks in that msgctl's return value is
-// UNCLASSIFIED. msgctl returns an int status (0, or a non-negative value for the
-// IPC_INFO/MSG_INFO/MSG_STAT info ops, and -1 on error) — never a byte count —
-// so it must never be tagged as a READ/WRITE/TRANSFER transfer size.
-func TestClassifyRetMsgctlUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_msgctl"); got != Unclassified {
- t.Errorf("msgctl ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateSemctlHandler locks in how semctl(2) is generated. Per the man
// page:
//
@@ -513,17 +482,6 @@ func TestGenerateSemctlHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetSemctlUnclassified locks in that semctl's return value is
-// UNCLASSIFIED. semctl returns an int status (0, or a non-negative value for the
-// GETVAL/GETPID/GETNCNT/GETZCNT/IPC_INFO/SEM_INFO/SEM_STAT info ops, and -1 on
-// error) — never a byte count — so it must never be tagged as a
-// READ/WRITE/TRANSFER transfer size.
-func TestClassifyRetSemctlUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_semctl"); got != Unclassified {
- t.Errorf("semctl ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateClone3Handler locks in how clone3(2) is generated. Per the man
// page:
//
@@ -718,16 +676,6 @@ func TestGenerateSigaltstackHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetSigaltstackUnclassified locks in that sigaltstack's return value
-// is UNCLASSIFIED. It returns 0 on success or -1 on error — a status code, not a
-// number of bytes transferred — so classifying it as READ/WRITE/TRANSFER would
-// wrongly count it as data movement.
-func TestClassifyRetSigaltstackUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_sigaltstack"); got != Unclassified {
- t.Errorf("sigaltstack ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateTkillHandler locks in how tkill(2) is generated. Per the man page:
//
// int tkill(pid_t tid, int sig)
@@ -778,33 +726,6 @@ func TestGenerateTkillHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyTkillFallsThroughToNull pins the classifier behaviour that makes
-// tkill safe: ClassifyFormat itself returns KindNone (no field matches an
-// fd/path/name pattern — crucially the pid_t tid field is NOT treated as an fd),
-// and only the generation-time fallback turns that into KindNull. If a future
-// change made the tid match the fd rule, this test would flip to KindFd and fail.
-func TestClassifyTkillFallsThroughToNull(t *testing.T) {
- f := mustParseOne(t, strings.Replace(
- strings.Replace(FormatKill, "sys_enter_kill", "sys_enter_tkill", 1),
- "ID: 183", "ID: 177", 1))
- if r := ClassifyFormat(&f); r.Kind != KindNone {
- t.Errorf("tkill ClassifyFormat = %d, want KindNone (tid must not match the fd rule)", r.Kind)
- }
- if r := classifyEnterForGeneration(&f); r.Kind != KindNull {
- t.Errorf("tkill classifyEnterForGeneration = %d, want KindNull", r.Kind)
- }
-}
-
-// TestClassifyRetTkillUnclassified locks in that tkill's return value is
-// UNCLASSIFIED. It returns 0 on success or -1 on error — a status code, not a
-// number of bytes transferred — so classifying it as READ/WRITE/TRANSFER would
-// wrongly count it as data movement.
-func TestClassifyRetTkillUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_tkill"); got != Unclassified {
- t.Errorf("tkill ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateSysinfoHandler locks in how sysinfo(2) is generated. Per the man
// page:
//
@@ -850,26 +771,6 @@ func TestGenerateSysinfoHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetSysinfoUnclassified locks in that sysinfo's return value is
-// UNCLASSIFIED. sysinfo(2) returns 0 on success or -1 on error — a status code,
-// not a number of bytes transferred — so classifying it as READ/WRITE/TRANSFER
-// would wrongly count it as data movement.
-func TestClassifyRetSysinfoUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_sysinfo"); got != Unclassified {
- t.Errorf("sysinfo ret classification = %q, want %q", got, Unclassified)
- }
-}
-
-// TestClassifyRetRtSigpendingUnclassified locks in that rt_sigpending's return
-// value is UNCLASSIFIED. It returns 0 on success or -1 on error — a status code,
-// not a number of bytes transferred — so classifying it as READ/WRITE/TRANSFER
-// would wrongly count it as data movement.
-func TestClassifyRetRtSigpendingUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_rt_sigpending"); got != Unclassified {
- t.Errorf("rt_sigpending ret classification = %q, want %q", got, Unclassified)
- }
-}
-
func TestGenerateLandlockAddRuleHandlerUsesFirstArgumentAsFd(t *testing.T) {
output := GenerateTracepointsC(mustParseAll(t, syntheticPair("landlock_add_rule")))
@@ -946,31 +847,6 @@ func TestGenerateMkdirHandlerCapturesPathFromArgs0(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestMkdiratFamilyAndKindMatchSiblings locks in that mkdirat and its siblings
-// mkdir/mknodat share the same FS family and pathname kind classification. A
-// drift here (e.g. mkdirat slipping into Misc) would split related directory/
-// node-creation syscalls across families in the dashboard.
-func TestMkdiratFamilyAndKindMatchSiblings(t *testing.T) {
- for _, syscall := range []string{"mkdirat", "mkdir", "mknodat"} {
- if got := ClassifySyscallFamily("sys_enter_" + syscall); got != FamilyFS {
- t.Errorf("%s family = %q, want %q", syscall, got, FamilyFS)
- }
- }
-
- mkdirat := mustParseOne(t, FormatMkdirat)
- if r := ClassifyFormat(&mkdirat); r.Kind != KindPathname || r.PathnameField != "pathname" {
- t.Errorf("mkdirat classified as kind=%d field=%q, want KindPathname/pathname", r.Kind, r.PathnameField)
- }
- if n := mkdirat.FieldNumber("pathname"); n != 1 {
- t.Errorf("mkdirat FieldNumber(pathname) = %d, want 1", n)
- }
-
- mkdir := mustParseOne(t, FormatMkdir)
- if n := mkdir.FieldNumber("pathname"); n != 0 {
- t.Errorf("mkdir FieldNumber(pathname) = %d, want 0", n)
- }
-}
-
// 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),
@@ -996,28 +872,6 @@ func TestGenerateRmdirHandlerCapturesPathFromArgs0(t *testing.T) {
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)
@@ -1231,16 +1085,6 @@ func TestGenerateSyncHandler(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetSyncUnclassified locks in that the bare sync(2) return value is
-// UNCLASSIFIED. sync returns void; the sys_exit_sync tracepoint still carries a
-// ret field, but it is meaningless and certainly not a byte count, so it must
-// stay UNCLASSIFIED — never READ/WRITE/TRANSFER.
-func TestClassifyRetSyncUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_sync"); got != Unclassified {
- t.Errorf("sync ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestSyncIsNotNoreturn locks in that bare sync(2) is NOT treated as a noreturn
// syscall: it is void but returns control to userspace, so its exit handler must
// be generated (see TestGenerateSyncHandler). Only exit(2)/exit_group(2)/
@@ -1430,16 +1274,6 @@ func TestGenerateNullHandlerMlockall(t *testing.T) {
requireContains(t, output, "ev->ret_type = UNCLASSIFIED;")
}
-// TestClassifyRetMlockallUnclassified locks in that mlockall's return value is
-// UNCLASSIFIED. mlockall(2) returns 0 on success or -1 on error — a status code,
-// not a number of bytes transferred — so classifying it as READ/WRITE/TRANSFER
-// would wrongly count it as data movement.
-func TestClassifyRetMlockallUnclassified(t *testing.T) {
- if got := ClassifyRet("sys_exit_mlockall"); got != Unclassified {
- t.Errorf("mlockall ret classification = %q, want %q", got, Unclassified)
- }
-}
-
// TestGenerateMemHandlerRemapFilePages locks in the BPF handler wiring for the
// (deprecated) remap_file_pages(2):
// int remap_file_pages(void *addr, size_t size, int prot, size_t pgoff, int flags).
diff --git a/internal/generate/family_test.go b/internal/generate/family_test.go
deleted file mode 100644
index c350977..0000000
--- a/internal/generate/family_test.go
+++ /dev/null
@@ -1,365 +0,0 @@
-package generate
-
-import "testing"
-
-func TestClassifySyscallFamily(t *testing.T) {
- tests := []struct {
- name string
- want SyscallFamily
- }{
- {"sys_enter_accept", FamilyNetwork},
- {"sys_exit_accept", FamilyNetwork},
- // bind(2) assigns an address to a socket; it is a socket-setup syscall and
- // shares FamilyNetwork with its connect/listen/accept/getsockname/
- // getpeername siblings. Assert both enter and exit (and the closest
- // siblings) so a stray reclassification of any one trips this test. Keep in
- // sync with the Network list in docs/syscall-tracing-plan.md.
- {"sys_enter_bind", FamilyNetwork},
- {"sys_exit_bind", FamilyNetwork},
- {"sys_enter_connect", FamilyNetwork},
- {"sys_enter_listen", FamilyNetwork},
- {"sys_enter_getsockname", FamilyNetwork},
- {"sys_enter_getpeername", FamilyNetwork},
- // setsockopt(2)/getsockopt(2) set and read socket options on the socket
- // referred to by sockfd (args[0], KindFd). They are socket-configuration
- // syscalls and share FamilyNetwork with the bind/connect/getsockname/
- // getpeername siblings above. Assert both enter and exit for the
- // setsockopt pair so a stray reclassification of either direction trips
- // this test; keep in sync with the Network list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_setsockopt", FamilyNetwork},
- {"sys_exit_setsockopt", FamilyNetwork},
- {"sys_enter_getsockopt", FamilyNetwork},
- {"sys_enter_pipe2", FamilyIPC},
- {"sys_enter_munmap", FamilyMemory},
- // process_madvise(2) gives memory advice (MADV_COLD/PAGEOUT/...) about
- // address ranges of another process selected by a pidfd. Although its
- // first arg is a pidfd (KindFd) rather than an address, the operation is
- // fundamentally a memory-advice call, so it shares FamilyMemory with its
- // madvise(2)/process_mrelease(2)/process_vm_readv/writev(2) siblings — not
- // FamilyIPC (where the pidfd_* lifecycle syscalls live).
- {"sys_enter_process_madvise", FamilyMemory},
- {"sys_exit_process_madvise", FamilyMemory},
- // set_mempolicy_home_node(2) (Linux 5.17+) sets the home NUMA node for a
- // memory range (start,len,home_node,flags); it returns 0/-1 with no byte
- // count, so it is KindNull and Unclassified. It is a NUMA memory-policy
- // syscall and shares FamilyMemory with its siblings set_mempolicy(2),
- // get_mempolicy(2), mbind(2), migrate_pages(2), and move_pages(2). Pin the
- // whole NUMA memory-policy cluster (enter+exit) so a stray reclassification
- // of any one syscall trips this test. In particular get_mempolicy(2)
- // retrieves the NUMA policy of a thread/address (not a security operation)
- // and was previously misclassified FamilySecurity; assert it here so the
- // group stays consistent. Keep in sync with the Memory list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_set_mempolicy_home_node", FamilyMemory},
- {"sys_exit_set_mempolicy_home_node", FamilyMemory},
- {"sys_enter_set_mempolicy", FamilyMemory},
- {"sys_exit_set_mempolicy", FamilyMemory},
- {"sys_enter_get_mempolicy", FamilyMemory},
- {"sys_exit_get_mempolicy", FamilyMemory},
- {"sys_enter_mbind", FamilyMemory},
- {"sys_enter_migrate_pages", FamilyMemory},
- {"sys_enter_move_pages", FamilyMemory},
- {"sys_enter_execve", FamilyProcess},
- // setsid(2) creates a new session and returns the new session ID
- // (a pid_t), or -1 on error; it takes no arguments. It is a
- // process/session-management syscall and shares FamilyProcess with its
- // session/process-group siblings getsid(2), setpgid(2), getpgid(2), and
- // getpgrp(2), as well as the pid-returning getpid(2)/getppid(2). Assert
- // the whole session/pgrp cluster so a stray reclassification of any one
- // trips this test. Keep in sync with the Process list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_setsid", FamilyProcess},
- {"sys_exit_setsid", FamilyProcess},
- {"sys_enter_getsid", FamilyProcess},
- {"sys_enter_setpgid", FamilyProcess},
- {"sys_enter_getpgid", FamilyProcess},
- {"sys_enter_getpgrp", FamilyProcess},
- {"sys_enter_getpid", FamilyProcess},
- {"sys_enter_getppid", FamilyProcess},
- // gettid(2) ("pid_t gettid(void)") returns the caller's thread ID and
- // belongs with the no-arg id-returning reader cluster
- // getpid/getppid/getuid/getgid under FamilyProcess (it is a per-thread
- // identity query, not Time/Sched/Misc). Assert enter+exit so a stray
- // reclassification trips this test. Keep in sync with the Process list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_gettid", FamilyProcess},
- {"sys_exit_gettid", FamilyProcess},
- {"sys_enter_rt_sigaction", FamilySignals},
- {"sys_enter_clock_gettime", FamilyTime},
- // gettimeofday(2) gets wall-clock time via a userspace timeval/timezone
- // pointer; it is a time/clock syscall and shares FamilyTime with its
- // sibling clock_gettime/settimeofday/time syscalls.
- {"sys_enter_gettimeofday", FamilyTime},
- {"sys_exit_gettimeofday", FamilyTime},
- // times(2) stores the calling process's CPU times (struct tms *buf) and
- // returns a clock_t tick count. It is a time/clock syscall and shares
- // FamilyTime with gettimeofday/clock_gettime — NOT FamilyProcess (where
- // getrusage lives). Assert both enter and exit so a stray reclassification
- // trips this test. Keep in sync with the Time list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_times", FamilyTime},
- {"sys_exit_times", FamilyTime},
- {"sys_enter_sched_yield", FamilySched},
- {"sys_enter_openat", FamilyFS},
- // lseek(2) repositions the file offset of an open fd; it is a per-file
- // positioning syscall and shares FamilyFS with its fd-based I/O siblings
- // read/write/fsync (also FamilyFS). Assert both enter and exit so a stray
- // reclassification trips this test. Keep in sync with the FS list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_lseek", FamilyFS},
- {"sys_exit_lseek", FamilyFS},
- // access(2) checks the calling process's permissions for a file named by
- // a real filesystem path (pathname at args[0]; no dirfd). Its siblings
- // faccessat(2)/faccessat2(2) perform the same check relative to a dirfd
- // (path at args[1]). All three are filesystem-metadata syscalls and must
- // stay in FamilyFS together — assert the whole cluster so a stray
- // reclassification of any one trips this test. Keep in sync with the FS
- // list in docs/syscall-tracing-plan.md.
- {"sys_enter_access", FamilyFS},
- {"sys_exit_access", FamilyFS},
- {"sys_enter_faccessat", FamilyFS},
- {"sys_exit_faccessat", FamilyFS},
- {"sys_enter_faccessat2", FamilyFS},
- {"sys_exit_faccessat2", FamilyFS},
- // utime(2)/utimes(2) change a file's access and modification times by
- // path (filename at args[0] is a real filesystem path, captured as
- // KindPathname). They are filesystem-metadata syscalls and share
- // FamilyFS with their siblings utimensat(2) and futimesat(2); they must
- // NOT fall through to Misc. Assert all four siblings so a stray
- // reclassification of any one trips this test. Keep in sync with the FS
- // list in docs/syscall-tracing-plan.md.
- {"sys_enter_utime", FamilyFS},
- {"sys_exit_utime", FamilyFS},
- {"sys_enter_utimes", FamilyFS},
- {"sys_exit_utimes", FamilyFS},
- {"sys_enter_utimensat", FamilyFS},
- {"sys_enter_futimesat", FamilyFS},
- // The filesystem-sync family commits cached file data/metadata to disk
- // and all classify as FamilyFS: sync(2) (no args, whole system),
- // syncfs(2) (one fd, the filesystem containing that fd), and the
- // per-file fsync(2)/fdatasync(2)/sync_file_range(2). syncfs and its
- // siblings must stay together in FamilyFS — assert the whole group so a
- // stray reclassification of any one trips this test. Keep in sync with
- // the FS list in docs/syscall-tracing-plan.md.
- {"sys_enter_sync", FamilyFS},
- {"sys_exit_sync", FamilyFS},
- {"sys_enter_syncfs", FamilyFS},
- {"sys_exit_syncfs", FamilyFS},
- {"sys_enter_fsync", FamilyFS},
- {"sys_enter_fdatasync", FamilyFS},
- {"sys_enter_sync_file_range", FamilyFS},
- // fallocate(2) manipulates the allocated disk space (preallocate,
- // punch-hole, collapse, zero, insert) for the file referred to by its
- // args[0] fd; it is a per-file space-management syscall and shares
- // FamilyFS with its fd-based siblings fadvise64(2) (access-pattern
- // advice), ftruncate(2) (resize by fd), and sync_file_range(2) (flush a
- // byte range). Assert the group so a stray reclassification of any one
- // trips this test. Keep in sync with the FS list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_fallocate", FamilyFS},
- {"sys_exit_fallocate", FamilyFS},
- {"sys_enter_fadvise64", FamilyFS},
- {"sys_enter_ftruncate", FamilyFS},
- {"sys_enter_epoll_wait", FamilyPolling},
- {"sys_enter_io_uring_enter", FamilyAIO},
- {"sys_enter_bpf", FamilySecurity},
- // kexec_load and kexec_file_load are siblings on the kexec_load(2) man
- // page (both load a new kernel for later execution by reboot(2)) and
- // must share the Security family even though kexec_load takes raw user
- // pointers (KindNull) and kexec_file_load takes fds (KindFd).
- {"sys_enter_kexec_load", FamilySecurity},
- {"sys_enter_kexec_file_load", FamilySecurity},
- {"sys_exit_kexec_load", FamilySecurity},
- // Futexes are shared-memory synchronization/IPC primitives ("fast
- // user-space locking", futex(2)); the classic futex() and the Linux
- // 6.7+ split syscalls all classify as IPC alongside the System V
- // semaphores, not Misc.
- {"sys_enter_futex", FamilyIPC},
- {"sys_enter_futex_wait", FamilyIPC},
- {"sys_enter_futex_wake", FamilyIPC},
- {"sys_exit_futex_wake", FamilyIPC},
- {"sys_enter_futex_requeue", FamilyIPC},
- {"sys_enter_futex_waitv", FamilyIPC},
- // x86 I/O-port / CPU-state syscalls are not in the explicit family
- // table and intentionally fall through to Misc (ioperm/iopl/modify_ldt
- // set port-access or LDT state, not file I/O). arch_prctl/personality
- // are deliberately classified as Process (they are in the explicit family
- // table) — locked in below to guard against drift toward Misc with their
- // x86 siblings.
- {"sys_enter_ioperm", FamilyMisc},
- {"sys_enter_iopl", FamilyMisc},
- {"sys_enter_modify_ldt", FamilyMisc},
- // arch_prctl(2) sets/gets x86-64 thread state (FS/GS base, CPUID faulting).
- // It is per-thread process/architecture state, grouped with the rest of the
- // process-state cluster, NOT with the port-access/LDT siblings above.
- {"sys_enter_arch_prctl", FamilyProcess},
- {"sys_exit_arch_prctl", FamilyProcess},
- // personality(2) sets the process execution domain — also Process, never Misc.
- {"sys_enter_personality", FamilyProcess},
- {"sys_exit_personality", FamilyProcess},
- // rseq(2) registers/unregisters a per-thread restartable-sequences area
- // (a userspace struct pointer, not an fd/path). It is not in the explicit
- // family table and intentionally falls through to Misc, sharing the family
- // with its closest per-thread sibling set_robust_list/get_robust_list
- // (also Misc). set_tid_address is Process, but rseq is grouped with the
- // robust-list pair rather than the tid-address syscall; keep this in sync
- // with the Misc list in docs/syscall-tracing-plan.md.
- //
- // Boundary rule (see family.go futex block): a syscall is IPC only if it
- // PERFORMS the actual IPC/sync operation (futex wait/wake/requeue, or an
- // op on an IPC object). set_robust_list/get_robust_list merely register or
- // query the per-thread robust-futex list head pointer — "managed in user
- // space: the kernel knows only about the location of the head"
- // (get_robust_list(2)) — and never wait/wake or touch the shared futex
- // word. That is registration/bookkeeping, exactly like rseq, so they stay
- // Misc and are NOT promoted to IPC alongside futex_* (asserted as IPC just
- // above). The split axis is operation-vs-registration, not name similarity.
- {"sys_enter_rseq", FamilyMisc},
- {"sys_exit_rseq", FamilyMisc},
- {"sys_enter_set_robust_list", FamilyMisc},
- {"sys_enter_get_robust_list", FamilyMisc},
- // set_tid_address(2) is the deliberate counterpoint to the
- // rseq/robust_list cluster above: it shares the surface form
- // ("per-thread registration of a pointer the kernel consults later")
- // but stays Process, NOT Misc. WHY: the tidptr it registers is
- // clear_child_tid — the kernel's primary thread-EXIT notification
- // mechanism (zeroed + FUTEX_WAKEd at thread teardown), set by the C
- // runtime for essentially every thread (clone(2) CLONE_CHILD_CLEARTID),
- // and the call returns the caller's thread ID like gettid/getpid. It is
- // mandatory thread-lifecycle plumbing and belongs with
- // clone/fork/exit/gettid, whereas rseq (scheduling optimization) and
- // robust_list (opt-in futex cleanup) are OPTIONAL per-thread features a
- // thread runs fine without. The Process-vs-Misc axis here is
- // mandatory-lifecycle vs optional-opt-in-feature, not registration-vs-
- // operation. Assert enter+exit so a stray move to Misc trips this test.
- // See the Process-vs-Misc boundary block in family.go and keep in sync
- // with the Process list in docs/syscall-tracing-plan.md.
- {"sys_enter_set_tid_address", FamilyProcess},
- {"sys_exit_set_tid_address", FamilyProcess},
- // sysinfo(2) returns overall system statistics (memory/swap usage and
- // load averages) into a single userspace struct sysinfo *info pointer
- // (an output buffer, not an fd/path). It is not in the explicit family
- // table and intentionally falls through to Misc, sharing the family with
- // its closest system-introspection siblings newuname/sysfs (also Misc).
- // NOTE: other "system info" relatives are deliberately classified
- // elsewhere — getrusage is Process, times/gettimeofday are Time — so
- // sysinfo is grouped with the uname/sysfs cluster rather than any of
- // those. ustat(2) is NOT a sibling here: it contains the "stat" name
- // marker and is classified FamilyFS by isFSSyscall. Keep this in sync
- // with the Misc list in docs/syscall-tracing-plan.md.
- {"sys_enter_sysinfo", FamilyMisc},
- {"sys_exit_sysinfo", FamilyMisc},
- {"sys_enter_newuname", FamilyMisc},
- {"sys_enter_sysfs", FamilyMisc},
- // rt_sigpending(2) examines the set of signals pending for delivery
- // (sigset_t *set, size_t sigsetsize). It is a signal-handling syscall and
- // shares FamilySignals with the whole rt_sig* group as well as kill/pause/
- // sigaltstack/tkill/tgkill. The entire group must stay consistent; assert
- // every rt_sig* sibling alongside rt_sigpending so a stray reclassification
- // of any one of them trips this test. Keep in sync with the Signals list in
- // docs/syscall-tracing-plan.md.
- {"sys_enter_rt_sigpending", FamilySignals},
- {"sys_exit_rt_sigpending", FamilySignals},
- {"sys_enter_rt_sigprocmask", FamilySignals},
- {"sys_enter_rt_sigsuspend", FamilySignals},
- {"sys_enter_rt_sigtimedwait", FamilySignals},
- {"sys_enter_rt_sigreturn", FamilySignals},
- {"sys_enter_rt_sigqueueinfo", FamilySignals},
- {"sys_enter_rt_tgsigqueueinfo", FamilySignals},
- {"sys_enter_sigaltstack", FamilySignals},
- // tkill(tid, sig) and its successor tgkill(tgid, tid, sig) deliver a signal
- // to a specific thread; kill(pid, sig) signals a whole process. All three
- // are signal-delivery syscalls and belong in FamilySignals with the rest of
- // the group above. tkill is the obsolete predecessor of tgkill (man 2 tkill)
- // and must not drift into FamilyProcess just because its first arg is a
- // thread id — the tid is a signal target, not a process-control operand.
- // Assert both enter and exit for tkill/tgkill/kill so a stray
- // reclassification of any of them trips this test.
- {"sys_enter_kill", FamilySignals},
- {"sys_exit_kill", FamilySignals},
- {"sys_enter_tkill", FamilySignals},
- {"sys_exit_tkill", FamilySignals},
- {"sys_enter_tgkill", FamilySignals},
- {"sys_exit_tgkill", FamilySignals},
- // ioprio_get/ioprio_set query/set the I/O scheduling class and priority of
- // a process, process group, or user. They are the I/O-priority analogues of
- // getpriority/setpriority (the CPU nice value) and share the identical
- // which/who selector signature, so they classify as Process alongside them
- // rather than falling through to Misc. Assert the ioprio pair together with
- // their getpriority/setpriority siblings so a stray reclassification of any
- // one of them trips this test. Note: the x86 I/O-port syscalls ioperm/iopl
- // (asserted above as Misc) only share an "io" name prefix; they set
- // port-access state, not process I/O priority, and stay in Misc. Keep in
- // sync with the Process list in docs/syscall-tracing-plan.md.
- {"sys_enter_ioprio_get", FamilyProcess},
- {"sys_exit_ioprio_get", FamilyProcess},
- {"sys_enter_ioprio_set", FamilyProcess},
- {"sys_exit_ioprio_set", FamilyProcess},
- {"sys_enter_getpriority", FamilyProcess},
- {"sys_enter_setpriority", FamilyProcess},
- // setuid(2) sets the process credential (effective, and possibly real and
- // saved, user ID); it is a process/credential-management syscall and shares
- // FamilyProcess with its credential-setting cluster — the uid setters
- // setresuid/setreuid/setfsuid, the gid analogues
- // setgid/setresgid/setregid/setfsgid/setgroups, and the matching credential
- // readers getuid/geteuid/getgid/getegid/getresuid/getresgid/getgroups.
- // Assert the cluster (enter and exit for setuid) so a stray
- // reclassification of any one credential syscall trips this test.
- // seteuid/setegid (set effective uid/gid) belong with the cluster too,
- // but have no dedicated kernel tracepoints (they are libc wrappers over
- // setreuid/setresuid), so they never reach the generated tracepoint map
- // or docs/syscall-tracing-plan.md. They are still classified as Process
- // in family.go for consistency, so assert them here by name directly
- // (no tracepoint required) to lock in that latent classification.
- {"sys_enter_setuid", FamilyProcess},
- {"sys_exit_setuid", FamilyProcess},
- {"sys_enter_seteuid", FamilyProcess},
- {"sys_exit_seteuid", FamilyProcess},
- {"sys_enter_setegid", FamilyProcess},
- {"sys_exit_setegid", FamilyProcess},
- {"sys_enter_setresuid", FamilyProcess},
- {"sys_enter_setreuid", FamilyProcess},
- {"sys_enter_setfsuid", FamilyProcess},
- {"sys_enter_setgid", FamilyProcess},
- {"sys_enter_setresgid", FamilyProcess},
- {"sys_enter_setregid", FamilyProcess},
- {"sys_enter_setfsgid", FamilyProcess},
- {"sys_enter_setgroups", FamilyProcess},
- {"sys_enter_getuid", FamilyProcess},
- {"sys_enter_geteuid", FamilyProcess},
- {"sys_enter_getgid", FamilyProcess},
- {"sys_enter_getegid", FamilyProcess},
- {"sys_enter_getresuid", FamilyProcess},
- {"sys_enter_getresgid", FamilyProcess},
- {"sys_enter_getgroups", FamilyProcess},
- {"sys_enter_unlisted_future_syscall", FamilyMisc},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := ClassifySyscallFamily(tt.name); got != tt.want {
- t.Errorf("ClassifySyscallFamily(%q) = %s, want %s", tt.name, got, tt.want)
- }
- })
- }
-}
-
-func TestParseFormatsTagsEveryFormatWithFamily(t *testing.T) {
- formats := mustParseAll(t, FormatRead+"\n"+FormatExitSocket+"\n"+FormatExitKill)
-
- tests := []struct {
- index int
- want SyscallFamily
- }{
- {0, FamilyFS},
- {1, FamilyNetwork},
- {2, FamilySignals},
- }
-
- for _, tt := range tests {
- if got := formats[tt.index].Family; got != tt.want {
- t.Errorf("formats[%d].Family = %s, want %s", tt.index, got, tt.want)
- }
- }
-}
diff --git a/internal/generate/retclassify_test.go b/internal/generate/retclassify_test.go
deleted file mode 100644
index 4fc7501..0000000
--- a/internal/generate/retclassify_test.go
+++ /dev/null
@@ -1,185 +0,0 @@
-package generate
-
-import "testing"
-
-func TestClassifyRetRead(t *testing.T) {
- reads := []string{
- "fgetxattr", "flistxattr", "getdents", "getdents64", "getxattr",
- // getxattrat (Linux 6.13+) returns the xattr value size in bytes, the
- // same read byte-count as getxattr/lgetxattr/fgetxattr.
- "getxattrat",
- "lgetxattr", "listxattr",
- // listxattrat (Linux 6.13+) returns the size in bytes of the xattr
- // name list, the same read byte-count as listxattr/llistxattr/flistxattr.
- "listxattrat",
- "llistxattr", "pread64", "preadv",
- "preadv2", "process_vm_readv", "read", "readlink", "readlinkat",
- "readv", "recvmsg", "recvfrom", "syslog", "mq_timedreceive", "getrandom", "msgrcv",
- }
- for _, name := range reads {
- if got := ClassifyRet("sys_exit_" + name); got != ReadClassified {
- t.Errorf("ClassifyRet(sys_exit_%s) = %q, want READ_CLASSIFIED", name, got)
- }
- }
-}
-
-func TestClassifyRetWrite(t *testing.T) {
- writes := []string{
- "process_vm_writev", "pwrite64", "pwritev", "pwritev2",
- "sendmsg", "sendto", "write", "writev", "mq_timedsend", "msgsnd",
- }
- for _, name := range writes {
- if got := ClassifyRet("sys_exit_" + name); got != WriteClassified {
- t.Errorf("ClassifyRet(sys_exit_%s) = %q, want WRITE_CLASSIFIED", name, got)
- }
- }
-}
-
-func TestClassifyRetTransfer(t *testing.T) {
- transfers := []string{
- "copy_file_range", "sendfile64", "splice", "tee", "vmsplice",
- }
- for _, name := range transfers {
- if got := ClassifyRet("sys_exit_" + name); got != TransferClassified {
- t.Errorf("ClassifyRet(sys_exit_%s) = %q, want TRANSFER_CLASSIFIED", name, got)
- }
- }
-}
-
-func TestClassifyRetUnclassified(t *testing.T) {
- unclassified := []string{
- "openat", "close", "rename", "unlink", "fcntl", "dup", "dup2", "dup3",
- "mkdir", "rmdir", "chmod", "chown", "chdir", "stat",
- "truncate", "fallocate", "mmap", "fsync", "flock", "recvmmsg", "sendmmsg",
- // lseek(2) repositions the file offset of args[0]'s fd and returns the
- // RESULTING file OFFSET (off_t, bytes from the start of the file) on
- // success, or -1 on error. That return is a file POSITION, NOT a count of
- // bytes transferred — so its exit must stay UNCLASSIFIED (plain
- // ret_event). Classifying it as READ/WRITE/TRANSFER would wrongly add the
- // absolute file position into I/O byte totals and grossly inflate them
- // (e.g. an lseek to offset 1 GiB would look like a 1 GiB transfer). lseek
- // is a KindFd FamilyFS syscall like its read/write/fsync siblings; only
- // read/write actually move bytes and carry a byte-count return.
- "lseek",
- // syncfs(2) returns int 0/-1 (no byte count); it commits the filesystem
- // containing args[0]'s fd and transfers no bytes, so its exit must stay
- // UNCLASSIFIED (plain ret_event), like its fsync/fdatasync siblings.
- "syncfs",
- // gettimeofday(2) returns int 0/-1 (no byte count); its exit carries a
- // plain ret_event and must stay UNCLASSIFIED, not a read/write transfer.
- "gettimeofday",
- // times(2) returns a clock_t tick count (clock ticks since an arbitrary
- // point in the past), or (clock_t)-1 on error. That is a tick count, NOT
- // a transferred byte count, so its exit must stay UNCLASSIFIED (plain
- // ret_event), like its time sibling gettimeofday.
- "times",
- // rseq(2) returns int 0/-1 on (un)registration of the restartable-
- // sequences area; it transfers no bytes, so its exit must stay
- // UNCLASSIFIED (plain ret_event), like its KindNull siblings.
- "rseq",
- // set_mempolicy_home_node(2) sets the home NUMA node for a memory range
- // and returns int 0/-1 (no byte count), so its exit carries a plain
- // ret_event and must stay UNCLASSIFIED, like its NUMA siblings
- // set_mempolicy/mbind/migrate_pages/move_pages.
- "set_mempolicy_home_node",
- // migrate_pages(2) moves all pages of a process (selected by pid, NOT an
- // fd) between NUMA node sets; on success it returns the number of pages
- // that could NOT be moved (>=0, zero meaning all moved), or -1 on error.
- // That count is a page tally, not a transferred byte count, so its exit
- // must stay UNCLASSIFIED (plain ret_event), like its NUMA siblings
- // set_mempolicy/mbind/set_mempolicy_home_node and move_pages.
- "migrate_pages",
- // move_pages(2) is the per-page NUMA sibling of migrate_pages(2); it also
- // returns 0/-1 (with per-page status reported via a userspace array, not
- // the return value), so its exit likewise stays UNCLASSIFIED.
- "move_pages",
- // setsid(2) returns the new session ID (a pid_t) on success, or
- // (pid_t)-1 on error; that return is a session/process identifier, not a
- // transferred byte count. Its exit must stay UNCLASSIFIED (plain
- // ret_event), exactly like its pid-returning siblings getsid/getpid/
- // getppid (asserted below), so it is never mistaken for a read/write
- // byte transfer.
- "setsid",
- "getsid",
- // setpgid(2) sets the process group ID of a process and returns int
- // 0 on success or -1 on error — a status code, not a transferred byte
- // count. Its exit must stay UNCLASSIFIED (plain ret_event), exactly
- // like its session/process-group siblings setsid/getsid above and the
- // pid-returning getpid/getppid below.
- "setpgid",
- "getpid",
- "getppid",
- // set_tid_address(2) sets the calling thread's clear_child_tid pointer
- // and ALWAYS returns the caller's thread ID — it never fails and never
- // returns -1 (set_tid_address(2): "always succeeds"). That return is a
- // thread identifier (a pid_t/tid), NOT a transferred byte count, so its
- // exit must stay UNCLASSIFIED (plain ret_event), exactly like its
- // pid/tid-returning Process siblings setsid/getsid/getpid/getppid above.
- "set_tid_address",
- // bind(2) assigns an address to a socket and returns int 0 on success or
- // -1 on error — a status code, NOT a transferred byte count. Its exit must
- // stay UNCLASSIFIED (plain ret_event), exactly like its socket-setup
- // siblings connect/listen/getsockname/getpeername (asserted alongside it),
- // so it is never mistaken for a recvfrom/sendto-style byte transfer.
- "bind",
- "connect",
- "listen",
- "getsockname",
- "getpeername",
- // kexec_load(2) loads a new kernel for later execution by reboot(2) and
- // returns long 0 on success or -1 on error — a status code, NOT a
- // transferred byte count. Its exit must stay UNCLASSIFIED (plain
- // ret_event), exactly like its sibling kexec_file_load and the
- // system/admin syscall reboot below.
- "kexec_load",
- "kexec_file_load",
- "reboot",
- }
- for _, name := range unclassified {
- if got := ClassifyRet("sys_exit_" + name); got != Unclassified {
- t.Errorf("ClassifyRet(sys_exit_%s) = %q, want UNCLASSIFIED", name, got)
- }
- }
-}
-
-func TestBatchMessageSyscallsDeferredFromRetByteClassification(t *testing.T) {
- tests := []string{"recvmmsg", "sendmmsg"}
- for _, name := range tests {
- t.Run(name, func(t *testing.T) {
- if got := ClassifyRet("sys_exit_" + name); got != Unclassified {
- t.Fatalf("ClassifyRet(sys_exit_%s) = %q, want %q", name, got, Unclassified)
- }
- })
- }
-}
-
-func TestClassifyRetCaseInsensitive(t *testing.T) {
- if got := ClassifyRet("sys_exit_READ"); got != ReadClassified {
- t.Errorf("ClassifyRet(sys_exit_READ) = %q, want READ_CLASSIFIED", got)
- }
-}
-
-func TestPhaseAByteClassifiedSyscallsUseExistingRetClassifications(t *testing.T) {
- tests := []struct {
- name string
- want RetClassification
- }{
- {"recvfrom", ReadClassified},
- {"recvmsg", ReadClassified},
- {"sendto", WriteClassified},
- {"sendmsg", WriteClassified},
- {"sendfile64", TransferClassified},
- {"splice", TransferClassified},
- {"tee", TransferClassified},
- {"process_vm_readv", ReadClassified},
- {"process_vm_writev", WriteClassified},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := ClassifyRet("sys_exit_" + tt.name); got != tt.want {
- t.Fatalf("ClassifyRet(sys_exit_%s) = %q, want %q", tt.name, got, tt.want)
- }
- })
- }
-}