summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/ioworkload/scenario_utime.go104
-rw-r--r--cmd/ioworkload/scenarios.go2
-rw-r--r--integrationtests/utime_test.go30
3 files changed, 136 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,
diff --git a/integrationtests/utime_test.go b/integrationtests/utime_test.go
index b1e6fdc..7bedcba 100644
--- a/integrationtests/utime_test.go
+++ b/integrationtests/utime_test.go
@@ -30,6 +30,36 @@ func TestUtimeUtimes(t *testing.T) {
})
}
+// TestUtimeFutimesat verifies the dirfd-relative sibling futimesat(2) is
+// path-classified and its filename captured from args[1] (after the dirfd at
+// args[0]). The scenario passes AT_FDCWD as the dirfd, so a hit on the file
+// name proves ior reads the path from the second arg, not the first.
+func TestUtimeFutimesat(t *testing.T) {
+ runScenario(t, "utime-futimesat", []ExpectedEvent{
+ {
+ PathContains: "futimesatfile.txt",
+ Tracepoint: "enter_futimesat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
+// TestUtimeUtimensat verifies the nanosecond-resolution sibling utimensat(2) is
+// path-classified and its filename captured from args[1] (after the dirfd at
+// args[0]). As with futimesat, AT_FDCWD is passed as the dirfd, so matching the
+// file name confirms the path is read from the second arg.
+func TestUtimeUtimensat(t *testing.T) {
+ runScenario(t, "utime-utimensat", []ExpectedEvent{
+ {
+ PathContains: "utimensatfile.txt",
+ Tracepoint: "enter_utimensat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
// TestUtimeEnoent verifies the path is still captured on the error path:
// utime(2) on a missing file fails with ENOENT, but ior records enter_utime
// because the filename is read on syscall entry.