summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 10:08:54 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 10:08:54 +0300
commit3ce0f52a9f608b28c550083574fa3ef442107f53 (patch)
treed3ae33124129b7c987330db651f28565ee7ebb5b
parent92ca9482e44432b85ce09ebdd8a1b4d199b1c77b (diff)
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 <noreply@anthropic.com>
-rw-r--r--cmd/ioworkload/scenario_stat.go37
-rw-r--r--cmd/ioworkload/scenario_timer.go53
-rw-r--r--cmd/ioworkload/scenarios.go2
-rw-r--r--integrationtests/stat_test.go19
-rw-r--r--integrationtests/timer_test.go29
5 files changed, 140 insertions, 0 deletions
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 <sys/time.h> (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},
+ })
+}