summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/ioworkload/scenario_xattr.go313
-rw-r--r--cmd/ioworkload/scenarios.go9
-rw-r--r--integrationtests/xattr_test.go240
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)
+}