diff options
| -rw-r--r-- | cmd/ioworkload/scenario_utime.go | 104 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 2 | ||||
| -rw-r--r-- | integrationtests/utime_test.go | 30 |
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(×[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, 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. |
