summaryrefslogtreecommitdiff
path: root/cmd/ioworkload
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-02 10:10:45 +0300
committerPaul Buetow <paul@buetow.org>2026-06-02 10:10:45 +0300
commit0cfdd29db2f9cae372617ef01ed4aecc2f6fa93d (patch)
tree6c030a22da20a85f813e2f52fc193b924deaf0dd /cmd/ioworkload
parent88769d471981911242dda27ed75d92866b2acf05 (diff)
test(xattr): add removexattrat end-to-end integration coverage
removexattrat(2) (Linux 6.13+) was the one xattr *at-variant lacking integration coverage: xattr_test.go exercised getxattrat/listxattrat (READ-classified byte counts) and the path-based setxattr, but never the REMOVE *at variant. Unlike its getxattrat/listxattrat siblings, removexattrat returns a 0/-1 status (not a byte count), so its exit must be UNCLASSIFIED — matching removexattr/lremovexattr/fremovexattr. Add an ioworkload scenario (xattr-removexattrat) that setxattr's a user.* attribute via a real path then removes it via raw removexattrat(AT_FDCWD, path, 0, name), plus TestXattrRemovexattrat asserting the path (args[1], after the dirfd) is captured (never the xattr name at args[3]) and that accounted bytes are exactly zero (guarding against wrongly READ-classifying it like getxattrat/ listxattrat). Distinct from the fd-based fremovexattr gap (task 8i0). Classification verified by inspection: FamilyFS (xattr marker), KindPathname at the pathname field (ctx->args[1]), and absent from the ret-classification table => UNCLASSIFIED. No generator change needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd/ioworkload')
-rw-r--r--cmd/ioworkload/scenario_xattr.go80
-rw-r--r--cmd/ioworkload/scenarios.go1
2 files changed, 81 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_xattr.go b/cmd/ioworkload/scenario_xattr.go
index f974c01..ef2e377 100644
--- a/cmd/ioworkload/scenario_xattr.go
+++ b/cmd/ioworkload/scenario_xattr.go
@@ -34,6 +34,20 @@ const sysGetxattrat = 464
// error — exactly like listxattr/llistxattr/flistxattr.
const sysListxattrat = 465
+// removexattrat is syscall number 466 on amd64 (added in Linux 6.13, right
+// after listxattrat). Go's syscall package does not export SYS_REMOVEXATTRAT,
+// so we invoke it by its raw number. Its signature is:
+//
+// removexattrat(int dfd, const char *pathname, unsigned int at_flags,
+// const char *name)
+//
+// The filesystem PATH is at args[1] (after the dirfd); args[3] is the xattr
+// NAME (e.g. "user.ior") and must NOT be captured as a path. Unlike
+// getxattrat/listxattrat, removexattrat REMOVES an attribute and returns 0 on
+// success / -1 on error (a status, NOT a read byte-count) — so its exit is
+// UNCLASSIFIED, exactly like removexattr/lremovexattr/fremovexattr.
+const sysRemovexattrat = 466
+
// xattrArgs mirrors struct xattr_args from <linux/xattr.h> (Linux 6.13+):
// a userspace value buffer pointer plus its size and flags.
type xattrArgs struct {
@@ -181,3 +195,69 @@ func callListxattrat(path string, wantMinSize int) error {
}
return nil
}
+
+// xattrRemovexattrat creates a file on tmpfs (/tmp), sets a user xattr on it,
+// then removes that xattr via the raw removexattrat(2) syscall with AT_FDCWD.
+// This exercises ior's removexattrat tracing end-to-end and confirms:
+// - the real filesystem path (args[1]) is captured, NOT the dirfd or the
+// xattr name string at args[3];
+// - the syscall exit is UNCLASSIFIED — removexattrat returns a 0/-1 status,
+// never a byte count, so no read bytes are attributed (contrast
+// getxattrat/listxattrat). This matches removexattr/lremovexattr/
+// fremovexattr.
+func xattrRemovexattrat() error {
+ dir, cleanup, err := makeTempDir("xattr-removexattrat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "xattrfile.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)
+
+ const xattrName = "user.ior"
+ if err := syscall.Setxattr(path, xattrName, []byte("removexattrat-value"), 0); err != nil {
+ return fmt.Errorf("setxattr: %w", err)
+ }
+
+ return callRemovexattrat(path, xattrName)
+}
+
+// callRemovexattrat performs the raw removexattrat(AT_FDCWD, path, 0, name)
+// call and verifies it succeeds (returns 0).
+func callRemovexattrat(path, name string) error {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return fmt.Errorf("name bytes: %w", err)
+ }
+
+ // Use a runtime int variable so the negative AT_FDCWD survives the uintptr
+ // conversion: converting the negative constant directly overflows uintptr.
+ dirfd := _AT_FDCWD
+ ret, _, errno := syscall.Syscall6(
+ sysRemovexattrat,
+ uintptr(dirfd),
+ uintptr(unsafe.Pointer(pathBytes)),
+ 0, // at_flags
+ uintptr(unsafe.Pointer(nameBytes)),
+ 0,
+ 0,
+ )
+ runtime.KeepAlive(pathBytes)
+ runtime.KeepAlive(nameBytes)
+ if errno != 0 {
+ return fmt.Errorf("removexattrat: %w", errno)
+ }
+ if int(ret) != 0 {
+ return fmt.Errorf("removexattrat returned %d, want 0", int(ret))
+ }
+ return nil
+}
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index 4fd2dd0..690386b 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -107,6 +107,7 @@ var scenarios = map[string]func() error{
"stat-fstat-ebadf": statFstatEbadf,
"xattr-getxattrat": xattrGetxattrat,
"xattr-listxattrat": xattrListxattrat,
+ "xattr-removexattrat": xattrRemovexattrat,
"utime-basic": utimeBasic,
"utime-utimes": utimeUtimes,
"utime-enoent": utimeEnoent,