From 3ce0f52a9f608b28c550083574fa3ef442107f53 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 6 Jun 2026 10:08:54 +0300 Subject: test: add coverage for setitimer (signal-safe) and statfs/fstatfs 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 --- cmd/ioworkload/scenario_stat.go | 37 ++++++++++++++++++++++++++++ cmd/ioworkload/scenario_timer.go | 53 ++++++++++++++++++++++++++++++++++++++++ cmd/ioworkload/scenarios.go | 2 ++ integrationtests/stat_test.go | 19 ++++++++++++++ integrationtests/timer_test.go | 29 ++++++++++++++++++++++ 5 files changed, 140 insertions(+) diff --git a/cmd/ioworkload/scenario_stat.go b/cmd/ioworkload/scenario_stat.go index 5d242c7..7e0c0eb 100644 --- a/cmd/ioworkload/scenario_stat.go +++ b/cmd/ioworkload/scenario_stat.go @@ -267,6 +267,43 @@ func statAccessEnoent() error { return nil } +// statStatfs creates a file and queries its filesystem via statfs(2) (path +// input) and fstatfs(2) (fd input) using Go's syscall wrappers, which map +// directly to SYS_STATFS / SYS_FSTATFS. +// +// - enter_statfs is a path_event: it captures the pathname at arg0, so the +// trace records carry the file's path. +// - enter_fstatfs is an fd_event: it captures the fd at arg0; ior resolves the +// path through its fd lookup table. Its exit is an UNCLASSIFIED ret_event. +func statStatfs() error { + dir, cleanup, err := makeTempDir("stat-statfs") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "statfsfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + // statfs(path, &buf): the pathname is read on entry (enter_statfs). + var sbuf syscall.Statfs_t + if err := syscall.Statfs(path, &sbuf); err != nil { + return fmt.Errorf("statfs: %w", err) + } + + // fstatfs(fd, &buf): the fd is read on entry (enter_fstatfs). + var fbuf syscall.Statfs_t + if err := syscall.Fstatfs(fd, &fbuf); err != nil { + return fmt.Errorf("fstatfs: %w", err) + } + + return nil +} + // statFstatEbadf calls raw SYS_FSTAT on an invalid fd (99999). // The syscall fails with EBADF, but ior captures the enter_newfstat // tracepoint because it is recorded on syscall entry. diff --git a/cmd/ioworkload/scenario_timer.go b/cmd/ioworkload/scenario_timer.go index 0bf628d..923c004 100644 --- a/cmd/ioworkload/scenario_timer.go +++ b/cmd/ioworkload/scenario_timer.go @@ -15,6 +15,59 @@ type itimerspec struct { Value unix.Timespec } +// itimerval mirrors struct itimerval from (it_interval, it_value), +// each a struct timeval. It is the argument to setitimer/getitimer (the classic +// interval timers, distinct from the POSIX per-process timer_* family above). +type itimerval struct { + Interval unix.Timeval + Value unix.Timeval +} + +// itimerReal is ITIMER_REAL (=0): a real-time interval timer that, when armed, +// delivers SIGALRM on expiry. +const itimerReal = 0 + +// intervalTimerNoop exercises the classic interval-timer syscalls setitimer(2) +// and getitimer(2) without ever arming anything, so the enter_setitimer / +// enter_getitimer tracepoints fire end-to-end while remaining fully SIGNAL-SAFE. +// +// SAFETY: setitimer is called with an ALL-ZERO struct itimerval. A zero it_value +// disarms the timer and arms nothing new, so NO SIGALRM is ever scheduled or +// delivered — this mirrors the alarm(0) pattern in miscAlarmCancel and the +// far-future / never-firing approach in posixTimerLifecycle. getitimer is a pure +// read of the (now disarmed) timer's state. +// +// Both setitimer and getitimer are KindNull (null_event) on enter and return an +// UNCLASSIFIED ret_event on exit: neither takes a pathname nor an fd, and the +// return value is not a descriptor. We therefore only assert enter-presence. +func intervalTimerNoop() error { + // All-zero itimerval: it_interval = it_value = {0,0}. This disarms + // ITIMER_REAL and arms nothing, so no SIGALRM can fire. + var zero itimerval + if _, _, errno := syscall.RawSyscall( + unix.SYS_SETITIMER, + itimerReal, + uintptr(unsafe.Pointer(&zero)), + 0, // old_value == NULL (we don't care about the previous setting) + ); errno != 0 { + return fmt.Errorf("setitimer: %w", errno) + } + + // getitimer reads the current (disarmed) ITIMER_REAL state into out; a safe, + // side-effect-free read included for symmetry with setitimer. + var out itimerval + if _, _, errno := syscall.RawSyscall( + unix.SYS_GETITIMER, + itimerReal, + uintptr(unsafe.Pointer(&out)), + 0, + ); errno != 0 { + return fmt.Errorf("getitimer: %w", errno) + } + + return nil +} + // posixTimerLifecycle exercises the full POSIX per-process timer family so the // tracer's null_event handling is covered end-to-end: // diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 869c9ec..2eb18aa 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -52,6 +52,7 @@ var scenarios = map[string]func() error{ "polling-epoll": pollingEpoll, "sleep-syscalls": sleepSyscalls, "posix-timer-lifecycle": posixTimerLifecycle, + "interval-timer-noop": intervalTimerNoop, "process-exec-lifecycle": processExecLifecycle, "family-mixed": familyMixed, "close-basic": closeBasic, @@ -107,6 +108,7 @@ var scenarios = map[string]func() error{ "stat-enoent": statEnoent, "stat-access-enoent": statAccessEnoent, "stat-fstat-ebadf": statFstatEbadf, + "stat-statfs": statStatfs, "xattr-getxattrat": xattrGetxattrat, "xattr-listxattrat": xattrListxattrat, "xattr-removexattrat": xattrRemovexattrat, diff --git a/integrationtests/stat_test.go b/integrationtests/stat_test.go index 400e61a..e803b9f 100644 --- a/integrationtests/stat_test.go +++ b/integrationtests/stat_test.go @@ -101,6 +101,25 @@ func TestStatAccessEnoent(t *testing.T) { }) } +// TestStatStatfs verifies the statfs family (statfs/fstatfs) is traced +// end-to-end. enter_statfs is a path_event, so its record must contain the +// file's path; enter_fstatfs is an fd_event, asserted via enter-presence. +func TestStatStatfs(t *testing.T) { + runScenario(t, "stat-statfs", []ExpectedEvent{ + { + PathContains: "statfsfile.txt", + Tracepoint: "enter_statfs", + Comm: "ioworkload", + MinCount: 1, + }, + { + Tracepoint: "enter_fstatfs", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} + func TestStatFstatEbadf(t *testing.T) { runScenario(t, "stat-fstat-ebadf", []ExpectedEvent{ { diff --git a/integrationtests/timer_test.go b/integrationtests/timer_test.go index 7d9b295..10f54e9 100644 --- a/integrationtests/timer_test.go +++ b/integrationtests/timer_test.go @@ -12,6 +12,15 @@ var posixTimerTraceArgs = []string{ "timer_create,timer_settime,timer_gettime,timer_getoverrun,timer_delete,timerfd_create", } +// intervalTimerTraceArgs restricts tracing to the classic interval-timer +// syscalls setitimer/getitimer, which the interval-timer-noop workload issues. +// Both are KindNull (null_event) on enter with an UNCLASSIFIED ret on exit, so +// the test asserts only enter-presence (no path/fd/return to inspect). +var intervalTimerTraceArgs = []string{ + "-trace-syscalls", + "setitimer,getitimer", +} + // TestPosixTimerLifecycle verifies the POSIX per-process timer family is traced // end-to-end. The workload runs timer_create -> timer_settime -> timer_gettime // -> timer_getoverrun -> timer_delete; each must appear as an enter event. @@ -41,3 +50,23 @@ func TestPosixTimerLifecycle(t *testing.T) { "timer_create returns a timer_t, not an fd, and must not be classified like timerfd_create", got) } } + +// TestIntervalTimerNoop verifies the classic interval-timer family (setitimer / +// getitimer) is traced end-to-end. The interval-timer-noop workload issues a +// setitimer(ITIMER_REAL, &{0,0,0,0}, NULL) — an all-zero itimerval that arms +// nothing, so NO SIGALRM is ever scheduled — followed by a getitimer read. +// Both are KindNull on enter, so we assert enter-presence for each. +func TestIntervalTimerNoop(t *testing.T) { + h := newTestHarness(t) + result, pid, err := h.RunWithIorArgs("interval-timer-noop", defaultDuration, intervalTimerTraceArgs) + if err != nil { + t.Fatalf("run scenario interval-timer-noop: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_setitimer", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_getitimer", Comm: "ioworkload", MinCount: 1}, + }) +} -- cgit v1.2.3