diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 09:43:50 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 09:43:50 +0300 |
| commit | 4292b4ef116ec72b66f3c19f8a9a00458d441b79 (patch) | |
| tree | ef88511e8261174cda9dbc44ead2f82f6aace967 /cmd | |
| parent | 494c277bd4937c0c8a4fce4f53401053036925b1 (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')
| -rw-r--r-- | cmd/ioworkload/scenario_utime.go | 104 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 2 |
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(×[0])), + ) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(×) + 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(×[0])), + 0, // flags + 0, 0, + ) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(×) + 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, |
