lib/acl: close the pre-6.13 ACL parent-symlink race via /proc/self/fd

On a kernel without the Linux 6.13 *xattrat syscalls, xacl_*_at() returned ENOSYS
so acls.c fell back to the unconfined path-based sys_acl_set_file() -- the
long-standing parent-symlink race for --acls on every pre-6.13 Linux (the
receiver applies a received ACL to a leaf an attacker raced to a symlink ->
arbitrary-ACL LPE; acl-symlink-race only documented it as a residual there).

Add a /proc/self/fd compat to the dirfd dispatchers: when the *xattrat syscall is
absent (runtime ENOSYS or unbuilt), address the leaf as
/proc/self/fd/<dirfd>/<leaf> and use the l*xattr (no-follow-leaf) calls.  The
/proc/self/fd/<dirfd> magic symlink resolves to the pinned parent inode (a raced
parent symlink can't redirect it) and l*xattr does not follow a raced leaf
symlink, so the dirfd+leaf ACL ops are race-safe on every Linux kernel with
procfs -- no libacl dependency and no 6.13 requirement.  xacl_at_available() now
returns true when the syscalls OR the /proc compat are usable, so acls.c takes
the race-safe dirfd path instead of the path-based fallback (now only the BSDs /
a /proc-less namespace).  The held-fd path (fsetxattr) is unchanged.

Verified on ubuntu-2004 (kernel 5.4, HAVE_XATTRAT_SYSCALLS=0): "ACL_at": true and
acl-symlink-race PASSES while asserting (a vacuous pass before); no regression on
a 6.13+ host (native syscalls unchanged) or t_acl.
This commit is contained in:
Andrew Tridgell committed 2026-06-15 15:26:15 +10:00
1 parent ef8f8dd8ef
commit 26cd6f4c7b
2 files changed
+104 -40

No files matched your search

+18 -17
View File
@@ -665,11 +665,11 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
free(ents);
return 0;
}
/* No held fd and no *xattrat syscalls (pre-6.13 Linux, the BSDs, an
* un-pinnable entry): prefer reading the real destination ACL via the
* path-based call over a mode-only fake, so --acls stays functional on
* platforms that cannot offer the race-safe primitive. This matches the
* matching set path's fall-through and the prior (3.4.x) behaviour. */
/* Neither a held fd nor a usable dirfd path (xacl_at_available() covers the
* *xattrat syscalls AND the pre-6.13 /proc/self/fd compat, so this is the
* BSDs / a /proc-less namespace / an un-pinnable entry): read the real
* destination ACL via the path-based call rather than a mode-only fake, so
* --acls stays functional where the race-safe primitive is unavailable. */
#endif
if ((sacl = sys_acl_get_file(fname, type)) != 0) {
@@ -1114,11 +1114,11 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
#endif
#ifdef SUPPORT_ACL_FD
/* Race-safe default-ACL delete via the held fd or dirfd+leaf. Where
* neither is available -- no held fd and no *xattrat syscalls (pre-6.13
* Linux, the BSDs, an un-pinnable entry) -- fall back to the path-based
* call: we prefer the documented --acls functionality over refusing it
* on platforms that cannot offer the race-safe primitive. A current
* Linux kernel (6.13+) takes the secure xacl_*_at() path above. */
* neither is available (xacl_at_available() is false -- the BSDs, a
* /proc-less namespace, an un-pinnable entry; every Linux with procfs
* takes the dirfd path via *xattrat or the /proc/self/fd compat) -- fall
* back to the path-based call, preferring the documented --acls behaviour
* over refusing it where the race-safe primitive is unavailable. */
if (fd >= 0)
rc = xacl_del_default_fd(fd);
else if (dirfd >= 0 && xacl_at_available())
@@ -1197,13 +1197,14 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
sxp->st.st_mode = cur_mode;
return 0;
}
/* No held fd and no *xattrat syscalls (pre-6.13 Linux, the BSDs, an
* un-pinnable entry): prefer the documented --acls functionality over
* refusing it and fall back to the path-based set, on platforms that
* cannot offer the race-safe primitive. (6.13+ takes xacl_set_at()
* above.) This re-resolves fname, so it carries the long-standing
* parent-symlink-race exposure on those platforms; it is the prior
* (3.4.x) behaviour and the only way to honour --acls there. */
/* No held fd and no usable dirfd path (xacl_at_available() is false --
* the BSDs, a /proc-less namespace, an un-pinnable entry; every Linux
* with procfs took xacl_set_at() above via *xattrat or the /proc/self/fd
* compat): prefer the documented --acls behaviour over refusing it and
* fall back to the path-based set. This re-resolves fname, so it still
* carries the parent-symlink-race exposure on those remaining platforms;
* it is the only way to honour --acls where no race-safe primitive
* exists. */
#endif
if (sys_acl_set_file(fname, type, duo_item->sacl) < 0) {
rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_file(%s, %s)",
+86 -23
View File
@@ -186,59 +186,90 @@ static int xattr_to_acl(const unsigned char *buf, size_t len,
/* === syscall dispatchers (fd-variant vs at-variant) === */
/* Pre-6.13 fallback for the dirfd+leaf at-variants: address the leaf as
* /proc/self/fd/<dirfd>/<leaf> and use the l*xattr (no-follow-leaf) calls. The
* /proc/self/fd/<dirfd> magic symlink resolves to the pinned parent inode -- a
* raced parent symlink cannot redirect it -- and l*xattr does not follow a raced
* leaf symlink, so this is race-safe without the Linux 6.13 *xattrat syscalls, as
* long as procfs is mounted. (`leaf` is a single component, <= NAME_MAX.)
* Returns 0 and fills `buf`, or -1 with ENAMETOOLONG. */
static int proc_fd_leaf_path(char *buf, size_t buflen, int dirfd, const char *leaf)
{
int n = snprintf(buf, buflen, "/proc/self/fd/%d/%s", dirfd, leaf);
if (n < 0 || (size_t)n >= buflen) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
static ssize_t do_getxattr(int fd, int dirfd, const char *leaf,
const char *name, void *val, size_t size)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fgetxattr(fd, name, val, size);
#ifdef HAVE_XATTRAT_SYSCALLS
{
struct rsync_xattr_args args;
ssize_t ret;
args.value = (uint64_t)(uintptr_t)val;
args.size = (uint32_t)size;
args.flags = 0;
return syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
ret = syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
if (ret != -1 || errno != ENOSYS)
return ret;
/* ENOSYS: kernel < 6.13 -- fall through to the /proc compat. */
}
#else
(void)dirfd; (void)leaf; (void)name; (void)val; (void)size;
errno = ENOSYS;
return -1;
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lgetxattr(p, name, val, size);
}
static int do_setxattr(int fd, int dirfd, const char *leaf,
const char *name, const void *val, size_t size)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fsetxattr(fd, name, val, size, 0);
#ifdef HAVE_XATTRAT_SYSCALLS
{
struct rsync_xattr_args args;
int ret;
args.value = (uint64_t)(uintptr_t)val;
args.size = (uint32_t)size;
args.flags = 0; /* replace */
return syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
ret = syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
if (ret != -1 || errno != ENOSYS)
return ret;
}
#else
(void)dirfd; (void)leaf; (void)name; (void)val; (void)size;
errno = ENOSYS;
return -1;
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lsetxattr(p, name, val, size, 0);
}
static int do_removexattr(int fd, int dirfd, const char *leaf, const char *name)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fremovexattr(fd, name);
#ifdef HAVE_XATTRAT_SYSCALLS
return syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name);
#else
(void)dirfd; (void)leaf; (void)name;
errno = ENOSYS;
return -1;
{
int ret = syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name);
if (ret != -1 || errno != ENOSYS)
return ret;
}
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lremovexattr(p, name);
}
/* Read the whole named xattr into a malloc'd buffer, growing as needed. */
@@ -367,20 +398,52 @@ int xacl_del_default_at(int dirfd, const char *leaf)
return acl_del_default_common(-1, dirfd, leaf);
}
/* True iff /proc/self/fd magic symlinks are usable, so the dirfd+leaf at-variants
* work race-safely via the /proc compat on a pre-6.13 kernel. */
static int proc_self_fd_usable(void)
{
int dfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
char p[64];
int usable = 0;
if (dfd < 0)
return 0;
if (snprintf(p, sizeof p, "/proc/self/fd/%d/.", dfd) < (int)sizeof p) {
/* The probe attr is absent; the path resolving (any errno but
* ENOENT/ENOTDIR -- e.g. ENODATA/ENOTSUP/EACCES) means procfs gives us
* the magic fd-symlink we need. */
errno = 0;
lgetxattr(p, "user.rsync_acl_probe", NULL, 0);
usable = !(errno == ENOENT || errno == ENOTDIR);
}
close(dfd);
return usable;
}
int xacl_at_available(void)
{
static int avail = -1;
if (avail < 0) {
#ifdef HAVE_XATTRAT_SYSCALLS
/* A harmless probe: query a nonexistent attr on ".". Any errno
* other than ENOSYS means the syscall itself is present. */
/* Probe the *xattrat syscall directly (not via do_getxattr's /proc
* fallback): any errno other than ENOSYS means it is present (6.13+). */
struct rsync_xattr_args args;
args.value = 0;
args.size = 0;
args.flags = 0;
errno = 0;
do_getxattr(-1, AT_FDCWD, ".", "user.rsync_acl_probe", NULL, 0);
avail = errno == ENOSYS ? 0 : 1;
#else
avail = 0;
syscall(SYS_getxattrat, AT_FDCWD, ".", AT_SYMLINK_NOFOLLOW,
"user.rsync_acl_probe", &args, sizeof args);
if (errno != ENOSYS) {
avail = 1;
return avail;
}
#endif
/* No *xattrat syscalls (pre-6.13, or a kernel built without them): the dirfd+leaf ACL ops
* are still race-safe via /proc/self/fd if procfs is mounted, closing
* the parent-symlink-race gap that otherwise forces the path-based set. */
avail = proc_self_fd_usable();
}
return avail;
}