diff options
| -rw-r--r-- | cmd/ioworkload/scenario_xattr.go | 80 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 | ||||
| -rw-r--r-- | integrationtests/xattr_test.go | 37 |
3 files changed, 118 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, diff --git a/integrationtests/xattr_test.go b/integrationtests/xattr_test.go index 16efb4a..f99629d 100644 --- a/integrationtests/xattr_test.go +++ b/integrationtests/xattr_test.go @@ -10,6 +10,10 @@ var xattrTraceArgs = []string{"-trace-syscalls", "getxattrat,setxattr,openat"} // test is not perturbed by unrelated xattr/open traffic. var xattrListTraceArgs = []string{"-trace-syscalls", "listxattrat,setxattr,openat"} +// xattrRemoveTraceArgs restricts tracing to the removexattrat tracepoints so +// the test is not perturbed by unrelated xattr/open traffic. +var xattrRemoveTraceArgs = []string{"-trace-syscalls", "removexattrat,setxattr,openat"} + // TestXattrGetxattrat verifies ior traces getxattrat(2) (Linux 6.13+) // end-to-end. getxattrat takes a dirfd plus a real filesystem path at args[1] // (NOT args[0]=dfd) and an xattr NAME at args[3]; only the path must be @@ -100,3 +104,36 @@ func TestXattrListxattrat(t *testing.T) { assertEventBytesAtLeast(t, result, exp, uint64(len("user.ior")+1)) assertEventDurationPositive(t, result, exp) } + +// TestXattrRemovexattrat verifies ior traces removexattrat(2) (Linux 6.13+) +// end-to-end. removexattrat takes a dirfd plus a real filesystem path at +// args[1] (NOT args[0]=dfd); the xattr name is at args[3] and must NOT be +// captured. The path is read on syscall entry, so enter_removexattrat must +// carry the file path "xattrfile.txt", never the name "user.ior". +func TestXattrRemovexattrat(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-removexattrat", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_removexattrat", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrRemoveTraceArgs) + + // The captured path must be the filesystem path, never the xattr name. + for _, rec := range result.Records { + if rec.TraceID.String() == "enter_removexattrat" && rec.Path == "user.ior" { + t.Errorf("removexattrat captured xattr name %q as path instead of file path", rec.Path) + } + } + + // removexattrat is UNCLASSIFIED: it REMOVES an attribute and returns a 0/-1 + // status, never a byte count (contrast getxattrat/listxattrat, which return + // value/name-list sizes). The accounted bytes must therefore be exactly + // zero — matching removexattr/lremovexattr/fremovexattr, and guarding + // against wrongly READ-classifying it like its getxattrat/listxattrat + // siblings. + exp := ExpectedEvent{Tracepoint: "enter_removexattrat", Comm: "ioworkload"} + assertEventBytesEqual(t, result, exp, 0) + assertEventDurationPositive(t, result, exp) +} |
