summaryrefslogtreecommitdiff
path: root/cmd/ioworkload
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 09:43:50 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 09:43:50 +0300
commit4292b4ef116ec72b66f3c19f8a9a00458d441b79 (patch)
treeef88511e8261174cda9dbc44ead2f82f6aace967 /cmd/ioworkload
parent494c277bd4937c0c8a4fce4f53401053036925b1 (diff)
test(utime): add end-to-end coverage for futimesat and utimensat
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>
Diffstat (limited to 'cmd/ioworkload')
-rw-r--r--cmd/ioworkload/scenario_utime.go104
-rw-r--r--cmd/ioworkload/scenarios.go2
2 files changed, 106 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_utime.go b/cmd/ioworkload/scenario_utime.go
index 3f86a54..e866a0a 100644
--- a/cmd/ioworkload/scenario_utime.go
+++ b/cmd/ioworkload/scenario_utime.go
@@ -100,6 +100,110 @@ func utimeUtimes() error {
return nil
}
+// timespec mirrors struct timespec used by utimensat(2): a {tv_sec, tv_nsec}
+// pair. utimensat takes a 2-element array for the new access and modification
+// times.
+type timespec struct {
+ tvSec int64
+ tvNsec int64
+}
+
+// utimeFutimesat creates a file and changes its timestamps via raw
+// SYS_FUTIMESAT. futimesat(2) takes a dirfd at args[0] and a pathname at
+// args[1] ("filename"), so ior must capture the path from args[1] (after the
+// dirfd), classify it as a path event (KindPathname), and tag it FamilyFS like
+// its siblings utime/utimes/utimensat. We pass AT_FDCWD as the dirfd so the
+// absolute path resolves relative to the cwd, and a 2-element timeval array to
+// set explicit times. The raw syscall guarantees the exact enter_futimesat
+// tracepoint fires.
+func utimeFutimesat() error {
+ dir, cleanup, err := makeTempDir("utime-futimesat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "futimesatfile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ syscall.Close(fd)
+
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ times := [2]timeval{
+ {tvSec: 1000000000, tvUsec: 0},
+ {tvSec: 1000000000, tvUsec: 0},
+ }
+ // Use a runtime int variable so the negative AT_FDCWD survives the uintptr
+ // conversion: converting the negative constant directly overflows uintptr.
+ dirfd := _AT_FDCWD
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_FUTIMESAT,
+ uintptr(dirfd),
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(&times[0])),
+ )
+ runtime.KeepAlive(pathBytes)
+ runtime.KeepAlive(&times)
+ if errno != 0 {
+ return fmt.Errorf("futimesat: %w", errno)
+ }
+ return nil
+}
+
+// utimeUtimensat creates a file and changes its timestamps via raw
+// SYS_UTIMENSAT. utimensat(2) is the nanosecond-resolution sibling: it takes a
+// dirfd at args[0] and a pathname at args[1] ("filename"), so the path must be
+// captured from args[1] (after the dirfd) and is path-classified and FamilyFS.
+// We pass AT_FDCWD as the dirfd, a 2-element timespec array for the times, and
+// 0 for flags. The raw syscall guarantees the exact enter_utimensat tracepoint
+// fires (Go's os.Chtimes also wraps utimensat, but going raw keeps the dirfd
+// and arg layout explicit).
+func utimeUtimensat() error {
+ dir, cleanup, err := makeTempDir("utime-utimensat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "utimensatfile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ syscall.Close(fd)
+
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ times := [2]timespec{
+ {tvSec: 1000000000, tvNsec: 0},
+ {tvSec: 1000000000, tvNsec: 0},
+ }
+ // Use a runtime int variable so the negative AT_FDCWD survives the uintptr
+ // conversion: converting the negative constant directly overflows uintptr.
+ dirfd := _AT_FDCWD
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_UTIMENSAT,
+ uintptr(dirfd),
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(&times[0])),
+ 0, // flags
+ 0, 0,
+ )
+ runtime.KeepAlive(pathBytes)
+ runtime.KeepAlive(&times)
+ if errno != 0 {
+ return fmt.Errorf("utimensat: %w", errno)
+ }
+ return nil
+}
+
// utimeEnoent calls raw SYS_UTIME on a nonexistent path. The syscall fails
// with ENOENT, but ior still captures the enter_utime tracepoint because the
// filename path is read on syscall entry. This locks in that the path is
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index b1f8cf6..811127a 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -123,6 +123,8 @@ var scenarios = map[string]func() error{
"chown-basic": chownBasic,
"utime-basic": utimeBasic,
"utime-utimes": utimeUtimes,
+ "utime-futimesat": utimeFutimesat,
+ "utime-utimensat": utimeUtimensat,
"utime-enoent": utimeEnoent,
"sync-basic": syncBasic,
"sync-fdatasync": syncFdatasync,