summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
5 daysbuild: harden Magefile.go to use sudo -n for discrete commands onlydevelopPaul Buetow
Implement the sudo hardening plan so mage itself never runs as root. Changes: - Remove implicit sudo wrapping for go test (buildGoTestCmd, progress ticker, drainTestEvents helpers removed). - Add compileIntegrationTestBinary() + runIntegrationTestBinary() helpers. The integration test binary is compiled unprivileged, then executed under sudo -n -E from the integrationtests/ directory so relative paths (../ior, ../ioworkload) resolve correctly. - Harden sudoOutput() to prefix with sudo -n. - Harden sudoRunWithEnv() to prefix with sudo -n env ... - Update docs/sudo-hardening-plan.md and docs/sudo-rules-for-ior.txt to document the working approach (sudo -n -E with SETENV flag).
6 daysdocs(follow-forks): add process-tree-following plan + filter.c referencePaul Buetow
Document the planned opt-in "follow forks" mode that would let ior trace a target PID and all its descendants (needed for the landlock_restrict_self integration case, task ci0, and for tracing forking workloads as a tree). The plan covers the BPF descendant-set map, sched_process_fork/exit hooks, the FOLLOW_FORK gate in filter(), userland flag/seeding/assertion changes, and explicitly requires syscall-count aggregation to roll up across the followed tree. Add a reference comment above filter() pointing to the plan. Plan only — not implemented. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daystest(memory): add coverage for mlock/mlock2/munlock/mlockall/munlockallPaul Buetow
The memory-locking cluster (FamilyMemory) had no end-to-end integration coverage; mmap_test.go only exercised mmap/msync/mremap/munmap. These syscalls return UNCLASSIFIED, so enter-tracepoint presence is the right end-to-end check (mlock/mlock2/munlock are KindMem, mlockall/munlockall are KindNull). Add a mmap-memory-lock scenario that, on a single anonymous page (small enough to stay under RLIMIT_MEMLOCK), issues raw syscalls for mlock, munlock, mlock2(addr,len,0), mlockall(MCL_CURRENT) and munlockall so the exact sys_enter_ tracepoints fire. mlock/mlock2/mlockall tolerate EPERM/ENOMEM best-effort since the enter tracepoint fires before the error; munlock/munlockall always succeed and clean up. TestMmapMemoryLock asserts enter_mlock, enter_munlock, enter_mlock2, enter_mlockall and enter_munlockall each fire (MinCount>=1, Comm ioworkload). All five captured under sudo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daystest: add mount_setattr coverage and assert vmsplice TRANSFER byte countPaul Buetow
mount_setattr(2) was the only new-mount-API sibling not exercised end-to-end (unlike move_mount/fsmount/fspick/open_tree), and vmsplice(2) had zero end-to-end assertion despite being TRANSFER_CLASSIFIED. uj0 (mount_setattr): add a best-effort RawSyscall6(SYS_MOUNT_SETATTR) call to mountfsManagement() aimed at the scenario mount point with AT_FDCWD and a MountAttr requesting MOUNT_ATTR_RDONLY. It needs CAP_SYS_ADMIN (Linux 5.12+) and the path is not a mount, so it returns EPERM/EINVAL, but its sys_enter_ tracepoint fires on kernel entry before any check -- the same best-effort pattern used for the other mount-API calls. Add mount_setattr to mountfsTraceArgs and assert enter_mount_setattr MinCount>=1 in TestMountFsManagementSyscalls. bl0 (vmsplice): add a deterministic retbytesVmsplice driver to the phase-A workload (mirroring the getdents64/readlinkat drivers): a spaced retry loop that gathers a fixed 18-byte user iovec into a fresh pipe via vmsplice and drains it each iteration. vmsplice is TRANSFER_CLASSIFIED, so the exit reports ctx->ret = bytes moved. Add vmsplice to retbytesTraceArgs and assert enter_vmsplice presence plus assertEventBytesAtLeast(payloadLen=18) and a positive duration in TestRetbytesPhaseA, locking in the TRANSFER byte attribution like its splice/tee siblings. Coverage hardening only; classification/tracing verified correct by inspection (mount_setattr=FamilyFS/KindPathname/UNCLASSIFIED, vmsplice=FamilyNetwork/KindFd/TRANSFER_CLASSIFIED). Verified: TEST_NAME=TestMountFsManagementSyscalls mage testWithName PASS (enter_mount_setattr captured); TEST_NAME=TestRetbytesPhaseA mage testWithName PASS (vmsplice bytes>=18 asserted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daysfeat(parquet): export rename/link oldname + assert oldname capture end-to-endPaul Buetow
rename-family (rename/renameat/renameat2) and link-family (link/linkat/ symlink/symlinkat) capture BOTH paths in BPF (name_event.oldname at args[1] for the AT-variants, after a dirfd, plus newname), but only newname reached any persisted output. event.Pair.FileName() resolves to oldnameNewnameFile.Name() == Newname, so the parquet `file` column, CSV, and flamegraph Path all carried newname; the captured oldname survived only in the TUI stream String() repr ("old:... ->new:..."). The oldname capture (and its args[1] index for the AT-variants) was therefore never validated end-to-end, and a wrong-oldname-index regression would surface in no persisted output or test. Surface oldname as one additive, backward-compatible column, mirroring the dedicated optional-column convention (address_space_bytes, requested_sleep_ns, epoll_*): - old_file (String): source/old path for rename/link syscalls; empty for every other syscall. The existing `file` column keeps its semantics (newname for rename/link). Data flows name_event.oldname -> event.Pair.Oldname (populated in handleNameExit alongside the existing File) -> streamrow.Row.OldName -> parquet.Record.OldFile. Docs (docs/parquet-querying.md) and the Magefile parquetValidate column list updated in lockstep. The new TestRenameRenameatOldnameInParquet integration test exercises the renameat AT-variant (oldname at args[1] after the olddfd dirfd) and asserts the parquet old_file column carries renameat-old.txt while file carries renameat-new.txt, and that old_file stays empty for non-rename/ link rows -- locking in the args[1] oldname capture end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daysfeat(parquet): surface epoll_ctl op/target-fd/events metadataPaul Buetow
epoll_ctl's BPF handler already decodes the operation (args[1]), target descriptor (args[2]), and requested event mask (args[3]->events) into an EpollCtlEvent, but the single resolved-epfd `fd` column was the only epoll detail reaching the output schema. Consumers could not see which descriptor was registered nor the operation performed. Surface the metadata as three additive, backward-compatible columns, mirroring the existing dedicated optional-column convention used by requested_sleep_ns and address_space_bytes: - epoll_op (String): ADD/MOD/DEL, or the raw decimal for unknown ops; empty for non-epoll_ctl rows. - epoll_target_fd (Int32): registered descriptor (args[2]); 0 otherwise. - epoll_events (UInt32): requested event mask; 0 otherwise. Data flows EpollCtlEvent -> event.Pair (new EpollCtl/HasEpoll fields, populated in handleEpollCtlExit) -> streamrow.Row -> parquet.Record. The op-to-string mapping lives on event.EpollCtl.OpName. Docs (docs/parquet-querying.md) and the Magefile parquetValidate column list updated in lockstep (also adding the previously-undocumented address_space_bytes/requested_sleep_ns columns). The polling parquet integration test now asserts epoll_ctl rows carry a decoded op and a valid target fd, and that other syscalls leave epoll_op empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daystest: add deterministic ioctl coverage and quotactl_fd in mountfsPaul Buetow
ioctl (FamilyFS, KindFd@arg0) previously only fired implicitly via the Go runtime/terminal. Add scenario_ioctl.go issuing a benign FIONREAD ioctl on an opened temp file (registered as ioctl-basic) and ioctl_test.go asserting enter_ioctl resolves to the temp file path, mirroring the fcntl suite. quotactl_fd (FamilyFS, KindFd@arg0) had no coverage while its sibling quotactl was tested in mountfs. Add a best-effort RawSyscall6 SYS_QUOTACTL_FD call on an fd opened on the mount point in scenario_mountfs.go, extend mountfsTraceArgs, and assert enter_quotactl_fd (MinCount>=1). The sys_enter tracepoint fires on kernel entry regardless of privilege/quota support. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6 daystest: add coverage for pidfd_send_signal and fadvise64Paul Buetow
pidfd_send_signal (FamilyIPC, KindFd@arg0) and fadvise64 (KindFd, UNCLASSIFIED fd-based hint) previously had no end-to-end integration coverage despite correct classification/tracing. pidfd_send_signal: add a pidfd-send-signal ioworkload scenario that opens a pidfd for the current process and issues a sig-0 liveness probe (delivers nothing, safe to target self) via syscall.Syscall6 with the per-arch nr 424. TestPidfdSendSignal asserts enter_pidfd_send_signal is captured; pidfd_send_signal added to the pidfd -trace-syscalls list. fadvise64: add readwrite-fadvise64 and readwrite-fadvise64-ebadf scenarios using unix.Fadvise(fd, 0, 0, FADV_NORMAL), mirroring the readahead tests. TestReadwriteFadvise64 asserts enter_fadvise64 with Bytes==0 (UNCLASSIFIED: offset/len are hints, not bytes transferred) and positive duration; the ebadf variant asserts enter capture with Bytes==0 on the failing call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest: add coverage for mknodat and ioprio_get/ioprio_setPaul Buetow
Add end-to-end integration scenarios and tests for two previously untested syscalls: - mknodat(2): new dir-mknodat-fifo scenario creates an unprivileged FIFO node (S_IFIFO, no CAP_MKNOD) via unix.Mknodat under AT_FDCWD and unlinks it. TestDirMknodatFifo asserts enter_mknodat fires with pathname@args[1] (after dirfd@args[0]), proven by a PathContains match on the distinct fifo name, mirroring the mkdirat coverage. - ioprio_get(2)/ioprio_set(2): new ioprio-basic scenario (the I/O-priority analogues of getpriority/setpriority) issues the raw syscalls (no x/sys wrapper exists), reading the current self I/O priority and re-applying it, or a harmless unprivileged best-effort value when none is set. TestIoprioBasic asserts enter_ioprio_get and enter_ioprio_set fire (null enters, UNCLASSIFIED ret), mirroring priority-basic. Realtime class is deliberately avoided as it needs CAP_SYS_ADMIN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest: add coverage for setitimer (signal-safe) and statfs/fstatfsPaul Buetow
setitimer/getitimer (di0): no scenario previously exercised the classic interval-timer family. Add intervalTimerNoop, which calls setitimer(ITIMER_REAL, &{0,0,0,0}, NULL) with an all-zero itimerval so the timer is disarmed and NO SIGALRM is ever scheduled (mirrors miscAlarmCancel's alarm(0) and posixTimerLifecycle's never-firing pattern), followed by a safe getitimer read. Both are KindNull on enter / UNCLASSIFIED on exit, so TestIntervalTimerNoop asserts enter_setitimer and enter_getitimer presence. statfs/fstatfs (7j0): stat_test.go covered stat/fstat/lstat/newfstatat/statx but not the statfs family. Add statStatfs, which calls syscall.Statfs(path) (enter_statfs path_event captures the pathname) and syscall.Fstatfs(fd) (enter_fstatfs fd_event). TestStatStatfs asserts enter_statfs PathContains the filename and enter_fstatfs presence. Covers audits it (fstatfs) and e00 (statfs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest: add end-to-end coverage for getrandom (READ byte count) and flock (KindFd)Paul Buetow
Two previously-untested syscalls now have integration coverage: - getrandom (Security family, READ_CLASSIFIED): new security-getrandom scenario fills a 32-byte buffer via unix.Getrandom, looping past any signal-interrupted short reads so the cumulative byte count is strictly positive. TestSecurityGetrandom asserts enter_getrandom MinCount>=1, bytes>=1 (locking in the READ byte-count classification end-to-end), and a positive duration. - flock (FamilyFS, KindFd@args[0], UNCLASSIFIED): new flock-basic scenario opens a temp file, takes LOCK_EX then LOCK_UN via syscall.Flock, and closes it. TestFlockBasic asserts enter_flock with PathContains the temp filename, confirming the fd resolves to the file path via the procfd cache. Both scenarios use raw unix/syscall calls so the exact sys_enter tracepoints fire, and are registered in cmd/ioworkload/scenarios.go. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(aio): add io_getevents and io_cancel enter coveragePaul Buetow
Close the integration-test gaps for two classic-AIO syscalls that the existing scenario never exercised. The AIO workload only drove io_setup/io_submit/io_destroy, so io_getevents (nr 208) and io_cancel (nr 210) had no end-to-end coverage despite their tracer classification (FamilyAIO, KindNull enter, UNCLASSIFIED ret) being correct by inspection. cmd/ioworkload/scenario_aio.go: - Factor the submit scaffolding (temp dir, target file, AIO context) into withAioTarget so each scenario stays short. - ioSubmitWrite now returns the submitted iocb pointer (io_cancel needs it). - aio-getevents: submit, then reap the completion with a blocking io_getevents (min_nr=1, NULL timeout); asserts the return is a count. - aio-cancel: submit, then best-effort io_cancel (return ignored: it races the I/O completion and often yields -EINVAL/-EAGAIN, but enter still fires), then drain the ring non-blockingly (min_nr=0) so io_destroy has nothing in flight and we never hang when the cancel left no completion. integrationtests/aio_test.go: - TestAioGetevents asserts enter_io_getevents (MinCount 1), mirroring TestAioSubmit, with io_getevents added to the trace-arg set. - TestAioCancel asserts ONLY enter_io_cancel (MinCount 1) — never success — because io_cancel's return is non-deterministic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest: close issued-but-unasserted gaps (signals, sched, sysv msgrcv) + fix ↵Paul Buetow
pidfd_getfd path expectation signals: TestSignalsBasic issued but never asserted enter_tkill and enter_rt_sigqueueinfo (both genuinely emitted by sendSelfSignals); add both MinCount>=1 enter assertions. Both tracepoints were already in signalsTraceArgs. sched: TestSchedBasic omitted enter_sched_setaffinity even though scenario_sched.go re-applies its affinity mask via SchedSetaffinity and the tracepoint is in schedTraceArgs; add the MinCount>=1 enter assertion. sysv: TestSysVMsgBasic only checked enter presence + duration for msgrcv. msgrcv is READ_CLASSIFIED, so add assertEventBytesAtLeast >= 12 ("ior-sysv-msg") mirroring the recvfrom/recvmsg byte assertions. pidfd: verified TestPidfdGetfdSuccess already PASSES deterministically with PathContains "pidfd-getfd-source.txt". The path is correct and meaningful: pidfd_getfd is an fd-transfer op, so at exit applyFdTransferOp re-points the event to the RETURNED dup fd, which for a self-pidfd refers to the same open source file. Kept the assertion and added a comment explaining why it resolves to the source file rather than the pidfd's anon_inode. No BPF/handler change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(landlock): cover landlock_add_rule end-to-end (nj0)Paul Buetow
Extend the existing security-landlock scenario to also exercise landlock_add_rule in-process. After creating the ruleset fd, the scenario builds a struct landlock_path_beneath_attr (allowed_access = LANDLOCK_ACCESS_FS_READ_FILE, parent_fd = open("/", O_PATH)) and calls landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0) (syscall nr 445), then closes both fds. landlock_add_rule is unprivileged and has no process-wide side effects (it only builds a ruleset that is never enforced), so it is safe to run in the shared workload process. The call is issued unconditionally even when ruleset creation fails: sys_enter_landlock_add_rule fires before the kernel validates the fd, so the enter tracepoint is captured regardless of whether Landlock is enabled, matching create_ruleset coverage. ret is UNCLASSIFIED (0/-1, not a byte count). TestSecurityLandlockCreateRuleset now adds landlock_add_rule to the trace-arg set and asserts enter_landlock_add_rule MinCount>=1 plus a positive event duration, capturing ruleset_fd at args[0] (KindFd). landlock_restrict_self (ci0) is intentionally NOT covered here: it would require a traced child subprocess, which the integration harness cannot support (it filters by the single workload PID at the BPF layer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(utime): add end-to-end coverage for futimesat and utimensatPaul Buetow
utime_test.go previously covered only utime/utimes/ENOENT. Add scenarios and tests for the dirfd-relative siblings futimesat(2) and utimensat(2), which take a dirfd at args[0] and the pathname at args[1]. Both scenarios use raw syscalls with AT_FDCWD as the dirfd so the exact enter_futimesat and enter_utimensat tracepoints fire, and the tests assert PathContains the filename, proving ior captures the path from args[1] (after the dirfd). Classification/tracing were already verified by audits qt/f10; this is pure coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(xattr): cover get/list/lset/setxattrat/removexattr + fd-based xattr ↵Paul Buetow
variants Extend the xattr scenario + integration test to exercise end-to-end the xattr syscall variants that previously had no coverage (only setxattr and the -at get/list/remove variants were tested). Classifications were already verified correct by inspection; this is pure coverage hardening. READ-classified variants (assert enter path/fd + bytes>=1): - getxattr(path,name,...) READ, KindPathname@arg0 (ej0) - lgetxattr(path,name,...) READ, KindPathname@arg0 (oj0) - listxattr(path,...) READ, KindPathname@arg0 (rj0) - llistxattr(path,...) READ, KindPathname@arg0 (rj0) - fgetxattr(fd,...) READ, KindFd@arg0 (8i0) - flistxattr(fd,...) READ, KindFd@arg0 (8i0) UNCLASSIFIED variants (assert enter path/fd + bytes==0): - lsetxattr(path,...) KindPathname@arg0 (cl0) - setxattrat(dirfd,path,...) KindPathname@arg1 (vj0) - removexattr(path,name) KindPathname@arg0 (kj0) - lremovexattr(path,name) KindPathname@arg0 (kj0) - fsetxattr(fd,...) KindFd@arg0 (8i0) - fremovexattr(fd,...) KindFd@arg0 (8i0) The l* GET/LIST/SET/REMOVE variants target a regular file (not a bare symlink) so they fire deterministically: user.* xattrs on a symlink itself are kernel-restricted (EPERM). setxattrat uses the raw syscall (nr 463, Linux 6.13+) since Go does not export it; this kernel (7.0.9) supports it. New scenarios use golang.org/x/sys/unix (raw syscalls, no glibc redirect) so the exact tracepoints fire. New tests scope -trace-syscalls to exactly the variant under test to avoid substring-match cross-contamination. All 13 TestXattr* integration tests PASS under root (mage testWithName). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(priority): add end-to-end coverage for getpriority/setpriorityPaul Buetow
getpriority/setpriority (FamilyProcess, KindNull enter, UNCLASSIFIED ret) were untested end-to-end: no ioworkload scenario invoked them. Add a safe, unprivileged, non-disruptive priority-basic scenario that reads the current nice value via getpriority(PRIO_PROCESS, 0) and re-applies that exact value via setpriority(PRIO_PROCESS, 0, currentNice) — a byte-for-byte no-op, mirroring the existing schedRoundtripAffinity round-trip. Note getpriority returns 20-nice, so the value is converted back before re-applying. Register the scenario in scenarios.go and add TestPriorityBasic, which asserts enter_getpriority and enter_setpriority appear as null_event enters attributed to the ioworkload process. Enter-tracepoint presence is the right check given KindNull/UNCLASSIFIED (no fd/path/bytes to assert). Coverage only — classification verified correct in audits 6u and pz. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(timerfd): assert timerfd_settime/gettime fd capture end-to-endPaul Buetow
The fd-from-air-eventfd-users workload only called timerfd_create then closed the fd, so timerfd_settime/timerfd_gettime were never exercised by any integration test. Commit 6ac9fa4 fixed those two syscalls to KindFd@arg0 (capturing the operating timerfd via fd_event instead of a null_event), but that fix had no end-to-end coverage. Extend the workload to arm the still-open timerfd via timerfd_settime (1s relative expiry, so it never fires) and read it back via timerfd_gettime before closing. Assert in TestFdFromAirEventfdUsers that both enter handlers fire (MinCount>=1) and resolve to the "timerfd:" path prefix, proving arg0 fd is captured rather than null. Locks in the 6ac9fa4 KindFd fix. splice/tee are NOT touched: retbytes_test.go already asserts enter_splice/enter_tee plus positive transfer byte counts, which inherently exercises their arg0 fd capture, so no new coverage is needed there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(chown): add end-to-end coverage for chown/fchown/lchown/fchownat familyPaul Buetow
chown/lchown/fchownat/fchown previously had no integration coverage: no scenario in cmd/ioworkload/ and no test in integrationtests/. All four are correctly classified (chown/lchown KindPathname args[0]; fchownat KindPathname args[1]; fchown KindFd args[0]; all FamilyFS, all UNCLASSIFIED ret) but nothing exercised them end-to-end. Add a chown-basic scenario that, on a temp file and a symlink the caller owns, issues raw chown(path,-1,-1), lchown(symlink,-1,-1), fchownat(AT_FDCWD,path, -1,-1,0) and fchown(fd,-1,-1). Owner/group -1/-1 means "leave both ids unchanged", which the kernel accepts without CAP_CHOWN, so the scenario is fully UNPRIVILEGED and nothing is actually modified. Raw syscalls are used so each distinct tracepoint fires rather than glibc redirecting chown to fchownat. TestChownBasic asserts enter_chown and enter_fchownat capture the regular file path (chownfile.txt), enter_lchown captures the symlink (chownlink), and enter_fchown fires (KindFd, no path). Mirrors the chmod coverage (scenario_chmod.go / chmod_test.go). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(chmod): add end-to-end coverage for chmod/fchmod/fchmodat familyPaul Buetow
chmod/fchmod/fchmodat/fchmodat2 previously had no integration coverage: no scenario in cmd/ioworkload/ and no test in integrationtests/. All four are correctly classified (chmod KindPathname args[0]; fchmodat/fchmodat2 KindPathname args[1]; fchmod KindFd args[0]; all FamilyFS, all UNCLASSIFIED ret) but nothing exercised them end-to-end. Add a chmod-basic scenario that, on a temp file the caller owns (so all calls are unprivileged), issues raw chmod(path, 0640), fchmodat(AT_FDCWD, path, 0644, 0), fchmod(fd, 0644), and best-effort fchmodat2(AT_FDCWD, path, 0640, 0) (raw syscall 452, ENOSYS tolerated on kernels < 6.6). Raw syscalls are used so each distinct tracepoint fires rather than glibc redirecting chmod to fchmodat. TestChmodBasic asserts enter_chmod and enter_fchmodat capture the file path (chmodfile.txt) and enter_fchmod fires (KindFd, no path). fchmodat2 is not asserted since it is version-gated, though it does fire on current kernels. Mirrors the utime coverage (scenario_utime.go / utime_test.go). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(copy_file_range): assert TRANSFER_CLASSIFIED copied byte count end-to-endPaul Buetow
TestCopyFileRangeBasic previously asserted only the enter event and source path (copyrangesrc.txt) but not the copied byte count on the TRANSFER_CLASSIFIED exit, unlike sibling sendfile64/splice/tee coverage in retbytes_test.go. The basic scenario copies exactly the 32-byte payload "copy_file_range integration data" in a single copy_file_range(2) call, so the exit byte count is deterministic. Switch to runScenarioResult and add assertEventBytesEqual(...,32) plus assertEventDurationPositive to lock in the transfer attribution (FamilyFS, fd_in@args[0], ret TransferClassified). Coverage hardening only; no defect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(retbytes): assert readlinkat READ_CLASSIFIED byte count end-to-endPaul Buetow
readlink/readlinkat are READ_CLASSIFIED (exit ctx->ret = link-target byte count), but the integration suite only asserted the enter_readlinkat pathname tracepoint via MinCount in link_test.go. The exit byte classification and positive duration were never validated end-to-end, unlike sibling READ-classified syscalls (read/recvfrom/getxattrat/ getdents64) in retbytes_test.go. Add retbytesReadlinkat to the phase-A workload: it creates a symlink with a known non-empty absolute target, opens the parent O_DIRECTORY, and re-issues SYS_READLINKAT in a short spaced window (mirroring the getdents64 driver) so ior can attach and capture an enter/exit pair under parallel load. Each call re-resolves the same link, so ctx->ret stays equal to the target length and is strictly positive. Add readlinkat (and symlink, used to build the link without mixing tracepoints) to retbytesTraceArgs, assert enter_readlinkat presence (MinCount) plus bytes>=1 via assertEventBytesAtLeast and a positive duration. bytes>=1 (not an exact target length) because the resolved path varies across temp dirs; >=1 is the safest invariant. Coverage hardening only; classify.go readlink/readlinkat=ReadClassified and the BPF arg capture (args[1]=pathname for readlinkat) are correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daystest(retbytes): assert getdents64 READ_CLASSIFIED byte count end-to-endPaul Buetow
getdents64 was listed in retbytesTraceArgs but the retbytes workload never issued it and TestRetbytesPhaseA never asserted its exit byte count, leaving the READ_CLASSIFIED ctx->ret path for getdents/getdents64 unverified end-to-end (unlike sibling read/recvfrom/getxattrat). Add retbytesGetdents to the phase-A workload: it populates a temp directory with several files, opens it O_DIRECTORY, and re-issues getdents64 in a short window (lseek-rewind each iteration) so ior can attach and capture an enter/exit pair under parallel load. A non-empty directory guarantees ctx->ret > 0. Assert enter_getdents64 presence (MinCount) plus bytes>=1 via assertEventBytesAtLeast and a positive duration, mirroring the existing READ-classified siblings. Bytes>=1 (not an exact payload length) because dirent size varies with filename length and alignment. Coverage hardening only; classify.go getdents/getdents64=ReadClassified is correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 daysfix(classify): assign vmsplice to FamilyNetwork, not MiscPaul Buetow
vmsplice(int fd, const struct iovec*, unsigned long nr_segs, unsigned int flags) is the iovec<->pipe variant of splice(2) and belongs to the same fd byte-mover cohort as its direct siblings splice/tee (and sendfile64/ copy_file_range). Its KIND (KindFd@arg0) and RET (TransferClassified, byte count) already matched splice/tee — only the family was wrong. Root cause: vmsplice was absent from the syscallFamilies map in internal/generate/family.go; its name matches no fsNameMarkers and it is not in fsSyscalls, so ClassifySyscallFamily fell through to FamilyMisc. This is the same documented Misc-fall-through anti-pattern already fixed for alarm/adjtimex/fanotify_init/fanotify_mark/file_getattr/file_setattr. The established mj0 decision placed splice/tee in Network, so the minimal sibling-consistent fix is vmsplice -> Network. Added "vmsplice": FamilyNetwork next to splice/tee with an explanatory comment, then re-ran `mage generate`. The regen is minimal and idempotent: only the two vmsplice trace IDs flip Misc->Network in generated_types.go and the vmsplice entry flips Misc->Network in generated_tracepoints.go; no TraceId renumbering and no other syscalls change. The generated C tracepoints are unaffected (family is a Go-side tag). Also moved vmsplice from the Misc list to the Network list in docs/syscall-tracing-plan.md (hand-maintained, docs-drift-validated), and corrected the misc_test.go comments which described vmsplice as a Misc syscall — it is still issued by the misc-basic workload and traced by name, but its transfer/byte-count coverage lives in retbytes_test.go alongside splice/tee. No vmsplice family assertion existed in the integration suite, so no coverage was relocated, only comments corrected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 daysfix(classify): capture timerfd_gettime/settime + splice/tee fd, not KindNullPaul Buetow
Root cause: the generic field matcher classifyByField only maps an arg literally named "fd" to KindFd. Several syscalls operate on an EXISTING fd whose tracepoint arg0 is named something else, so they fell through to KindNull -> null_event, capturing NO descriptor and dropping the fd they act on: - timerfd_gettime / timerfd_settime: arg0 is "int ufd" (the timerfd) - splice: arg0 is "int fd_in" (source fd of an in-kernel transfer) - tee: arg0 is "int fdin" (source fd of an in-kernel transfer) Fix: add explicit KindFd overrides for these four sys_enter_* keys to nameOnlyKindsTable so the enter handler captures arg0, mirroring the established epoll_wait(epfd) / mq_*(mqdes) / sendfile64(out_fd) / copy_file_range(fd_in) precedent. splice/tee were surfaced by a systemic sweep of tracepoint formats for fd-typed arg0 named other than "fd" that currently classify to null; they are TransferClassified siblings of sendfile64/copy_file_range and clearly fd-operating. The *at() family (dfd arg0) is intentionally untouched: it is path-classified, and timerfd_create remains the KindEventfd fd CREATOR. Regenerated artifacts (mage generate): the four enter handlers now emit fd_event capturing ctx->args[0] instead of null_event; exit handlers stay UNCLASSIFIED. Updated the generated kind maps, the golden result.txt, the classify_test expectations, and docs/syscall-tracing-plan.md (moved the four from kind "null" to kind "fd"; families IPC/Network unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daystest(integration): add landlock_create_ruleset coveragePaul Buetow
Add a Security-family end-to-end scenario + test for landlock_create_ruleset, which was previously untested. The new securityLandlockCreateRuleset scenario (registered as "security-landlock") builds a minimal valid struct landlock_ruleset_attr{handled_access_fs=LANDLOCK_ACCESS_FS_READ_FILE}, calls landlock_create_ruleset(&attr, sizeof(attr), 0) via raw syscall (nr=444 on amd64/arm64), and closes the returned ruleset fd. It tolerates ENOSYS/EOPNOTSUPP (kernel < 5.13 or Landlock LSM disabled) since the sys_enter tracepoint fires before any such error. It deliberately never calls landlock_restrict_self, which would irreversibly sandbox the shared integration-test runner. TestSecurityLandlockCreateRuleset asserts enter_landlock_create_ruleset MinCount>=1 and positive duration unconditionally, plus conditional "landlockfd:" path-prefix assertions on the create/close pair with an open/close path-stability check. Verified: TEST_NAME=TestSecurityLandlockCreateRuleset mage testWithName PASS (kernel 7.0.9); mage build, go build ./cmd/ioworkload/, and go vet ./integrationtests/ all clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daysfix(classify): assign file_setattr to FamilyFS, not MiscPaul Buetow
file_setattr(2) (Linux 6.13+) is the write counterpart of file_getattr: it sets a file's extended attributes (struct file_attr) given dirfd@arg0 + pathname@arg1 + attr-buffer + size + at_flags, and returns 0/-1 (not a byte count). Its name matches none of the fsNameMarkers substrings ("stat"/"xattr"/"chmod"/"chown") and it is absent from the fsSyscalls set, so it was falling through to FamilyMisc -- the same alarm/fanotify/file_getattr-style misclassification. Add it to the explicit family map for sibling consistency with file_getattr. This also completes the file_getattr regeneration: the prior fix (96de9ef) was generated with a partial target that updated generated_tracepoints.go but did not propagate file_getattr into the traceId2Family map in generated_types.go; a full `mage generate` here reconciles both file_getattr (1058/1059) and file_setattr (1056/1057) to FamilyFS. mage build, generate idempotency, and the internal/generate, /tracepoints, /types unit tests (incl. docs-drift sync) all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daysfix(classify): assign file_getattr to FamilyFS, not MiscPaul Buetow
file_getattr(2) (Linux 6.13+) retrieves a file's extended attributes (struct file_attr) given a dirfd + pathname + attr buffer + size + at_flags. It is a path-based filesystem operation, the counterpart of statx and the FS_IOC_FSGETXATTR ioctl, so it belongs in FamilyFS. It was falling through to FamilyMisc because the fsNameMarkers substring list keys on "stat"/"xattr"/"chmod"/"chown" — "getattr" matches none of them — and the syscall is absent from the fsSyscalls set, the same Misc-fall-through defect previously fixed for alarm/fanotify_init/ fanotify_mark. Add an explicit "file_getattr": FamilyFS entry to the syscallFamilies map, regenerate the Go tracepoint map, and update the docs/syscall-tracing-plan.md family listing to match. KIND stays KindPathname (pathname@args[1], data-driven from the live tracepoint) and the return is 0/-1, hence UNCLASSIFIED (not a byte count) — both already correct, only the family was wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daysfix(classify): assign fanotify_mark to FamilyIPC, not MiscPaul Buetow
fanotify_mark(2) adds, removes, or modifies a mark on an fanotify notification group. Its arg0 is the fanotify group fd returned by fanotify_init(2); it carries a dirfd@arg3 and an optional pathname@arg4 and returns 0/-1 (not a byte count). It is the operation counterpart of fanotify_init and the direct analog of inotify_add_watch (both register a watch/mark on a filesystem object via the notification-group fd). inotify_add_watch is FamilyIPC, and fanotify_init was just moved Misc->IPC to sit with the fd-based event-notification primitives (eventfd, signalfd, timerfd, userfaultfd, inotify_*). fanotify_mark, however, was still falling through to FamilyMisc by omission from the explicit family table -- the same alarm/adjtimex-style misclassification fixed for fanotify_init in 88769d4, and flagged there as still-outstanding for fanotify_mark. Add it to the IPC family map for sibling consistency and regenerate. KIND is unchanged and correct: KindPathname capturing the optional pathname at args[4]. This matches the *at() cohort convention (fchmodat, fchownat, unlinkat, mkdirat, newfstatat, utimensat, name_to_handle_at all carry a dirfd at arg0 yet capture the pathname), since fanotify_mark has a dirfd@arg3 + pathname@arg4 pair. RET stays UNCLASSIFIED (returns 0/-1). Docs plan updated to keep the docs-drift tests in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daystest(integration): add cachestat end-to-end coveragePaul Buetow
cachestat(2) had no integration coverage. Add a readwrite-cachestat ioworkload scenario and TestReadwriteCachestat mirroring the readahead precedent: open a temp file, write data to populate the page cache, then issue cachestat via a raw syscall (no glibc/unix wrapper) with a whole-file cachestat_range and zeroed cachestat output buffer, flags=0. ENOSYS on kernels < 6.5 is tolerated for portability. The test asserts enter_cachestat is captured with the fd-resolved file path, that the UNCLASSIFIED return attributes zero bytes, and that the syscall duration is positive. golang.org/x/sys is promoted to a direct dependency. Verified PASS under sudo on kernel 7.0.9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 daystest(integration): add inotify family tracing coveragePaul Buetow
Add an inotify-basic ioworkload scenario and an end-to-end integration test covering the inotify IPC family, which previously had no integration coverage (only inotify_init1 had a unit-level eventloop test). The scenario issues inotify_init1(IN_CLOEXEC) -> inotify_add_watch on a temp file (IN_CREATE|IN_DELETE|IN_MODIFY) -> inotify_rm_watch -> close. It is non-blocking: it registers and removes the watch without reading events, and cleans up the temp dir on return. TestInotifyBasic asserts enter_inotify_init1, enter_inotify_add_watch, enter_inotify_rm_watch and enter_close each fire at least once, with positive durations and PID/comm hermetic guards. The init1 instance fd resolves to the inotifyfd: path label; add_watch/rm_watch capture the instance fd@arg0 which resolves to the same registered label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 daysfix(close): deregister fd only on successful close (ret==0)Paul Buetow
applyFdCloseState evicted the fd->path mapping (fdState + proc-fd cache) on every close exit, ignoring the return value. A failed close — most importantly EBADF ("fd isn't a valid open file descriptor"), but also EINTR/EIO — did not release any descriptor we track, so dropping the mapping there would let a later genuine close or a reuse of the fd number resolve against stale/empty state. Gate the eviction on ret==0, mirroring the ret==0 guard already used by applyCloseRangeState for close_range. close's exit tracepoint is generated as a ret_event (UNCLASSIFIED), so the exit carries the close return value in RetEvent.Ret. Tests: the close-exit event was being built as an fd_event, which does not match the real BPF wire format (sys_exit_close emits EXIT_RET_EVENT). Add a makeExitCloseEvent helper that emits the correct ret_event, route all 18 close-exit call sites and TestHandleFdExitCloseClearsProcFdCache through it, and add CloseFailureTest asserting a failed close (ret=-1) leaves the fd tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 daystest(xattr): add removexattrat end-to-end integration coveragePaul Buetow
removexattrat(2) (Linux 6.13+) was the one xattr *at-variant lacking integration coverage: xattr_test.go exercised getxattrat/listxattrat (READ-classified byte counts) and the path-based setxattr, but never the REMOVE *at variant. Unlike its getxattrat/listxattrat siblings, removexattrat returns a 0/-1 status (not a byte count), so its exit must be UNCLASSIFIED — matching removexattr/lremovexattr/fremovexattr. Add an ioworkload scenario (xattr-removexattrat) that setxattr's a user.* attribute via a real path then removes it via raw removexattrat(AT_FDCWD, path, 0, name), plus TestXattrRemovexattrat asserting the path (args[1], after the dirfd) is captured (never the xattr name at args[3]) and that accounted bytes are exactly zero (guarding against wrongly READ-classifying it like getxattrat/ listxattrat). Distinct from the fd-based fremovexattr gap (task 8i0). Classification verified by inspection: FamilyFS (xattr marker), KindPathname at the pathname field (ctx->args[1]), and absent from the ret-classification table => UNCLASSIFIED. No generator change needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 daysfix(classify): assign fanotify_init to FamilyIPC, not MiscPaul Buetow
fanotify_init(2) creates and initializes an fanotify notification group and returns an event-queue file descriptor. It is the direct analog of inotify_init1 (both are filesystem-event notification facilities whose group-creating syscall is a flags-taking fd-creator). inotify_init/ inotify_init1 are FamilyIPC alongside the other fd-based event-notification primitives (eventfd, signalfd, timerfd, userfaultfd), yet fanotify_init fell through to FamilyMisc by omission from the explicit family table -- an alarm/adjtimex-style misclassification inconsistent with its siblings. Add fanotify_init to the IPC family map and regenerate. Kind (KindEventfd, flags at args[0]) and ret (UNCLASSIFIED, returned fd captured via the fd mechanism) were already correct and are unchanged. fanotify_mark stays in Misc (path-marking, not fd creation). Docs plan updated to keep the docs-drift test in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): cover fsconfig/fspick/open_tree in mount-API scenarioPaul Buetow
Extend the mountfs-management scenario with best-effort fsconfig (KindFd), fspick (KindPathname), and open_tree (KindOpen) calls to complete new-mount-API end-to-end coverage. fsconfig reuses the fscontext fd from fsopen (FSCONFIG_SET_STRING + FSCONFIG_CMD_CREATE), fspick targets "/" with FSPICK_NO_AUTOMOUNT, and open_tree clones the scenario mount point with OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC. All returned fds are closed and all errno values are ignored, so ENOSYS/EPERM/EINVAL/EBADF are tolerated; the sys_enter_ tracepoints fire on kernel entry regardless, creating no mounts on the host. Assert enter_fsconfig/enter_fspick/enter_open_tree (MinCount>=1) in TestMountFsManagementSyscalls and add the three syscalls to the trace filter. Gating is unchanged (root-only via the shared harness). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01fix(classify): mq_timedsend returns status, not bytes — make ret UNCLASSIFIEDPaul Buetow
mq_timedsend(2)/mq_send(3) return 0 on success or -1 on error; the payload size msg_len is an INPUT argument, never the return value. It was wrongly listed in retClassifications as WriteClassified, which made bytesFromRet attribute its 0 return as "bytes written" (the stats engine WriteClassified path). Remove it so its return stays UNCLASSIFIED, consistent with its POSIX mq sibling mq_timedreceive (which legitimately stays ReadClassified because it returns the received byte count). This is the exact same defect just fixed for SysV msgsnd (5057bd9) and mirrors the msgrcv/msgsnd asymmetry. Regenerated tracepoints/docs accordingly and updated the pre-existing classify unit test and the TestPosixMqBasic integration assertion: the mq_timedsend send no longer asserts a write byte count (now expects 0), while mq_timedreceive keeps its received-byte-count assertion. Verified: mage generate idempotent, mage build OK, internal/generate tests pass. TestPosixMqBasic skips in this sandbox (mq_open: permission denied) but compiles with the corrected assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(xattr): add path-based setxattr end-to-end integration coveragePaul Buetow
The xattr integration tests previously only asserted on getxattrat/ listxattrat (path/dirfd READ-classified variants). The path-based setxattr(2) was traced by the existing xattr-getxattrat scenario (workload calls syscall.Setxattr; xattrTraceArgs includes setxattr) but never asserted. Add TestXattrSetxattr to verify end-to-end that: - enter_setxattr captures the filesystem PATH at args[0] (kind=pathname), never the xattr NAME at args[1]; - exit_setxattr is UNCLASSIFIED: setxattr returns a 0/-1 status, not a byte count (the size arg is the INPUT value length), so accounted bytes must be exactly zero. This guards against the msgsnd-style bug of treating a status return as bytes written, and contrasts with getxattr/listxattr which DO return byte counts (READ-classified). This is the PATH-based set complement to filed task 8i0 (fd-based fsetxattr/fgetxattr/flistxattr/fremovexattr); it does not duplicate it. Classification (FamilyFS, KindPathname enter, UNCLASSIFIED ret) is verified by inspection per task guidance, not by a unit test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(sched): assert sched_rr_get_interval enter in TestSchedBasicPaul Buetow
The sched-basic scenario already issues sched_rr_get_interval(0, &ts) via schedQueryPriorityRange, but TestSchedBasic only asserted the other sched_get* queries. Add an explicit enter_sched_rr_get_interval assertion to lock in its KindNull/UNCLASSIFIED tracing end-to-end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01fix(classify): msgsnd returns status, not bytes — make ret UNCLASSIFIEDPaul Buetow
msgsnd(2) returns 0 on success or -1 on error; the payload size msgsz is an INPUT argument, never the return value. It was wrongly listed in retClassifications as WriteClassified, which made the stats engine treat its 0 return as "bytes written". Remove it so its return stays UNCLASSIFIED, consistent with its SysV IPC siblings (msgrcv legitimately stays ReadClassified because it returns a received byte count). Regenerated tracepoints/docs accordingly. Verified: mage generate idempotent, mage build OK, internal/generate tests pass, and the TestSysVMsgBasic integration test (added in task 7i0) still passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01fix(classify): assign alarm to FamilyTime, not MiscPaul Buetow
alarm(2) arranges for a SIGALRM after a given number of seconds; it is a simplified setitimer(ITIMER_REAL) and, per alarm(2) NOTES, "alarm() and setitimer(2) share the same timer; calls to one will interfere with use of the other." The syscallFamilies table omitted alarm, so it fell through to FamilyMisc while its siblings setitimer/getitimer/timer_create were correctly FamilyTime — an adjtimex-style misclassification (cf. 7243b7c). Add alarm -> FamilyTime and move it from Misc to Time in the tracing plan; regenerate the family maps (trace IDs 468/469 now FamilyTime, "alarm": "Time"). Kind classification (KindNull/null_event: the single arg is an unsigned int seconds, no fd/path) and the UNCLASSIFIED return (seconds remaining, not a byte count; alarm never fails) were already correct. Also harden the misc-basic integration test with a deterministic enter_alarm assertion (alarm(0) is issued unconditionally by the scenario; the syscall-entry tracepoint always fires) so the alarm enter path is covered end-to-end even though alarm is now FamilyTime rather than Misc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add SysV msg/sem tracing coveragePaul Buetow
Add sysv-msg-basic and sysv-sem-basic ioworkload scenarios that exercise the SysV message-queue and semaphore families end-to-end via raw syscalls, mirroring the existing sysv-shm-basic scenario. sysv-msg-basic: msgget(IPC_PRIVATE) -> msgsnd -> msgrcv -> msgctl(IPC_RMID), using a struct msgbuf {int64 mtype; [16]byte mtext} and msgsz = body length (excluding mtype). sysv-sem-basic: semget(IPC_PRIVATE, 1) -> semop(+1) -> semop(-1) -> semctl(IPC_RMID), incrementing before decrementing so the operation can never block. Both defer IPC_RMID right after the get so no kernel IPC object leaks even on partial failure. Add TestSysVMsgBasic and TestSysVSemBasic asserting the enter_ events for msgget/msgsnd/msgrcv/msgctl and semget/semop/semctl are traced with MinCount>=1 and positive duration, plus PID/comm hermetic guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add Sched family tracing coveragePaul Buetow
Add a self-targeted, non-disruptive sched-basic ioworkload scenario and a dedicated TestSchedBasic integration test. The scenario pins to one OS thread (LockOSThread) and exercises only safe Sched syscalls: sched_yield; sched_getaffinity then sched_setaffinity re-applying the identical mask (a no-op); and read-only sched_getscheduler, sched_getparam, sched_getattr, sched_get_priority_max/min, and sched_rr_get_interval. sched_setscheduler, sched_setattr, and sched_setparam are intentionally excluded. The test scopes -trace-syscalls to the sched_* family, guards on PID and comm, and asserts enter_ tracepoints fire (MinCount>=1) for sched_yield, sched_getaffinity, sched_getscheduler, and sched_getparam. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add Misc family tracing coveragePaul Buetow
Add a misc-basic ioworkload scenario and an end-to-end integration test for the previously-uncovered Misc syscall family. The scenario exercises only the safe, unprivileged, non-blocking, side-effect-free Misc syscalls: getcpu (raw SYS_GETCPU), uname / sys_newuname (unix.Uname), sysinfo (unix.Sysinfo), vmsplice into a self-created and self-drained pipe with a tiny buffer, and alarm(0) to cancel any pending alarm. Code comments document why the remaining Misc syscalls are intentionally excluded (CAP_SYS_ADMIN / global host mutation, CAP_SYS_RAWIO / x86-only, Linux 6.13+ availability, runtime-managed, or not user-callable). misc_test.go asserts enter_getcpu, enter_newuname, and enter_sysinfo are each traced at least once for the ioworkload process, restricting tracing to the issued syscalls and keeping the existing PID/comm hermetic guards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add Signals family tracing coveragePaul Buetow
The SIGNALS syscall family previously had zero end-to-end coverage (signalfd/signalfd4 are IPC-family fd creators, not Signals). Add a self-targeting ioworkload scenario and an integration test that assert the family's tracepoints fire. scenario_signals.go (signals-basic) issues, all self-directed so it mutates no other process: - rt_sigaction : install SIG_IGN disposition for SIGUSR1 - rt_sigprocmask: BLOCK SIGUSR1 before sending, so self-delivery only marks it pending (never runs a handler / kills us) - sigaltstack : set then disable an alternate signal stack - kill/tgkill/tkill/rt_sigqueueinfo: send SIGUSR1 to self four ways - rt_sigpending : query the pending mask - rt_sigtimedwait: reap the pending signal with a SHORT 100ms timeout (hang guard); EAGAIN tolerated Safety: signal is blocked before any send; rt_sigtimedwait uses a 100ms timeout so it cannot hang; the original signal mask and SIGUSR1 disposition are restored and the alt stack disabled on exit. The goroutine is pinned with LockOSThread so gettid() matches the tgkill/tkill target and the per-thread mask applies to the waiting thread. Raw syscalls are issued directly so the tracepoints fire regardless of the Go runtime's own signal handling. pause (noreturn) and rt_sigreturn (handler-return only) are deliberately excluded. signals_test.go asserts enter_ tracepoints with MinCount>=1 for rt_sigaction, rt_sigprocmask, rt_sigpending, sigaltstack, kill, tgkill, and rt_sigtimedwait, plus a positive duration for the rt_sigtimedwait enter/exit pair. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(internal): make comm-propagation/filter tests hermeticPaul Buetow
TestCommPropagation and TestEventTypeFiltering/FdEventFiltering were flaky in full `mage test` runs (passing in isolation). For synthetic events whose tid was not in the comm cache, the event loop fell back to commResolver's default resolveFn, which reads /proc/<tid>/comm on the host. The fixed test pids/tids are small (e.g. defaultTid+100 == 111, defaultPid+1 == 11) and collide with real transient kernel threads (e.g. kworker/0:1-events), so the resolved comm depended on what happened to be running on the host at that instant. Fix: use commResolver's existing injectable resolveFn seam. Add a newHermeticCommResolver() test helper whose resolveFn returns ("", nil) and never touches /proc, and inject it into TestCommPropagation (via eventLoopConfig.commResolver) and newEventLoopWithFilter (used by TestEventTypeFiltering). No production code changes. Assertions are unchanged: positive comm names still come from the synthetic OpenEvent.Comm bytes; cache-miss tids now deterministically resolve to empty regardless of host state. Updated the stale "use a very large TID to avoid /proc collisions" comment accordingly. Verified: -count=50 (affected tests) and -race -count=10 green, full `mage test` and `mage build` green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add SysV shm tracing coveragePaul Buetow
The SysV shared-memory family (shmget/shmat/shmdt/shmctl) had no end-to-end integration coverage. Add an ioworkload `sysv-shm-basic` scenario that, without privileges, runs shmget(IPC_PRIVATE) -> shmat -> write into the mapped segment -> shmdt -> shmctl(IPC_RMID), always issuing IPC_RMID (via defer) so no kernel segment leaks. Add TestSysVShmBasic asserting enter_shmget/enter_shmat/enter_shmdt/ enter_shmctl are each traced with a positive (paired enter/exit) duration. msg/sem coverage is scoped out and tracked as a follow-up task (7i0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(integration): add pwritev/pwritev2 retbytes coveragePaul Buetow
Add write-side positional vectored coverage to close the remaining gap in the pwrite64-family byte-accounting validation. The retbytes/readwrite integration suite already exercised pwrite64 (scalar) and the read-side preadv/preadv2, but the WRITE_CLASSIFIED byte attribution for the vectored positional writes pwritev/pwritev2 was only covered by unit tests, not end-to-end. New ioworkload scenarios: - readwrite-pwritev: issues pwritev (syscall.SYS_PWRITEV) writing a known two-iovec payload at offset 0 to a temp file. - readwrite-pwritev2: issues pwritev2 via the explicit syscall number (328 amd64 / 287 arm64, mirroring preadv2SyscallNr) with offset 0 and no flags. New integration tests assert enter_pwritev/enter_pwritev2 fired and that the attributed retbytes equal the exact iovec total, validating WRITE_CLASSIFIED end-to-end. Both pass as root. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01fix(types): fast-decode hot events at padded kernel struct sizePaul Buetow
The BPF side reserves sizeof(struct ...) via bpf_ringbuf_reserve, so the userspace ringbuf sample length equals the C struct size INCLUDING trailing alignment padding. For five event structs whose last field is 32-bit but which also contain a __u64 (forcing 8-byte trailing pad), the *EventSize fast-decode gate constants were set to the unpadded field-sum size instead of sizeof: open_event 300 -> 304 fd_event 28 -> 32 ret_event 36 -> 40 socket_event 36 -> 40 open_by_handle_at_event 28 -> 32 Because the kernel payload length never equalled those constants, the fast explicit-offset decoders were silently bypassed and every such event (including the very hot ret_event on each syscall exit, and fd_event on read/write/etc enter) fell back to the slow reflection-based binary.Read path. binary.Read reads only the field bytes and ignores trailing padding, so values were always correct -- this was a performance regression, not a correctness bug; verified by decoding padded payloads. Fix mirrors the existing socketpair/accept/pipe/eventfd/poll V1/V2 handling: the size gate now accepts both the kernel sizeof and the legacy field-sum size (the latter still emitted by Go binary.Write in tests and Bytes()), and the trailing pad bytes are ignored. Added kernel-layout tests feeding the padded payload for all five fixed decoders. Audit (task i20) confirmed every other event struct's Go layout, field order, and fast-decode offsets match the C side byte-for-byte. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01test(generate): remove redundant pure-classification unit testsPaul Buetow
Classification correctness (which family/kind/return-class a syscall maps to) is verified by inspection against the man pages and the classifier rules, not by dedicated unit tests. The tracing-relevant outcome — which fd/path/byte-count the generated BPF C actually captures — is covered by the GenerateTracepointsC codegen tests and the end-to-end integration tests, all of which are retained. Removed: - internal/generate/family_test.go (ClassifySyscallFamily / .Family table) - internal/generate/retclassify_test.go (ClassifyRet read/write/transfer/ unclassified tables) - ~70 pure-classification tests trimmed from classify_test.go, keeping only the GenerateTracepointsC codegen/tracing tests plus the shared helpers (mustParseAll, mqFormats, phaseAFormats, syntheticEnter/Exit, itoa) used by codegen_test.go. - pure-classification funcs interleaved in codegen_test.go (TestClassifyRet*Unclassified, TestClassifyTkillFallsThroughToNull, Test{Mkdirat,Rmdir}FamilyAndKindMatchSiblings). Kept all TestGenerate* handler tests (they assert the generated BPF C captures the correct fd/path/arg-index/return classification), the isNoreturnSyscall tests, docs-drift guards, eventloop dispatch tests, and the integration suite — so every affected syscall still has tracing coverage. No tracing gaps discovered. generate package: go test (incl. -race) green; mage build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31fix(classify): assign adjtimex to FamilyTime, not MiscPaul Buetow
adjtimex(2) and clock_adjtime(2) share one man page: both tune or query the kernel clock (clock_adjtime is adjtimex with an explicit clockid) and return a clock-state code or -1. The syscallFamilies table omitted adjtimex, so it fell through to FamilyMisc while its sibling clock_adjtime was correctly FamilyTime. Add adjtimex -> FamilyTime and move it from Misc to Time in the tracing plan; regenerate the family maps (trace IDs 418/419 now FamilyTime, "adjtimex": "Time"). Kind classification (KindNull/null_event) and the UNCLASSIFIED return (a clock-state code, not a byte count) were already correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>