diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 09:16:11 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 09:16:11 +0300 |
| commit | e44475525894cb5a184e7fecd449c258f204f189 (patch) | |
| tree | cf2c3d69e1671c80f215d56cf2fa69ab1997e85d /cmd | |
| parent | fc4d0a4332ba72d9ecbb46b1233bd39fac80812f (diff) | |
test(chmod): add end-to-end coverage for chmod/fchmod/fchmodat family
chmod/fchmod/fchmodat/fchmodat2 previously had no integration coverage:
no scenario in cmd/ioworkload/ and no test in integrationtests/. All four
are correctly classified (chmod KindPathname args[0]; fchmodat/fchmodat2
KindPathname args[1]; fchmod KindFd args[0]; all FamilyFS, all UNCLASSIFIED
ret) but nothing exercised them end-to-end.
Add a chmod-basic scenario that, on a temp file the caller owns (so all
calls are unprivileged), issues raw chmod(path, 0640), fchmodat(AT_FDCWD,
path, 0644, 0), fchmod(fd, 0644), and best-effort fchmodat2(AT_FDCWD, path,
0640, 0) (raw syscall 452, ENOSYS tolerated on kernels < 6.6). Raw syscalls
are used so each distinct tracepoint fires rather than glibc redirecting
chmod to fchmodat.
TestChmodBasic asserts enter_chmod and enter_fchmodat capture the file path
(chmodfile.txt) and enter_fchmod fires (KindFd, no path). fchmodat2 is not
asserted since it is version-gated, though it does fire on current kernels.
Mirrors the utime coverage (scenario_utime.go / utime_test.go).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ioworkload/scenario_chmod.go | 144 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 |
2 files changed, 145 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_chmod.go b/cmd/ioworkload/scenario_chmod.go new file mode 100644 index 0000000..18b7ac7 --- /dev/null +++ b/cmd/ioworkload/scenario_chmod.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +// sysFchmodat2 is syscall number 452 on amd64 (added in Linux 6.6). Go's +// syscall package does not export SYS_FCHMODAT2, so we invoke it by its raw +// number. Its signature is: +// +// fchmodat2(int dfd, const char *pathname, mode_t mode, unsigned int flags) +// +// Like fchmodat, the filesystem PATH is at args[1] (after the dirfd), so ior +// must capture it as a path event (KindPathname) and tag it FamilyFS. fchmodat2 +// adds an explicit flags argument (e.g. AT_SYMLINK_NOFOLLOW) that plain +// fchmodat lacks. On kernels older than 6.6 the syscall returns ENOSYS, so the +// caller treats it as best-effort and only asserts the older siblings. +const sysFchmodat2 = 452 + +// chmodBasic drives the chmod permission-change family end-to-end on a file the +// caller owns, so every call is UNPRIVILEGED. It exercises, in order: +// +// - chmod(path, mode) — path at args[0], KindPathname, FamilyFS +// - fchmodat(AT_FDCWD, path, …) — path at args[1], KindPathname, FamilyFS +// - fchmod(fd, mode) — fd at args[0], KindFd, FamilyFS +// - fchmodat2(AT_FDCWD, path, …) — path at args[1], KindPathname (best-effort) +// +// We use raw syscalls (rather than syscall.Chmod etc.) so each distinct +// tracepoint actually fires; glibc/Go wrappers can redirect chmod to fchmodat +// and hide the syscall under test. The modes are harmless (0644 → 0640 → 0644), +// so nothing destructive happens, and the temp file is cleaned up. +func chmodBasic() error { + dir, cleanup, err := makeTempDir("chmod-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "chmodfile.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) + + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + + if err := callChmod(pathBytes); err != nil { + return err + } + if err := callFchmodat(pathBytes); err != nil { + return err + } + if err := callFchmod(fd); err != nil { + return err + } + // fchmodat2 is best-effort: ENOSYS on kernels < 6.6 is tolerated. + if err := callFchmodat2(pathBytes); err != nil { + return err + } + return nil +} + +// callChmod issues raw chmod(path, 0640). chmod takes the filesystem path at +// args[0], so ior captures it as a KindPathname event under enter_chmod. +func callChmod(pathBytes *byte) error { + _, _, errno := syscall.Syscall( + syscall.SYS_CHMOD, + uintptr(unsafe.Pointer(pathBytes)), + uintptr(0o640), + 0, + ) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("chmod: %w", errno) + } + return nil +} + +// callFchmodat issues raw fchmodat(AT_FDCWD, path, 0644, 0). The path is at +// args[1] (after the dirfd), so ior captures it as a KindPathname event under +// enter_fchmodat. A runtime int holds AT_FDCWD so the negative value survives +// the uintptr conversion instead of overflowing. +func callFchmodat(pathBytes *byte) error { + dirfd := _AT_FDCWD + _, _, errno := syscall.Syscall6( + syscall.SYS_FCHMODAT, + uintptr(dirfd), + uintptr(unsafe.Pointer(pathBytes)), + uintptr(0o644), + 0, // flags + 0, + 0, + ) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("fchmodat: %w", errno) + } + return nil +} + +// callFchmod issues raw fchmod(fd, 0644). fchmod operates on an open fd at +// args[0] (KindFd) and carries no path, so ior records enter_fchmod keyed by +// the descriptor rather than a filename. +func callFchmod(fd int) error { + _, _, errno := syscall.Syscall( + syscall.SYS_FCHMOD, + uintptr(fd), + uintptr(0o644), + 0, + ) + if errno != 0 { + return fmt.Errorf("fchmod: %w", errno) + } + return nil +} + +// callFchmodat2 issues raw fchmodat2(AT_FDCWD, path, 0640, 0). The path is at +// args[1], so it is KindPathname like fchmodat. fchmodat2 is best-effort: on +// kernels older than 6.6 it returns ENOSYS, which we tolerate rather than fail. +func callFchmodat2(pathBytes *byte) error { + dirfd := _AT_FDCWD + _, _, errno := syscall.Syscall6( + sysFchmodat2, + uintptr(dirfd), + uintptr(unsafe.Pointer(pathBytes)), + uintptr(0o640), + 0, // flags + 0, + 0, + ) + runtime.KeepAlive(pathBytes) + if errno != 0 && errno != syscall.ENOSYS { + return fmt.Errorf("fchmodat2: %w", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 8be86db..c234f78 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -110,6 +110,7 @@ var scenarios = map[string]func() error{ "xattr-getxattrat": xattrGetxattrat, "xattr-listxattrat": xattrListxattrat, "xattr-removexattrat": xattrRemovexattrat, + "chmod-basic": chmodBasic, "utime-basic": utimeBasic, "utime-utimes": utimeUtimes, "utime-enoent": utimeEnoent, |
