diff options
| -rw-r--r-- | cmd/ioworkload/scenario_xattr.go | 313 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 9 | ||||
| -rw-r--r-- | integrationtests/xattr_test.go | 240 |
3 files changed, 562 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_xattr.go b/cmd/ioworkload/scenario_xattr.go index ef2e377..aa33d60 100644 --- a/cmd/ioworkload/scenario_xattr.go +++ b/cmd/ioworkload/scenario_xattr.go @@ -6,6 +6,8 @@ import ( "runtime" "syscall" "unsafe" + + "golang.org/x/sys/unix" ) // getxattrat is syscall number 464 on amd64 (added in Linux 6.13). Go's @@ -48,6 +50,21 @@ const sysListxattrat = 465 // UNCLASSIFIED, exactly like removexattr/lremovexattr/fremovexattr. const sysRemovexattrat = 466 +// setxattrat is syscall number 463 on amd64 (added in Linux 6.13, the first of +// the xattr -at quartet). Go's syscall package does not export SYS_SETXATTRAT, +// so we invoke it by its raw number. Its signature is: +// +// setxattrat(int dfd, const char *pathname, unsigned int at_flags, +// const char *name, const struct xattr_args *uargs, size_t usize) +// +// The filesystem PATH is at args[1] (after the dirfd); args[3] is the xattr +// NAME and must NOT be captured as a path. Unlike getxattrat/listxattrat, +// setxattrat SETS an attribute and returns 0 on success / -1 on error (a +// status, NOT a read byte-count — the value size is an INPUT field of +// xattr_args) — so its exit is UNCLASSIFIED, exactly like +// setxattr/lsetxattr/fsetxattr. +const sysSetxattrat = 463 + // 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 { @@ -261,3 +278,299 @@ func callRemovexattrat(path, name string) error { } return nil } + +// xattrName is the user-namespace attribute name used by every xattr scenario. +// User xattrs must live under the "user." prefix and are only permitted on +// regular files/dirs on a supporting filesystem (tmpfs satisfies this). +const xattrName = "user.ior" + +// makeXattrFile creates an empty regular file under a fresh temp dir and sets a +// user.* xattr on it so subsequent get/list/remove syscalls have something to +// operate on. It returns the file path, the value that was set (so callers can +// assert the READ byte count), and the dir cleanup func. +func makeXattrFile(prefix string, value []byte) (string, func(), error) { + dir, cleanup, err := makeTempDir(prefix) + if err != nil { + return "", nil, err + } + path := filepath.Join(dir, "xattrfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + cleanup() + return "", nil, fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + if err := syscall.Setxattr(path, xattrName, value, 0); err != nil { + cleanup() + return "", nil, fmt.Errorf("setxattr: %w", err) + } + return path, cleanup, nil +} + +// xattrGetxattr creates a file, sets a user xattr on it, then reads that xattr +// back via the path-based getxattr(2) syscall (which FOLLOWS symlinks; here the +// path is a regular file). golang.org/x/sys/unix.Getxattr issues the raw +// syscall directly (no glibc redirection), so the enter_getxattr tracepoint +// fires with the file path at args[0] and the exit is READ-classified with the +// returned value size as the byte count, consistent with +// lgetxattr/fgetxattr/getxattrat. +func xattrGetxattr() error { + value := []byte("getxattr-value") + path, cleanup, err := makeXattrFile("xattr-getxattr", value) + if err != nil { + return err + } + defer cleanup() + + buf := make([]byte, 256) + n, err := unix.Getxattr(path, xattrName, buf) + if err != nil { + return fmt.Errorf("getxattr: %w", err) + } + if n != len(value) { + return fmt.Errorf("getxattr returned %d, want %d", n, len(value)) + } + return nil +} + +// xattrLgetxattr is the no-follow counterpart of xattrGetxattr. lgetxattr(2) +// does NOT follow symlinks, but since the target path is a regular file (not a +// symlink) it behaves identically to getxattr and returns the value size — this +// is the deterministic way to fire the tracepoint, because user.* xattrs on a +// symlink itself are kernel-restricted (EPERM). The enter_lgetxattr tracepoint +// captures the file path at args[0]; the exit is READ-classified. +func xattrLgetxattr() error { + value := []byte("lgetxattr-value") + path, cleanup, err := makeXattrFile("xattr-lgetxattr", value) + if err != nil { + return err + } + defer cleanup() + + buf := make([]byte, 256) + n, err := unix.Lgetxattr(path, xattrName, buf) + if err != nil { + return fmt.Errorf("lgetxattr: %w", err) + } + if n != len(value) { + return fmt.Errorf("lgetxattr returned %d, want %d", n, len(value)) + } + return nil +} + +// xattrListxattr creates a file, sets a user xattr on it, then lists the file's +// xattr names via the path-based listxattr(2) syscall. enter_listxattr captures +// the file path at args[0]; the exit is READ-classified with the NUL-separated +// name-list byte count (at least len("user.ior")+1), consistent with +// llistxattr/flistxattr/listxattrat. +func xattrListxattr() error { + path, cleanup, err := makeXattrFile("xattr-listxattr", []byte("listxattr-value")) + if err != nil { + return err + } + defer cleanup() + + buf := make([]byte, 256) + n, err := unix.Listxattr(path, buf) + if err != nil { + return fmt.Errorf("listxattr: %w", err) + } + if n < len(xattrName)+1 { + return fmt.Errorf("listxattr returned %d, want at least %d", n, len(xattrName)+1) + } + return nil +} + +// xattrLlistxattr is the no-follow counterpart of xattrListxattr. As with +// lgetxattr, the target is a regular file so llistxattr(2) returns the name-list +// size deterministically (user.* on a bare symlink is restricted). +// enter_llistxattr captures the file path; the exit is READ-classified. +func xattrLlistxattr() error { + path, cleanup, err := makeXattrFile("xattr-llistxattr", []byte("llistxattr-value")) + if err != nil { + return err + } + defer cleanup() + + buf := make([]byte, 256) + n, err := unix.Llistxattr(path, buf) + if err != nil { + return fmt.Errorf("llistxattr: %w", err) + } + if n < len(xattrName)+1 { + return fmt.Errorf("llistxattr returned %d, want at least %d", n, len(xattrName)+1) + } + return nil +} + +// xattrLsetxattr creates a file, sets an initial xattr, then updates it via the +// no-follow path-based lsetxattr(2) syscall. As with the other l* variants the +// target is a regular file, so lsetxattr succeeds (user.* on a bare symlink is +// restricted). enter_lsetxattr captures the file path at args[0]; the exit is +// UNCLASSIFIED — lsetxattr returns 0/-1, never a byte count (its `size` arg is +// the INPUT value length), exactly like setxattr/setxattrat/fsetxattr. +func xattrLsetxattr() error { + path, cleanup, err := makeXattrFile("xattr-lsetxattr", []byte("lsetxattr-initial")) + if err != nil { + return err + } + defer cleanup() + + if err := unix.Lsetxattr(path, xattrName, []byte("lsetxattr-updated"), 0); err != nil { + return fmt.Errorf("lsetxattr: %w", err) + } + return nil +} + +// xattrSetxattrat creates a file then sets a user xattr on it via the raw +// setxattrat(2) syscall with AT_FDCWD. This exercises ior's setxattrat tracing +// end-to-end and confirms the real filesystem path (args[1]) is captured (NOT +// the dirfd or the xattr name at args[3]) and that the exit is UNCLASSIFIED +// (0/-1 status, never a byte count), matching setxattr/lsetxattr/fsetxattr. +// setxattrat is Linux 6.13+; if the kernel lacks it (ENOSYS) the scenario +// reports the error so the caller can treat it as best-effort. +func xattrSetxattrat() error { + dir, cleanup, err := makeTempDir("xattr-setxattrat") + 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) + + return callSetxattrat(path, xattrName, []byte("setxattrat-value")) +} + +// callSetxattrat performs the raw setxattrat(AT_FDCWD, path, 0, name, args, +// sizeof(args)) call. The value to write is passed via struct xattr_args (its +// `size` field is the value length); the syscall returns 0 on success / -1 on +// error — never a byte count. +func callSetxattrat(path, name string, value []byte) 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) + } + + args := xattrArgs{ + value: uint64(uintptr(unsafe.Pointer(&value[0]))), + size: uint32(len(value)), + 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( + sysSetxattrat, + 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(&value[0]) + runtime.KeepAlive(&args) + if errno != 0 { + return fmt.Errorf("setxattrat: %w", errno) + } + if int(ret) != 0 { + return fmt.Errorf("setxattrat returned %d, want 0", int(ret)) + } + return nil +} + +// xattrRemovexattr creates a file, sets a user xattr on it, then removes it via +// the path-based removexattr(2) syscall. enter_removexattr captures the file +// path at args[0] (NOT the xattr name at args[1]); the exit is UNCLASSIFIED — +// removexattr returns 0/-1, never a byte count, matching +// lremovexattr/fremovexattr/removexattrat. +func xattrRemovexattr() error { + path, cleanup, err := makeXattrFile("xattr-removexattr", []byte("removexattr-value")) + if err != nil { + return err + } + defer cleanup() + + if err := unix.Removexattr(path, xattrName); err != nil { + return fmt.Errorf("removexattr: %w", err) + } + return nil +} + +// xattrLremovexattr is the no-follow counterpart of xattrRemovexattr. The target +// is a regular file, so lremovexattr(2) removes the user.* attr deterministically +// (user.* on a bare symlink is restricted). enter_lremovexattr captures the file +// path at args[0]; the exit is UNCLASSIFIED. +func xattrLremovexattr() error { + path, cleanup, err := makeXattrFile("xattr-lremovexattr", []byte("lremovexattr-value")) + if err != nil { + return err + } + defer cleanup() + + if err := unix.Lremovexattr(path, xattrName); err != nil { + return fmt.Errorf("lremovexattr: %w", err) + } + return nil +} + +// xattrFd exercises the entire fd-based xattr family on an open file descriptor: +// fsetxattr (set), fgetxattr (READ), flistxattr (READ), then fremovexattr +// (remove). golang.org/x/sys/unix issues the raw syscalls directly, so each +// tracepoint fires with the FD at args[0] (kind=fd, no path). fgetxattr and +// flistxattr are READ-classified (return value/name-list sizes); fsetxattr and +// fremovexattr are UNCLASSIFIED (return 0/-1 status). +func xattrFd() error { + dir, cleanup, err := makeTempDir("xattr-fd") + 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) + } + defer syscall.Close(fd) + + value := []byte("fxattr-value") + if err := unix.Fsetxattr(fd, xattrName, value, 0); err != nil { + return fmt.Errorf("fsetxattr: %w", err) + } + + buf := make([]byte, 256) + n, err := unix.Fgetxattr(fd, xattrName, buf) + if err != nil { + return fmt.Errorf("fgetxattr: %w", err) + } + if n != len(value) { + return fmt.Errorf("fgetxattr returned %d, want %d", n, len(value)) + } + + list := make([]byte, 256) + ln, err := unix.Flistxattr(fd, list) + if err != nil { + return fmt.Errorf("flistxattr: %w", err) + } + if ln < len(xattrName)+1 { + return fmt.Errorf("flistxattr returned %d, want at least %d", ln, len(xattrName)+1) + } + + if err := unix.Fremovexattr(fd, xattrName); err != nil { + return fmt.Errorf("fremovexattr: %w", err) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 7e03b3d..b1f8cf6 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -110,6 +110,15 @@ var scenarios = map[string]func() error{ "xattr-getxattrat": xattrGetxattrat, "xattr-listxattrat": xattrListxattrat, "xattr-removexattrat": xattrRemovexattrat, + "xattr-getxattr": xattrGetxattr, + "xattr-lgetxattr": xattrLgetxattr, + "xattr-listxattr": xattrListxattr, + "xattr-llistxattr": xattrLlistxattr, + "xattr-lsetxattr": xattrLsetxattr, + "xattr-setxattrat": xattrSetxattrat, + "xattr-removexattr": xattrRemovexattr, + "xattr-lremovexattr": xattrLremovexattr, + "xattr-fd": xattrFd, "chmod-basic": chmodBasic, "chown-basic": chownBasic, "utime-basic": utimeBasic, diff --git a/integrationtests/xattr_test.go b/integrationtests/xattr_test.go index f99629d..73e74c7 100644 --- a/integrationtests/xattr_test.go +++ b/integrationtests/xattr_test.go @@ -14,6 +14,25 @@ var xattrListTraceArgs = []string{"-trace-syscalls", "listxattrat,setxattr,opena // the test is not perturbed by unrelated xattr/open traffic. var xattrRemoveTraceArgs = []string{"-trace-syscalls", "removexattrat,setxattr,openat"} +// The trace selectors below scope tracing to exactly the path/fd-based xattr +// variant under test (plus setxattr/openat used to prime the attribute), so the +// substring-based assertion matcher cannot pick up sibling tracepoints. The +// syscall filter is exact-match, so e.g. tracing "getxattr" does NOT enable +// lgetxattr/fgetxattr/getxattrat. +var ( + xattrGetTraceArgs = []string{"-trace-syscalls", "getxattr,setxattr,openat"} + xattrLgetTraceArgs = []string{"-trace-syscalls", "lgetxattr,setxattr,openat"} + xattrListPathTraceArgs = []string{"-trace-syscalls", "listxattr,setxattr,openat"} + xattrLlistTraceArgs = []string{"-trace-syscalls", "llistxattr,setxattr,openat"} + xattrLsetTraceArgs = []string{"-trace-syscalls", "lsetxattr,setxattr,openat"} + xattrSetatTraceArgs = []string{"-trace-syscalls", "setxattrat,setxattr,openat"} + xattrRemovePathTraceArgs = []string{"-trace-syscalls", "removexattr,setxattr,openat"} + xattrLremoveTraceArgs = []string{"-trace-syscalls", "lremovexattr,setxattr,openat"} + // The fd family runs all four fd-based syscalls in one scenario, so trace + // them together. + xattrFdTraceArgs = []string{"-trace-syscalls", "fsetxattr,fgetxattr,flistxattr,fremovexattr,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 @@ -137,3 +156,224 @@ func TestXattrRemovexattrat(t *testing.T) { assertEventBytesEqual(t, result, exp, 0) assertEventDurationPositive(t, result, exp) } + +// assertNoXattrNameAsPath fails if any record for the given enter tracepoint +// captured the xattr NAME ("user.ior") as its path instead of the file path. +func assertNoXattrNameAsPath(t *testing.T, result TestResult, tracepoint string) { + t.Helper() + for _, rec := range result.Records { + if rec.TraceID.String() == tracepoint && rec.Path == "user.ior" { + t.Errorf("%s captured xattr name %q as path instead of file path", tracepoint, rec.Path) + } + } +} + +// TestXattrGetxattr verifies ior traces the PATH-based getxattr(2) end-to-end. +// getxattr(const char *path, const char *name, void *value, size_t size) takes +// the filesystem PATH at args[0] and the xattr NAME at args[1]; only the path +// must be captured on entry. getxattr returns the xattr value size in bytes, so +// ior READ-classifies the exit and the recorded byte count must reflect at +// least the value written by the workload (consistent with +// lgetxattr/fgetxattr/getxattrat). +func TestXattrGetxattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-getxattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_getxattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrGetTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_getxattr") + + exp := ExpectedEvent{Tracepoint: "enter_getxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, exp, 1) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrLgetxattr verifies ior traces the no-follow PATH-based lgetxattr(2) +// end-to-end. lgetxattr has getxattr's signature but does NOT follow symlinks; +// the workload targets a regular file, so it returns the value size (user.* +// xattrs on a bare symlink are kernel-restricted). The PATH is at args[0]; the +// exit is READ-classified. +func TestXattrLgetxattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-lgetxattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_lgetxattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrLgetTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_lgetxattr") + + exp := ExpectedEvent{Tracepoint: "enter_lgetxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, exp, 1) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrListxattr verifies ior traces the PATH-based listxattr(2) end-to-end. +// listxattr(const char *path, char *list, size_t size) takes the PATH at +// args[0]; it returns the size in bytes of the NUL-separated xattr name list, +// so ior READ-classifies the exit (consistent with llistxattr/flistxattr/ +// listxattrat). +func TestXattrListxattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-listxattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_listxattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrListPathTraceArgs) + + exp := ExpectedEvent{Tracepoint: "enter_listxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, exp, 1) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrLlistxattr verifies ior traces the no-follow PATH-based llistxattr(2) +// end-to-end. As with lgetxattr the target is a regular file, so it returns the +// name-list size. The PATH is at args[0]; the exit is READ-classified. +func TestXattrLlistxattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-llistxattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_llistxattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrLlistTraceArgs) + + exp := ExpectedEvent{Tracepoint: "enter_llistxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, exp, 1) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrLsetxattr verifies ior traces the no-follow PATH-based lsetxattr(2) +// end-to-end. lsetxattr(const char *path, const char *name, const void *value, +// size_t size, int flags) takes the PATH at args[0] and the xattr NAME at +// args[1]; only the path must be captured. lsetxattr returns 0/-1 (its `size` +// arg is the INPUT value length), so the exit is UNCLASSIFIED — the accounted +// bytes must be exactly zero (contrast getxattr/lgetxattr, which ARE READ- +// classified), matching setxattr/setxattrat/fsetxattr. +func TestXattrLsetxattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-lsetxattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_lsetxattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrLsetTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_lsetxattr") + + exp := ExpectedEvent{Tracepoint: "enter_lsetxattr", Comm: "ioworkload"} + assertEventBytesEqual(t, result, exp, 0) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrSetxattrat verifies ior traces the -at variant setxattrat(2) (Linux +// 6.13+) end-to-end. setxattrat takes a dirfd plus a real filesystem PATH at +// args[1] (NOT args[0]=dfd) and the xattr NAME at args[3]; only the path must be +// captured. setxattrat SETS an attribute and returns 0/-1 (the value size is an +// INPUT field of struct xattr_args), so the exit is UNCLASSIFIED — the accounted +// bytes must be exactly zero (guarding against accidental READ-classification +// like its getxattrat/listxattrat siblings), matching setxattr/lsetxattr/ +// fsetxattr. +func TestXattrSetxattrat(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-setxattrat", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_setxattrat", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrSetatTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_setxattrat") + + exp := ExpectedEvent{Tracepoint: "enter_setxattrat", Comm: "ioworkload"} + assertEventBytesEqual(t, result, exp, 0) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrRemovexattr verifies ior traces the PATH-based removexattr(2) +// end-to-end. removexattr(const char *path, const char *name) takes the PATH at +// args[0] and the xattr NAME at args[1]; only the path must be captured. +// removexattr returns 0/-1, never a byte count, so the exit is UNCLASSIFIED — +// the accounted bytes must be exactly zero, matching lremovexattr/fremovexattr/ +// removexattrat. +func TestXattrRemovexattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-removexattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_removexattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrRemovePathTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_removexattr") + + exp := ExpectedEvent{Tracepoint: "enter_removexattr", Comm: "ioworkload"} + assertEventBytesEqual(t, result, exp, 0) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrLremovexattr verifies ior traces the no-follow PATH-based +// lremovexattr(2) end-to-end. As with the other l* variants the target is a +// regular file, so it removes the user.* attr. The PATH is at args[0]; the exit +// is UNCLASSIFIED. +func TestXattrLremovexattr(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-lremovexattr", []ExpectedEvent{ + { + PathContains: "xattrfile.txt", + Tracepoint: "enter_lremovexattr", + Comm: "ioworkload", + MinCount: 1, + }, + }, xattrLremoveTraceArgs) + + assertNoXattrNameAsPath(t, result, "enter_lremovexattr") + + exp := ExpectedEvent{Tracepoint: "enter_lremovexattr", Comm: "ioworkload"} + assertEventBytesEqual(t, result, exp, 0) + assertEventDurationPositive(t, result, exp) +} + +// TestXattrFd verifies ior traces the entire fd-based xattr family end-to-end: +// fsetxattr/fgetxattr/flistxattr/fremovexattr. Each takes the FD at args[0] +// (kind=fd, no path) rather than a pathname. fgetxattr and flistxattr return +// value/name-list sizes and are READ-classified (bytes>0); fsetxattr and +// fremovexattr return 0/-1 status and are UNCLASSIFIED (bytes==0). All four +// fire on the single open fd in the xattr-fd scenario. +func TestXattrFd(t *testing.T) { + result, _ := runScenarioResultWithIorArgs(t, "xattr-fd", []ExpectedEvent{ + {Tracepoint: "enter_fsetxattr", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_fgetxattr", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_flistxattr", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_fremovexattr", Comm: "ioworkload", MinCount: 1}, + }, xattrFdTraceArgs) + + // READ-classified fd reads: returned value/name-list sizes are accounted. + fget := ExpectedEvent{Tracepoint: "enter_fgetxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, fget, 1) + assertEventDurationPositive(t, result, fget) + + flist := ExpectedEvent{Tracepoint: "enter_flistxattr", Comm: "ioworkload"} + assertEventBytesAtLeast(t, result, flist, 1) + assertEventDurationPositive(t, result, flist) + + // UNCLASSIFIED fd ops: 0/-1 status returns, never a byte count. + fset := ExpectedEvent{Tracepoint: "enter_fsetxattr", Comm: "ioworkload"} + assertEventBytesEqual(t, result, fset, 0) + assertEventDurationPositive(t, result, fset) + + fremove := ExpectedEvent{Tracepoint: "enter_fremovexattr", Comm: "ioworkload"} + assertEventBytesEqual(t, result, fremove, 0) + assertEventDurationPositive(t, result, fremove) +} |
