diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-01 11:19:19 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-01 11:19:19 +0300 |
| commit | 55aa404fc93deeff27205b3cc9af407ab071be4b (patch) | |
| tree | 8e35e1cdfce0e3057920f5697babb942c00cd673 /cmd | |
| parent | 96065ab5c13295f0c2ec810cf540c229c41e2647 (diff) | |
test(integration): add Misc family tracing coverage
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>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ioworkload/scenario_misc.go | 141 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 |
2 files changed, 142 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_misc.go b/cmd/ioworkload/scenario_misc.go new file mode 100644 index 0000000..4a79c6d --- /dev/null +++ b/cmd/ioworkload/scenario_misc.go @@ -0,0 +1,141 @@ +package main + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +// miscVmspliceLen is the size of the tiny buffer vmsplice gathers into the +// pipe. It is kept far below the default pipe capacity (64 KiB) so vmsplice +// never blocks waiting for room, and we drain the read end afterwards anyway. +const miscVmspliceLen = 16 + +// miscBasic exercises the SAFE, UNPRIVILEGED members of the Misc syscall +// family so the enter_/exit_ tracepoints fire end-to-end. Every call here is +// read-only or self-contained: none mutate global host state, none require +// elevated capabilities, and none can block. +// +// - getcpu reports the CPU/NUMA node the caller runs on (raw syscall; the +// unix package has no portable wrapper). +// - newuname (via unix.Uname) reads the kernel/host name strings. +// - sysinfo (via unix.Sysinfo) reads memory/load/uptime counters. +// - vmsplice gathers a tiny in-memory buffer into a self-created pipe; we +// drain (and close) the pipe so it can never fill up or block. +// - alarm(0) cancels any pending SIGALRM and returns the previous value; +// passing 0 is harmless and arms no new timer (raw syscall; no wrapper). +// +// INTENTIONALLY EXCLUDED from this scenario (and documented here so the reasons +// travel with the code): +// - acct, sethostname, setdomainname, syslog, fanotify_init, fanotify_mark: +// require CAP_SYS_ADMIN and/or mutate GLOBAL host state (hostname, kernel +// log, process accounting) — unsafe to invoke from a test workload. +// - ioperm, iopl, modify_ldt: require CAP_SYS_RAWIO and are x86-only port/LDT +// manipulation — privileged and non-portable. +// - file_getattr, file_setattr: only exist on Linux 6.13+, so may be absent +// on the kernels the integration suite runs against. +// - rseq, get_robust_list, set_robust_list: auto-managed by the Go/C runtime +// for restartable sequences and robust futexes; re-invoking them by hand +// would corrupt the runtime's own registration. +// - uprobe, uretprobe: probe-attach mechanisms, not user-callable syscalls. +func miscBasic() error { + if err := miscGetcpu(); err != nil { + return err + } + if err := miscUname(); err != nil { + return err + } + if err := miscSysinfo(); err != nil { + return err + } + if err := miscVmsplice(); err != nil { + return err + } + return miscAlarmCancel() +} + +// miscGetcpu issues getcpu(2) via a raw syscall (golang.org/x/sys/unix ships +// no portable wrapper). It writes the current CPU and NUMA node into two output +// words; the third argument (the obsolete tcache) is NULL. +func miscGetcpu() error { + var cpu, node uint32 + if _, _, errno := syscall.RawSyscall( + unix.SYS_GETCPU, + uintptr(unsafe.Pointer(&cpu)), + uintptr(unsafe.Pointer(&node)), + 0, + ); errno != 0 { + return fmt.Errorf("getcpu: %w", errno) + } + return nil +} + +// miscUname issues sys_newuname (the modern uname(2)) via unix.Uname, reading +// the kernel/host identification strings into a caller-owned Utsname buffer. +func miscUname() error { + var uts unix.Utsname + if err := unix.Uname(&uts); err != nil { + return fmt.Errorf("uname: %w", err) + } + return nil +} + +// miscSysinfo issues sysinfo(2) via unix.Sysinfo, reading uptime/load/memory +// counters into a caller-owned Sysinfo_t buffer. Purely a read. +func miscSysinfo() error { + var info unix.Sysinfo_t + if err := unix.Sysinfo(&info); err != nil { + return fmt.Errorf("sysinfo: %w", err) + } + return nil +} + +// miscVmsplice gathers a tiny fixed buffer into a freshly created pipe via +// vmsplice(2), then drains and closes the pipe. The buffer (miscVmspliceLen +// bytes) is far smaller than the pipe capacity, so vmsplice cannot block, and +// draining the read end leaves no descriptors or data behind. +func miscVmsplice() error { + var fds [2]int + if err := unix.Pipe(fds[:]); err != nil { + return fmt.Errorf("pipe for vmsplice: %w", err) + } + readEnd, writeEnd := fds[0], fds[1] + defer unix.Close(readEnd) + defer unix.Close(writeEnd) + + buf := make([]byte, miscVmspliceLen) + iov := unix.Iovec{Base: &buf[0], Len: uint64(len(buf))} + n, _, errno := syscall.Syscall6( + unix.SYS_VMSPLICE, + uintptr(writeEnd), + uintptr(unsafe.Pointer(&iov)), + 1, // one iovec + 0, // no SPLICE_F_* flags needed for this tiny, non-blocking write + 0, 0, + ) + if errno != 0 { + return fmt.Errorf("vmsplice: %w", errno) + } + + // Drain whatever vmsplice placed into the pipe so nothing lingers and the + // pipe never approaches its capacity. A short read is fine. + drain := make([]byte, int(n)) + if int(n) > 0 { + if _, err := unix.Read(readEnd, drain); err != nil { + return fmt.Errorf("drain vmsplice pipe: %w", err) + } + } + return nil +} + +// miscAlarmCancel issues alarm(0) via a raw syscall (no unix wrapper). Passing 0 +// CANCELS any pending SIGALRM and returns the seconds remaining on the previous +// timer; it arms no new alarm, so it is entirely harmless. The previous value +// is ignored — we only care that the syscall is issued and traced. +func miscAlarmCancel() error { + // alarm never fails; RawSyscall's errno is always 0 here. + _, _, _ = syscall.RawSyscall(unix.SYS_ALARM, 0, 0, 0) + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index f069596..3505984 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -138,6 +138,7 @@ var scenarios = map[string]func() error{ "aio-setup-einval": aioSetupEinval, "aio-submit": aioSubmit, "signals-basic": signalsBasic, + "misc-basic": miscBasic, } func makeTempDir(prefix string) (string, func(), error) { |
