From c3177bd82c16429c1bb246d19af76012479f0c01 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sun, 31 May 2026 19:04:44 +0300 Subject: getxattrat: READ-classify return for xattr-get family consistency getxattrat(2) (Linux 6.13+) returns the xattr value size in bytes, exactly like getxattr/lgetxattr/fgetxattr, but its exit was classified UNCLASSIFIED, so its read bytes were dropped from I/O totals. Classify it as ReadClassified and regenerate the BPF handler (ret_type now READ_CLASSIFIED). Path extraction (args[1], after the dirfd) and the name-not-captured-as-path behaviour were already correct. Update the docs ReadClassified list and the retclassify expectation, and add the first xattr integration coverage: an ioworkload scenario that sets then getxattrat-reads a user xattr on tmpfs, plus a test that asserts enter_getxattrat captures the file path (not the xattr name) and accounts the returned value size as read bytes. Co-Authored-By: Claude Opus 4.8 --- cmd/ioworkload/scenario_xattr.go | 107 +++++++++++++++++++++++++++++++++++++++ cmd/ioworkload/scenarios.go | 1 + 2 files changed, 108 insertions(+) create mode 100644 cmd/ioworkload/scenario_xattr.go (limited to 'cmd') diff --git a/cmd/ioworkload/scenario_xattr.go b/cmd/ioworkload/scenario_xattr.go new file mode 100644 index 0000000..42c679f --- /dev/null +++ b/cmd/ioworkload/scenario_xattr.go @@ -0,0 +1,107 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +// getxattrat is syscall number 464 on amd64 (added in Linux 6.13). Go's +// syscall package does not yet export SYS_GETXATTRAT, so we invoke it by its +// raw number. Its signature is: +// +// getxattrat(int dfd, const char *pathname, unsigned int at_flags, +// const char *name, struct xattr_args *uargs, size_t usize) +// +// The filesystem PATH is at args[1] (after the dirfd), while args[3] ("name") +// is the xattr NAME (e.g. "user.ior") and must NOT be captured as a path. The +// syscall returns the size in bytes of the xattr value (a read byte-count), +// or -1 on error. +const sysGetxattrat = 464 + +// xattrArgs mirrors struct xattr_args from (Linux 6.13+): +// a userspace value buffer pointer plus its size and flags. +type xattrArgs struct { + value uint64 // __aligned_u64: pointer to the value buffer + size uint32 // size of the value buffer + flags uint32 // operation flags (0 for getxattrat) +} + +// xattrGetxattrat creates a file on tmpfs (/tmp), sets a user xattr on it, then +// reads that xattr back via the raw getxattrat(2) syscall with AT_FDCWD. This +// exercises ior's getxattrat 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 READ-classified so the returned value size is +// accounted as read bytes, consistent with getxattr/lgetxattr/fgetxattr. +func xattrGetxattrat() error { + dir, cleanup, err := makeTempDir("xattr-getxattrat") + 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" + value := []byte("getxattrat-value") + if err := syscall.Setxattr(path, xattrName, value, 0); err != nil { + return fmt.Errorf("setxattr: %w", err) + } + + if err := callGetxattrat(path, xattrName, len(value)); err != nil { + return err + } + return nil +} + +// callGetxattrat performs the raw getxattrat(AT_FDCWD, path, 0, name, args, +// sizeof(args)) call and verifies it returns the expected value size. +func callGetxattrat(path, name string, wantSize int) 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) + } + + buf := make([]byte, 256) + args := xattrArgs{ + value: uint64(uintptr(unsafe.Pointer(&buf[0]))), + size: uint32(len(buf)), + flags: 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 + ret, _, errno := syscall.Syscall6( + sysGetxattrat, + uintptr(dirfd), + uintptr(unsafe.Pointer(pathBytes)), + 0, // at_flags + uintptr(unsafe.Pointer(nameBytes)), + uintptr(unsafe.Pointer(&args)), + unsafe.Sizeof(args), + ) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(nameBytes) + runtime.KeepAlive(&buf[0]) + runtime.KeepAlive(&args) + if errno != 0 { + return fmt.Errorf("getxattrat: %w", errno) + } + if int(ret) != wantSize { + return fmt.Errorf("getxattrat returned %d, want %d", int(ret), wantSize) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 3feb76c..b7ea44a 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -98,6 +98,7 @@ var scenarios = map[string]func() error{ "stat-enoent": statEnoent, "stat-access-enoent": statAccessEnoent, "stat-fstat-ebadf": statFstatEbadf, + "xattr-getxattrat": xattrGetxattrat, "utime-basic": utimeBasic, "utime-utimes": utimeUtimes, "utime-enoent": utimeEnoent, -- cgit v1.2.3