]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
xattr-util: try new *xattrat() family syscalls first
authorMike Yuan <me@yhndnzj.com>
Mon, 20 Jan 2025 19:43:02 +0000 (20:43 +0100)
committerMike Yuan <me@yhndnzj.com>
Sun, 9 Feb 2025 13:51:04 +0000 (14:51 +0100)
Added in https://github.com/torvalds/linux/commit/6140be90ec70c39fa844741ca3cc807dd0866394

However, when O_PATH fds are encountered we'd have to go by
/proc/self/fd/ still, since the kernel people are reluctant
to make the new syscalls work with them
(https://lore.kernel.org/linux-fsdevel/20250206-steril-raumplanung-733224062432@brauner/)
Hence getxattrat() and listxattrat() are not employed.

While at it, remove the discrepancy between path being NULL
and empty - I don't grok the "security issue" claimed earlier,
but nowadays even the kernel treats the two as identical:
https://github.com/torvalds/linux/commit/e896474fe4851ffc4dd860c92daa906783090346

README
src/basic/xattr-util.c
src/basic/xattr-util.h
src/shared/copy.c
src/test/test-xattr-util.c

diff --git a/README b/README
index 975f5e5a5e5f0d437441a770443eaecae77535e5..1f3d1df2754870e051fd3c9aa74e57b71a916927 100644 (file)
--- a/README
+++ b/README
@@ -67,6 +67,7 @@ REQUIREMENTS:
                                and MOVE_MOUNT_BENEATH
                      ≥ 6.6 for quota support on tmpfs
                      ≥ 6.9 for pidfs
+                     ≥ 6.13 for PIDFD_GET_INFO and {set,remove}xattrat()
 
         ✅ systemd utilizes several new kernel APIs, but will fall back gracefully
            when unavailable.
index a68cff79860521b8ae9206d4d3174e2b657d43eb..411103b83e94876939c598555fb38694ef50a346 100644 (file)
@@ -12,6 +12,7 @@
 #include "fd-util.h"
 #include "macro.h"
 #include "missing_syscall.h"
+#include "missing_threads.h"
 #include "parse-util.h"
 #include "sparse-endian.h"
 #include "stat-util.h"
 #include "time-util.h"
 #include "xattr-util.h"
 
+/* Use a single cache for all of *xattrat syscalls (added in kernel 6.13) */
+static thread_local bool have_xattrat = true;
+
+static int normalize_and_maybe_pin_inode(
+                int *fd,
+                const char **path,
+                int *at_flags,
+                int *ret_tfd,
+                bool *ret_opath) {
+
+        int r;
+
+        assert(fd);
+        assert(*fd >= 0 || *fd == AT_FDCWD);
+        assert(path);
+        assert(at_flags);
+        assert(ret_tfd);
+        assert(ret_opath);
+
+        if (isempty(*path))
+                *path = NULL; /* Normalize "" to NULL */
+
+        if (*fd == AT_FDCWD) {
+                if (!*path) /* Both unspecified? Then operate on current working directory */
+                        *path = ".";
+
+                *ret_tfd = -EBADF;
+                *ret_opath = false;
+                return 0;
+        }
+
+        *at_flags |= AT_EMPTY_PATH;
+
+        if (!*path) {
+                r = fd_is_opath(*fd);
+                if (r < 0)
+                        return r;
+                *ret_opath = r;
+
+                *ret_tfd = -EBADF;
+                return 0;
+        }
+
+        /* If both have been specified, then we go via O_PATH */
+
+        int tfd = openat(*fd, *path, O_PATH|O_CLOEXEC|(FLAGS_SET(*at_flags, AT_SYMLINK_FOLLOW) ? 0 : O_NOFOLLOW));
+        if (tfd < 0)
+                return -errno;
+
+        *fd = *ret_tfd = tfd;
+        *path = NULL;
+        *ret_opath = true;
+
+        return 0;
+}
+
+static int getxattr_pinned_internal(
+                int fd,
+                const char *path,
+                int at_flags,
+                bool by_procfs,
+                const char *name,
+                char *buf,
+                size_t size) {
+
+        ssize_t n;
+
+        assert(!path || !isempty(path));
+        assert((fd >= 0) == !path);
+        assert((at_flags & ~(AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH)) == 0);
+        assert(path || FLAGS_SET(at_flags, AT_EMPTY_PATH));
+        assert(name);
+        assert(buf || size == 0);
+
+        if (path)
+                n = FLAGS_SET(at_flags, AT_SYMLINK_NOFOLLOW) ? lgetxattr(path, name, buf, size)
+                                                             : getxattr(path, name, buf, size);
+        else
+                n = by_procfs ? getxattr(FORMAT_PROC_FD_PATH(fd), name, buf, size)
+                              : fgetxattr(fd, name, buf, size);
+        if (n < 0)
+                return -errno;
+
+        assert((size_t) n <= size);
+
+        if (n > INT_MAX) /* We couldn't return this as 'int' anymore */
+                return -E2BIG;
+
+        return (int) n;
+}
+
 int getxattr_at_malloc(
                 int fd,
                 const char *path,
                 const char *name,
-                int flags,
+                int at_flags,
                 char **ret) {
 
         _cleanup_close_ int opened_fd = -EBADF;
-        unsigned n_attempts = 7;
-        bool by_procfs = false;
-        size_t l = 100;
+        bool by_procfs;
+        int r;
 
         assert(fd >= 0 || fd == AT_FDCWD);
         assert(name);
-        assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
+        assert((at_flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
         assert(ret);
 
         /* So, this is single function that does what getxattr()/lgetxattr()/fgetxattr() does, but in one go,
          * and with additional bells and whistles. Specifically:
          *
-         * 1. This works on O_PATH fds (which fgetxattr() does not)
-         * 2. Provides full openat()-style semantics, i.e. by-fd, by-path and combination thereof
-         * 3. As extension to openat()-style semantics implies AT_EMPTY_PATH if path is NULL.
-         * 4. Does a malloc() loop, automatically sizing the allocation
-         * 5. NUL-terminates the returned buffer (for safety)
+         * 1. This works on O_PATH fds (via /proc/self/fd/, since getxattrat() syscall refuses them...)
+         * 2. As extension to openat()-style semantics implies AT_EMPTY_PATH if path is empty
+         * 3. Does a malloc() loop, automatically sizing the allocation
+         * 4. NUL-terminates the returned buffer (for safety)
          */
 
-        if (!path) /* If path is NULL, imply AT_EMPTY_PATH. – But if it's "", don't — for safety reasons. */
-                flags |= AT_EMPTY_PATH;
-
-        if (isempty(path)) {
-                if (!FLAGS_SET(flags, AT_EMPTY_PATH))
-                        return -EINVAL;
-
-                if (fd == AT_FDCWD) /* Both unspecified? Then operate on current working directory */
-                        path = ".";
-                else
-                        path = NULL;
-
-        } else if (fd != AT_FDCWD) {
-
-                /* If both have been specified, then we go via O_PATH */
-                opened_fd = openat(fd, path, O_PATH|O_CLOEXEC|(FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? 0 : O_NOFOLLOW));
-                if (opened_fd < 0)
-                        return -errno;
+        r = normalize_and_maybe_pin_inode(&fd, &path, &at_flags, &opened_fd, &by_procfs);
+        if (r < 0)
+                return r;
 
-                fd = opened_fd;
-                path = NULL;
-                by_procfs = true; /* fgetxattr() is not going to work, go via /proc/ link right-away */
-        }
+        at_flags = at_flags_normalize_nofollow(at_flags);
 
-        for (;;) {
+        size_t l = 100;
+        for (unsigned n_attempts = 7;;) {
                 _cleanup_free_ char *v = NULL;
-                ssize_t n;
 
                 if (n_attempts == 0) /* If someone is racing against us, give up eventually */
                         return -EBUSY;
                 n_attempts--;
 
-                v = new0(char, l+1);
+                v = new(char, l+1);
                 if (!v)
                         return -ENOMEM;
 
                 l = MALLOC_ELEMENTSOF(v) - 1;
 
-                if (path)
-                        n = FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? getxattr(path, name, v, l) : lgetxattr(path, name, v, l);
-                else
-                        n = by_procfs ? getxattr(FORMAT_PROC_FD_PATH(fd), name, v, l) : fgetxattr(fd, name, v, l);
-                if (n < 0) {
-                        if (errno == EBADF) {
-                                if (by_procfs || path)
-                                        return -EBADF;
-
-                                by_procfs = true; /* Might be an O_PATH fd, try again via /proc/ link */
-                                continue;
-                        }
-
-                        if (errno != ERANGE)
-                                return -errno;
-                } else {
-                        v[n] = 0; /* NUL terminate */
+                r = getxattr_pinned_internal(fd, path, at_flags, by_procfs, name, v, l);
+                if (r >= 0) {
+                        v[r] = 0; /* NUL terminate */
                         *ret = TAKE_PTR(v);
-                        return (int) n;
+                        return r;
                 }
+                if (r != -ERANGE)
+                        return r;
 
-                if (path)
-                        n = FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? getxattr(path, name, NULL, 0) : lgetxattr(path, name, NULL, 0);
-                else
-                        n = by_procfs ? getxattr(FORMAT_PROC_FD_PATH(fd), name, NULL, 0) : fgetxattr(fd, name, NULL, 0);
-                if (n < 0)
-                        return -errno;
-                if (n > INT_MAX) /* We couldn't return this as 'int' anymore */
-                        return -E2BIG;
+                r = getxattr_pinned_internal(fd, path, at_flags, by_procfs, name, NULL, 0);
+                if (r < 0)
+                        return r;
 
-                l = (size_t) n;
+                l = (size_t) r;
         }
 }
 
-int getxattr_at_bool(int fd, const char *path, const char *name, int flags) {
+int getxattr_at_bool(int fd, const char *path, const char *name, int at_flags) {
         _cleanup_free_ char *v = NULL;
         int r;
 
-        r = getxattr_at_malloc(fd, path, name, flags, &v);
+        r = getxattr_at_malloc(fd, path, name, at_flags, &v);
         if (r < 0)
                 return r;
 
@@ -133,49 +188,59 @@ int getxattr_at_bool(int fd, const char *path, const char *name, int flags) {
         return parse_boolean(v);
 }
 
-int listxattr_at_malloc(
+static int listxattr_pinned_internal(
                 int fd,
                 const char *path,
-                int flags,
-                char **ret) {
+                int at_flags,
+                bool by_procfs,
+                char *buf,
+                size_t size) {
+
+        ssize_t n;
 
+        assert(!path || !isempty(path));
+        assert((fd >= 0) == !path);
+        assert((at_flags & ~(AT_SYMLINK_NOFOLLOW|AT_EMPTY_PATH)) == 0);
+        assert(path || FLAGS_SET(at_flags, AT_EMPTY_PATH));
+        assert(buf || size == 0);
+
+        if (path)
+                n = FLAGS_SET(at_flags, AT_SYMLINK_NOFOLLOW) ? llistxattr(path, buf, size)
+                                                             : listxattr(path, buf, size);
+        else
+                n = by_procfs ? listxattr(FORMAT_PROC_FD_PATH(fd), buf, size)
+                              : flistxattr(fd, buf, size);
+        if (n < 0)
+                return -errno;
+
+        assert((size_t) n <= size);
+
+        if (n > INT_MAX) /* We couldn't return this as 'int' anymore */
+                return -E2BIG;
+
+        return (int) n;
+}
+
+int listxattr_at_malloc(int fd, const char *path, int at_flags, char **ret) {
         _cleanup_close_ int opened_fd = -EBADF;
-        bool by_procfs = false;
-        unsigned n_attempts = 7;
-        size_t l = 100;
+        bool by_procfs;
+        int r;
 
         assert(fd >= 0 || fd == AT_FDCWD);
-        assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
+        assert((at_flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
         assert(ret);
 
         /* This is to listxattr()/llistattr()/flistattr() what getxattr_at_malloc() is to getxattr()/… */
 
-        if (!path) /* If path is NULL, imply AT_EMPTY_PATH. – But if it's "", don't. */
-                flags |= AT_EMPTY_PATH;
-
-        if (isempty(path)) {
-                if (!FLAGS_SET(flags, AT_EMPTY_PATH))
-                        return -EINVAL;
-
-                if (fd == AT_FDCWD) /* Both unspecified? Then operate on current working directory */
-                        path = ".";
-                else
-                        path = NULL;
+        r = normalize_and_maybe_pin_inode(&fd, &path, &at_flags, &opened_fd, &by_procfs);
+        if (r < 0)
+                return r;
 
-        } else if (fd != AT_FDCWD) {
-                /* If both have been specified, then we go via O_PATH */
-                opened_fd = openat(fd, path, O_PATH|O_CLOEXEC|(FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? 0 : O_NOFOLLOW));
-                if (opened_fd < 0)
-                        return -errno;
+        at_flags = at_flags_normalize_nofollow(at_flags);
 
-                fd = opened_fd;
-                path = NULL;
-                by_procfs = true;
-        }
-
-        for (;;) {
+        size_t l = 100;
+        for (unsigned n_attempts = 7;;) {
                 _cleanup_free_ char *v = NULL;
-                ssize_t n;
 
                 if (n_attempts == 0) /* If someone is racing against us, give up eventually */
                         return -EBUSY;
@@ -187,103 +252,110 @@ int listxattr_at_malloc(
 
                 l = MALLOC_ELEMENTSOF(v) - 1;
 
-                if (path)
-                        n = FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? listxattr(path, v, l) : llistxattr(path, v, l);
-                else
-                        n = by_procfs ? listxattr(FORMAT_PROC_FD_PATH(fd), v, l) : flistxattr(fd, v, l);
-                if (n < 0) {
-                        if (errno == EBADF) {
-                                if (by_procfs || path)
-                                        return -EBADF;
-
-                                by_procfs = true; /* Might be an O_PATH fd, try again via /proc/ link */
-                                continue;
-                        }
-
-                        if (errno != ERANGE)
-                                return -errno;
-                } else {
-                        v[n] = 0; /* NUL terminate */
+                r = listxattr_pinned_internal(fd, path, at_flags, by_procfs, v, l);
+                if (r >= 0) {
+                        v[r] = 0; /* NUL terminate */
                         *ret = TAKE_PTR(v);
-                        return (int) n;
+                        return r;
                 }
+                if (r != -ERANGE)
+                        return r;
 
-                if (path)
-                        n = FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? listxattr(path, NULL, 0) : llistxattr(path, NULL, 0);
-                else
-                        n = by_procfs ? listxattr(FORMAT_PROC_FD_PATH(fd), NULL, 0) : flistxattr(fd, NULL, 0);
-                if (n < 0)
-                        return -errno;
-                if (n > INT_MAX) /* We couldn't return this as 'int' anymore */
-                        return -E2BIG;
+                r = listxattr_pinned_internal(fd, path, at_flags, by_procfs, NULL, 0);
+                if (r < 0)
+                        return r;
 
-                l = (size_t) n;
+                l = (size_t) r;
         }
 }
 
-int xsetxattr(int fd,
-              const char *path,
-              const char *name,
-              const char *value,
-              size_t size,
-              int flags) {
+int xsetxattr_full(
+                int fd,
+                const char *path,
+                int at_flags,
+                const char *name,
+                const char *value,
+                size_t size,
+                int xattr_flags) {
 
-        _cleanup_close_ int opened_fd = -EBADF;
-        bool by_procfs = false;
         int r;
 
         assert(fd >= 0 || fd == AT_FDCWD);
+        assert((at_flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
         assert(name);
         assert(value);
-        assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
-
-        /* So, this is a single function that does what setxattr()/lsetxattr()/fsetxattr() do, but in one go,
-         * and with additional bells and whistles. Specifically:
-         *
-         * 1. This works on O_PATH fds (which fsetxattr() does not)
-         * 2. Provides full openat()-style semantics, i.e. by-fd, by-path and combination thereof
-         * 3. As extension to openat()-style semantics implies AT_EMPTY_PATH if path is NULL.
-         */
-
-        if (!path) /* If path is NULL, imply AT_EMPTY_PATH. – But if it's "", don't — for safety reasons. */
-                flags |= AT_EMPTY_PATH;
 
         if (size == SIZE_MAX)
                 size = strlen(value);
 
-        if (isempty(path)) {
-                if (!FLAGS_SET(flags, AT_EMPTY_PATH))
-                        return -EINVAL;
+        if (have_xattrat && !isempty(path)) {
+                struct xattr_args args = {
+                        .value = PTR_TO_UINT64(value),
+                        .size = size,
+                        .flags = xattr_flags,
+                };
+
+                r = RET_NERRNO(setxattrat(fd, path,
+                                          at_flags_normalize_nofollow(at_flags),
+                                          name,
+                                          &args, sizeof(args)));
+                if (r != -ENOSYS) /* No ERRNO_IS_NOT_SUPPORTED here, as EOPNOTSUPP denotes the fs doesn't
+                                     support xattr */
+                        return r;
+
+                have_xattrat = false;
+        }
 
-                if (fd == AT_FDCWD) /* Both unspecified? Then operate on current working directory */
-                        path = ".";
-                else {
-                        r = fd_is_opath(fd);
-                        if (r < 0)
-                                return r;
+        _cleanup_close_ int opened_fd = -EBADF;
+        bool by_procfs;
 
-                        by_procfs = r;
-                        path = NULL;
-                }
+        r = normalize_and_maybe_pin_inode(&fd, &path, &at_flags, &opened_fd, &by_procfs);
+        if (r < 0)
+                return r;
 
-        } else if (fd != AT_FDCWD) {
+        if (path)
+                r = FLAGS_SET(at_flags, AT_SYMLINK_FOLLOW) ? setxattr(path, name, value, size, xattr_flags)
+                                                           : lsetxattr(path, name, value, size, xattr_flags);
+        else
+                r = by_procfs ? setxattr(FORMAT_PROC_FD_PATH(fd), name, value, size, xattr_flags)
+                              : fsetxattr(fd, name, value, size, xattr_flags);
+        if (r < 0)
+                return -errno;
 
-                /* If both have been specified, then we go via O_PATH */
-                opened_fd = openat(fd, path, O_PATH|O_CLOEXEC|(FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? 0 : O_NOFOLLOW));
-                if (opened_fd < 0)
-                        return -errno;
+        return 0;
+}
+
+int xremovexattr(int fd, const char *path, int at_flags, const char *name) {
+        int r;
 
-                fd = opened_fd;
-                path = NULL;
-                by_procfs = true; /* fsetxattr() is not going to work, go via /proc/ link right-away */
+        assert(fd >= 0 || fd == AT_FDCWD);
+        assert((at_flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH)) == 0);
+        assert(name);
+
+        if (have_xattrat && !isempty(path)) {
+                r = RET_NERRNO(removexattrat(fd, path,
+                                             at_flags_normalize_nofollow(at_flags),
+                                             name));
+                if (r != -ENOSYS) /* No ERRNO_IS_NOT_SUPPORTED here, as EOPNOTSUPP denotes the fs doesn't
+                                     support xattr */
+                        return r;
+
+                have_xattrat = false;
         }
 
+        _cleanup_close_ int tfd = -EBADF;
+        bool by_procfs;
+
+        r = normalize_and_maybe_pin_inode(&fd, &path, &at_flags, &tfd, &by_procfs);
+        if (r < 0)
+                return r;
+
         if (path)
-                r = FLAGS_SET(flags, AT_SYMLINK_FOLLOW) ? setxattr(path, name, value, size, 0)
-                                                        : lsetxattr(path, name, value, size, 0);
+                r = FLAGS_SET(at_flags, AT_SYMLINK_FOLLOW) ? removexattr(path, name)
+                                                           : lremovexattr(path, name);
         else
-                r = by_procfs ? setxattr(FORMAT_PROC_FD_PATH(fd), name, value, size, 0)
-                              : fsetxattr(fd, name, value, size, 0);
+                r = by_procfs ? removexattr(FORMAT_PROC_FD_PATH(fd), name)
+                              : fremovexattr(fd, name);
         if (r < 0)
                 return -errno;
 
@@ -373,5 +445,7 @@ int fd_setcrtime(int fd, usec_t usec) {
                 usec = now(CLOCK_REALTIME);
 
         le = htole64((uint64_t) usec);
-        return xsetxattr(fd, /* path = */ NULL, "user.crtime_usec", (const char*) &le, sizeof(le), AT_EMPTY_PATH);
+        return xsetxattr_full(fd, /* path = */ NULL, AT_EMPTY_PATH,
+                              "user.crtime_usec", (const char*) &le, sizeof(le),
+                              /* xattr_flags = */ 0);
 }
index 7d5aea30171fc9cdd0887afe89e02381c4a5ef8e..03e6a46cdbc712a168e75b5113dab0bad67e5f9a 100644 (file)
@@ -7,7 +7,7 @@
 
 #include "time-util.h"
 
-int getxattr_at_malloc(int fd, const char *path, const char *name, int flags, char **ret);
+int getxattr_at_malloc(int fd, const char *path, const char *name, int at_flags, char **ret);
 static inline int getxattr_malloc(const char *path, const char *name, char **ret) {
         return getxattr_at_malloc(AT_FDCWD, path, name, AT_SYMLINK_FOLLOW, ret);
 }
@@ -18,9 +18,9 @@ static inline int fgetxattr_malloc(int fd, const char *name, char **ret) {
         return getxattr_at_malloc(fd, NULL, name, AT_EMPTY_PATH, ret);
 }
 
-int getxattr_at_bool(int fd, const char *path, const char *name, int flags);
+int getxattr_at_bool(int fd, const char *path, const char *name, int at_flags);
 
-int listxattr_at_malloc(int fd, const char *path, int flags, char **ret);
+int listxattr_at_malloc(int fd, const char *path, int at_flags, char **ret);
 static inline int listxattr_malloc(const char *path, char **ret) {
         return listxattr_at_malloc(AT_FDCWD, path, AT_SYMLINK_FOLLOW, ret);
 }
@@ -31,7 +31,24 @@ static inline int flistxattr_malloc(int fd, char **ret) {
         return listxattr_at_malloc(fd, NULL, AT_EMPTY_PATH, ret);
 }
 
-int xsetxattr(int fd, const char *path, const char *name, const char *value, size_t size, int flags);
+int xsetxattr_full(
+                int fd,
+                const char *path,
+                int at_flags,
+                const char *name,
+                const char *value,
+                size_t size,
+                int xattr_flags);
+static inline int xsetxattr(
+                int fd,
+                const char *path,
+                int at_flags,
+                const char *name,
+                const char *value) {
+        return xsetxattr_full(fd, path, at_flags, name, value, SIZE_MAX, 0);
+}
+
+int xremovexattr(int fd, const char *path, int at_flags, const char *name);
 
 int fd_setcrtime(int fd, usec_t usec);
 int getcrtime_at(int fd, const char *path, int at_flags, usec_t *ret);
index cb3944b279ceaea5fbe38b14b521342ffa052319..17c027a9e89e6c8c9f754fc811643621de4010b2 100644 (file)
@@ -1679,8 +1679,7 @@ int copy_xattr(int df, const char *from, int dt, const char *to, CopyFlags copy_
                 if (r < 0)
                         return r;
 
-                if (xsetxattr(dt, to, p, value, r, 0) < 0)
-                        ret = -errno;
+                RET_GATHER(ret, xsetxattr_full(dt, to, /* at_flags = */ 0, p, value, r, /* xattr_flags = */ 0));
         }
 
         return ret;
index c754ac526e85877d69c8abdf101c93d85026c045..57b5939d9394d3f2c3c7bffc51ac44c8c14a9506 100644 (file)
@@ -82,7 +82,7 @@ TEST(getcrtime) {
 static void verify_xattr(int dfd, const char *expected) {
         _cleanup_free_ char *value = NULL;
 
-        assert_se(getxattr_at_malloc(dfd, "test", "user.foo", 0, &value) == (int) strlen(expected));
+        ASSERT_OK_EQ(getxattr_at_malloc(dfd, "test", "user.foo", 0, &value), (int) strlen(expected));
         ASSERT_STREQ(value, expected);
 }
 
@@ -98,31 +98,38 @@ TEST(xsetxattr) {
         assert_se(touch(x) >= 0);
 
         /* by full path */
-        r = xsetxattr(AT_FDCWD, x, "user.foo", "fullpath", SIZE_MAX, 0);
+        r = xsetxattr(AT_FDCWD, x, 0, "user.foo", "fullpath");
         if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
                 return (void) log_tests_skipped_errno(r, "no xattrs supported on /var/tmp");
-        assert_se(r >= 0);
+        ASSERT_OK(r);
         verify_xattr(dfd, "fullpath");
 
         /* by dirfd */
-        assert_se(xsetxattr(dfd, "test", "user.foo", "dirfd", SIZE_MAX, 0) >= 0);
+        ASSERT_ERROR(xsetxattr_full(dfd, "test", 0, "user.foo", "dirfd", SIZE_MAX, XATTR_CREATE), EEXIST);
+        verify_xattr(dfd, "fullpath");
+
+        ASSERT_OK(xsetxattr_full(dfd, "test", 0, "user.foo", "dirfd", SIZE_MAX, XATTR_REPLACE));
         verify_xattr(dfd, "dirfd");
 
         /* by fd (O_PATH) */
-        fd = openat(dfd, "test", O_PATH|O_CLOEXEC);
-        assert_se(fd >= 0);
-        assert_se(xsetxattr(fd, NULL, "user.foo", "fd_opath", SIZE_MAX, 0) >= 0);
+        ASSERT_OK_ERRNO(fd = openat(dfd, "test", O_PATH|O_CLOEXEC));
+
+        ASSERT_OK(xremovexattr(fd, "", 0, "user.foo"));
+
+        ASSERT_OK(xsetxattr_full(fd, NULL, AT_EMPTY_PATH, "user.foo", "fd_opath", SIZE_MAX, XATTR_CREATE));
         verify_xattr(dfd, "fd_opath");
-        assert_se(xsetxattr(fd, "", "user.foo", "fd_opath", SIZE_MAX, 0) == -EINVAL);
-        assert_se(xsetxattr(fd, "", "user.foo", "fd_opath_empty", SIZE_MAX, AT_EMPTY_PATH) >= 0);
+
+        ASSERT_OK(xsetxattr(fd, "", 0, "user.foo", "fd_opath_empty"));
         verify_xattr(dfd, "fd_opath_empty");
+
         fd = safe_close(fd);
 
         fd = openat(dfd, "test", O_RDONLY|O_CLOEXEC);
-        assert_se(xsetxattr(fd, NULL, "user.foo", "fd_regular", SIZE_MAX, 0) >= 0);
+
+        ASSERT_OK(xsetxattr_full(fd, NULL, 0, "user.foo", "fd_regular", SIZE_MAX, XATTR_REPLACE));
         verify_xattr(dfd, "fd_regular");
-        assert_se(xsetxattr(fd, "", "user.foo", "fd_regular_empty", SIZE_MAX, 0) == -EINVAL);
-        assert_se(xsetxattr(fd, "", "user.foo", "fd_regular_empty", SIZE_MAX, AT_EMPTY_PATH) >= 0);
+
+        ASSERT_OK(xsetxattr(fd, "", 0, "user.foo", "fd_regular_empty"));
         verify_xattr(dfd, "fd_regular_empty");
 }