diff options
| -rw-r--r-- | cmd/ioworkload/scenario_xattr.go | 107 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 1 | ||||
| -rw-r--r-- | docs/syscall-tracing-plan.md | 2 | ||||
| -rw-r--r-- | integrationtests/xattr_test.go | 37 | ||||
| -rw-r--r-- | internal/c/generated_tracepoints.c | 4 | ||||
| -rw-r--r-- | internal/c/generated_tracepoints_result.txt | 2 | ||||
| -rw-r--r-- | internal/generate/classify.go | 3 | ||||
| -rw-r--r-- | internal/generate/retclassify_test.go | 3 |
8 files changed, 155 insertions, 4 deletions
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/xattr.h> (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, diff --git a/docs/syscall-tracing-plan.md b/docs/syscall-tracing-plan.md index 9d36d8a..8cca6fd 100644 --- a/docs/syscall-tracing-plan.md +++ b/docs/syscall-tracing-plan.md @@ -92,7 +92,7 @@ sudo ./ior -trace-syscalls openat,recvmsg,nanosleep -no-trace-kinds null Payload bytes classified by return value: -- ReadClassified: `fgetxattr`, `flistxattr`, `getdents`, `getdents64`, `getrandom`, `getxattr`, `lgetxattr`, `listxattr`, `llistxattr`, `mq_timedreceive`, `msgrcv`, `pread64`, `preadv`, `preadv2`, `process_vm_readv`, `read`, `readlink`, `readlinkat`, `readv`, `recvfrom`, `recvmsg`, `syslog` +- ReadClassified: `fgetxattr`, `flistxattr`, `getdents`, `getdents64`, `getrandom`, `getxattr`, `getxattrat`, `lgetxattr`, `listxattr`, `llistxattr`, `mq_timedreceive`, `msgrcv`, `pread64`, `preadv`, `preadv2`, `process_vm_readv`, `read`, `readlink`, `readlinkat`, `readv`, `recvfrom`, `recvmsg`, `syslog` - TransferClassified: `copy_file_range`, `sendfile64`, `splice`, `tee`, `vmsplice` - WriteClassified: `mq_timedsend`, `msgsnd`, `process_vm_writev`, `pwrite64`, `pwritev`, `pwritev2`, `sendmsg`, `sendto`, `write`, `writev` diff --git a/integrationtests/xattr_test.go b/integrationtests/xattr_test.go new file mode 100644 index 0000000..d36043c --- /dev/null +++ b/integrationtests/xattr_test.go @@ -0,0 +1,37 @@ +package integrationtests + +import "testing" + +// xattrTraceArgs restricts tracing to the getxattrat tracepoints so the test is +// not perturbed by unrelated xattr/open traffic. +var xattrTraceArgs = []string{"-trace-syscalls", "getxattrat,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 +// captured. The path is read on syscall entry, so enter_getxattrat must carry +// the file path "xattrfile.txt" and never the xattr name "user.ior". +func TestXattrGetxattrat(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-getxattrat", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_getxattrat", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrTraceArgs) + + // The captured path must be the filesystem path, never the xattr name. + for _, rec := range result.Records { + if rec.TraceID.String() == "enter_getxattrat" && rec.Path == "user.ior" { + t.Errorf("getxattrat captured xattr name %q as path instead of file path", rec.Path) + } + } + + // getxattrat returns the xattr value size in bytes; ior READ-classifies the + // exit, so the recorded byte count must reflect the 16-byte value written by + // the workload (consistent with getxattr/lgetxattr/fgetxattr). + exp := ExpectedEvent{Tracepoint: "enter_getxattrat", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, exp, uint64(len("getxattrat-value"))) + assertEventDurationPositive(t, result, exp) +} diff --git a/internal/c/generated_tracepoints.c b/internal/c/generated_tracepoints.c index 5c72813..8c76e7a 100644 --- a/internal/c/generated_tracepoints.c +++ b/internal/c/generated_tracepoints.c @@ -6372,7 +6372,7 @@ int handle_sys_enter_getxattrat(struct syscall_trace_enter *ctx) { return 0; } -/// sys_exit_getxattrat is a struct ret_event (UNCLASSIFIED) (kind=ret) +/// sys_exit_getxattrat is a struct ret_event (READ_CLASSIFIED) (kind=ret) SEC("tracepoint/syscalls/sys_exit_getxattrat") int handle_sys_exit_getxattrat(struct syscall_trace_exit *ctx) { __u32 pid, tid; @@ -6392,7 +6392,7 @@ int handle_sys_exit_getxattrat(struct syscall_trace_exit *ctx) { ev->tid = tid; ev->time = bpf_ktime_get_boot_ns(); ev->ret = ctx->ret; - ev->ret_type = UNCLASSIFIED; + ev->ret_type = READ_CLASSIFIED; bpf_ringbuf_submit(ev, 0); return 0; diff --git a/internal/c/generated_tracepoints_result.txt b/internal/c/generated_tracepoints_result.txt index 971f92c..0842bda 100644 --- a/internal/c/generated_tracepoints_result.txt +++ b/internal/c/generated_tracepoints_result.txt @@ -474,7 +474,7 @@ sys_exit_gettid is a struct ret_event (UNCLASSIFIED) (kind=ret) sys_exit_gettimeofday is a struct ret_event (UNCLASSIFIED) (kind=ret) sys_exit_getuid is a struct ret_event (UNCLASSIFIED) (kind=ret) sys_exit_getxattr is a struct ret_event (READ_CLASSIFIED) (kind=ret) -sys_exit_getxattrat is a struct ret_event (UNCLASSIFIED) (kind=ret) +sys_exit_getxattrat is a struct ret_event (READ_CLASSIFIED) (kind=ret) sys_exit_init_module is a struct null_event (kind=module) sys_exit_inotify_add_watch is a struct ret_event (UNCLASSIFIED) (kind=ret) sys_exit_inotify_init is a struct eventfd_event (kind=eventfd) diff --git a/internal/generate/classify.go b/internal/generate/classify.go index f85cb93..3746bd9 100644 --- a/internal/generate/classify.go +++ b/internal/generate/classify.go @@ -588,6 +588,9 @@ var retClassifications = map[string]RetClassification{ "getdents": ReadClassified, "getdents64": ReadClassified, "getxattr": ReadClassified, + // getxattrat (Linux 6.13+) returns the size in bytes of the xattr value, + // exactly like getxattr/lgetxattr/fgetxattr, so it is a read byte-count. + "getxattrat": ReadClassified, "lgetxattr": ReadClassified, "listxattr": ReadClassified, "llistxattr": ReadClassified, diff --git a/internal/generate/retclassify_test.go b/internal/generate/retclassify_test.go index 25c5e71..acd019b 100644 --- a/internal/generate/retclassify_test.go +++ b/internal/generate/retclassify_test.go @@ -5,6 +5,9 @@ import "testing" func TestClassifyRetRead(t *testing.T) { reads := []string{ "fgetxattr", "flistxattr", "getdents", "getdents64", "getxattr", + // getxattrat (Linux 6.13+) returns the xattr value size in bytes, the + // same read byte-count as getxattr/lgetxattr/fgetxattr. + "getxattrat", "lgetxattr", "listxattr", "llistxattr", "pread64", "preadv", "preadv2", "process_vm_readv", "read", "readlink", "readlinkat", "readv", "recvmsg", "recvfrom", "syslog", "mq_timedreceive", "getrandom", "msgrcv", |
