summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 09:16:11 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 09:16:11 +0300
commite44475525894cb5a184e7fecd449c258f204f189 (patch)
treecf2c3d69e1671c80f215d56cf2fa69ab1997e85d /cmd
parentfc4d0a4332ba72d9ecbb46b1233bd39fac80812f (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.go144
-rw-r--r--cmd/ioworkload/scenarios.go1
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,