From 893d3f612e2adcc777f7446dbcd241b40867482d Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Thu, 11 Jun 2026 15:40:08 +1000 Subject: [PATCH] acls: route race-safe ACL ops through libacl *_at when available When configure detects a patched libacl exporting acl_get_file_at / acl_set_file_at / acl_delete_def_file_at (new HAVE_LIBACL_AT; ACL_1.3), route the receiver's race-safe ACL get/set/delete through them instead of lib/acl.c's xacl_* -- held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW, reusing the existing unpack_smb_acl/pack_smb_acl conversion. libacl's /proc/self/fd compat makes this race-safe on every Linux kernel. lib/acl.c stays as the fallback for a stock libacl (macro undefined -> zero change there); it already closes the same pre-6.13 parent-symlink-race gap via its own /proc/self/fd compat, so the two paths are equivalent in safety and prefer libacl only when its newer bindings are present. -VV reports ACL-libacl-at and keeps "ACL_at": true. A stock -lacl lacks these symbols, so HAVE_LIBACL_AT is undefined on real distros and the build is byte-identical to the lib/acl.c path until libacl ships ACL_1.3; build against the patched lib via CPPFLAGS/LDFLAGS pointing at its install prefix. --- acls.c | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++- configure.ac | 24 ++++++++++++++++++ lib/sysacls.c | 20 +++++++++++++++ lib/sysacls.h | 6 +++++ usage.c | 18 +++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/acls.c b/acls.c index b1b3d3b6..c9f339c5 100644 --- a/acls.c +++ b/acls.c @@ -22,6 +22,9 @@ #include "rsync.h" #include "lib/sysacls.h" #include "lib/acl.h" +#ifdef HAVE_LIBACL_AT +#include /* AT_EMPTY_PATH / AT_SYMLINK_NOFOLLOW */ +#endif #ifdef SUPPORT_ACLS @@ -470,7 +473,10 @@ static int find_matching_rsync_acl(const rsync_acl *racl, SMB_ACL_TYPE_T type, return *match; } -#ifdef SUPPORT_ACL_FD +/* These two bridge lib/acl.c's neutral (tag,perm,id) entry array; with + * HAVE_LIBACL_AT the libacl *_at path uses unpack_smb_acl/pack_smb_acl directly, + * so they are unused there. */ +#if defined(SUPPORT_ACL_FD) && !defined(HAVE_LIBACL_AT) /* Convert a packed system ACL into the neutral (tag,perm,id) entry array that * lib/acl.c serializes. Reuses pack_smb_acl()+change_sacl_perms() output so * the bytes we write match exactly what acl_set_file() would have written. @@ -627,6 +633,33 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, #endif #ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Read the ACL via the new libacl *_at calls; fd<0 && dirfd<0 + * (e.g. a synthetic dir) falls through to the path-based call below. */ + if (fd >= 0 || dirfd >= 0) { + if (fd >= 0) + sacl = sys_acl_get_file_at(fd, "", AT_EMPTY_PATH, type); + else + sacl = sys_acl_get_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type); + if (sacl != 0) { + BOOL ok = unpack_smb_acl(sacl, racl); + sys_acl_free_acl(sacl); + if (!ok) { + rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname); + return -1; + } + return 0; + } + if (no_acl_syscall_error(errno)) { + if (type == SMB_ACL_TYPE_ACCESS) + rsync_acl_fake_perms(racl, mode); + return 0; + } + rsyserr(FERROR_XFER, errno, "get_acl: acl_get_file_at(%s, %s)", + fname, str_acl_type(type)); + return -1; + } +#else /* Race-safe path: read the ACL through the held O_NOFOLLOW fd, or via * setxattrat(AT_SYMLINK_NOFOLLOW) on dirfd+leaf, instead of re-resolving * fname. Only for real-root ACLs (am_root >= 0; the fake-super branch @@ -670,6 +703,7 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, * 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 /* HAVE_LIBACL_AT */ #endif if ((sacl = sys_acl_get_file(fname, type)) != 0) { @@ -1114,6 +1148,16 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, else #endif #ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Race-safe default-ACL delete via the new libacl *_at + * calls (held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW) + * -- race-safe on every Linux kernel. fd<0 && dirfd<0 falls to path. */ + if (fd >= 0) + rc = sys_acl_delete_def_file_at(fd, "", AT_EMPTY_PATH); + else if (dirfd >= 0) + rc = sys_acl_delete_def_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW); + else +#else /* Race-safe default-ACL delete via the held fd or dirfd+leaf. Where * neither is available (xacl_at_available() is false -- the BSDs, a * /proc-less namespace, an un-pinnable entry; every Linux with procfs @@ -1125,6 +1169,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, else if (dirfd >= 0 && xacl_at_available()) rc = xacl_del_default_at(dirfd, leaf); else +#endif /* HAVE_LIBACL_AT */ #endif rc = sys_acl_delete_def_file(fname); if (rc < 0) { @@ -1172,6 +1217,28 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, } #endif #ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* Apply the packed/perm-reconciled ACL (duo_item->sacl) + * through the new libacl *_at calls -- held fd via AT_EMPTY_PATH, + * dirfd+leaf via AT_SYMLINK_NOFOLLOW -- race-safe on every Linux kernel, + * and byte-identical to the path-based sys_acl_set_file() below. */ + if (fd >= 0 || dirfd >= 0) { + int rc; + + if (fd >= 0) + rc = sys_acl_set_file_at(fd, "", AT_EMPTY_PATH, type, duo_item->sacl); + else + rc = sys_acl_set_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type, duo_item->sacl); + if (rc < 0) { + rsyserr(FERROR_XFER, errno, "set_acl: acl_set_file_at(%s, %s)", + fname, str_acl_type(type)); + return -1; + } + if (type == SMB_ACL_TYPE_ACCESS) + sxp->st.st_mode = cur_mode; + return 0; + } +#else /* Race-safe write: serialize the packed (and perm-reconciled) * system ACL to the kernel xattr format and apply it through the * held fd or dirfd+leaf -- never re-resolving fname. This matches @@ -1206,6 +1273,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname, * 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 /* HAVE_LIBACL_AT */ #endif if (sys_acl_set_file(fname, type, duo_item->sacl) < 0) { rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_file(%s, %s)", diff --git a/configure.ac b/configure.ac index f6c077fe..ac947b5d 100644 --- a/configure.ac +++ b/configure.ac @@ -1492,6 +1492,30 @@ struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };]], esac fi +################################################# +# Detect a patched libacl providing the race-safe +# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at, +# ACL_1.3, unreleased upstream). When present we route the race-safe ACL get/ +# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses +# *xattrat; older uses libacl's /proc/self/fd compat). A stock -lacl lacks these +# symbols, so this stays undefined and the build falls back to lib/acl.c; +# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS). +AH_TEMPLATE([HAVE_LIBACL_AT], +[Define to 1 if libacl provides acl_get_file_at/acl_set_file_at/acl_delete_def_file_at]) +if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes"; then + case "$host_os" in + *linux*) + AC_CHECK_LIB(acl, acl_get_file_at, [rsync_have_libacl_at=yes], [rsync_have_libacl_at=no]) + if test x"$rsync_have_libacl_at" = x"yes"; then + AC_CHECK_FUNCS([acl_set_file_at acl_delete_def_file_at], [], [rsync_have_libacl_at=no]) + fi + if test x"$rsync_have_libacl_at" = x"yes"; then + AC_DEFINE(HAVE_LIBACL_AT, 1) + fi + ;; + esac +fi + if test x"$enable_acl_support" = x"no" || test x"$enable_xattr_support" = x"no" || test x"$enable_iconv" = x"no"; then AC_MSG_CHECKING([whether $CC supports -Wno-unused-parameter]) OLD_CFLAGS="$CFLAGS" diff --git a/lib/sysacls.c b/lib/sysacls.c index a5abe408..de74525d 100644 --- a/lib/sysacls.c +++ b/lib/sysacls.c @@ -180,6 +180,26 @@ int sys_acl_free_acl(SMB_ACL_T the_acl) return acl_free(the_acl); } +#ifdef HAVE_LIBACL_AT +/* Dirfd/AT-flag ACL ops via the new libacl, + * race-safe on every Linux kernel. at_flags is AT_SYMLINK_NOFOLLOW (dirfd+leaf) + * or AT_EMPTY_PATH (operate on an open fd passed as dirfd, path ""). */ +SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type) +{ + return acl_get_file_at(dirfd, path_p, at_flags, type); +} + +int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl) +{ + return acl_set_file_at(dirfd, path_p, at_flags, type, theacl); +} + +int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags) +{ + return acl_delete_def_file_at(dirfd, path_p, at_flags); +} +#endif /* HAVE_LIBACL_AT */ + #elif defined(HAVE_TRU64_ACLS) /*--------------------------------------------*/ /* * The interface to DEC/Compaq Tru64 UNIX ACLs diff --git a/lib/sysacls.h b/lib/sysacls.h index c0695974..640890b1 100644 --- a/lib/sysacls.h +++ b/lib/sysacls.h @@ -304,4 +304,10 @@ int sys_acl_delete_def_file(const char *name); int sys_acl_free_acl(SMB_ACL_T the_acl); int no_acl_syscall_error(int err); +#ifdef HAVE_LIBACL_AT +SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type); +int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl); +int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags); +#endif + #endif /* SUPPORT_ACLS */ diff --git a/usage.c b/usage.c index f346385f..ab6063a0 100644 --- a/usage.c +++ b/usage.c @@ -23,6 +23,7 @@ #include "git-version.h" #include "default-cvsignore.h" #include "itypes.h" +#include "lib/acl.h" extern struct name_num_obj valid_checksums, valid_compressions, valid_auth_checksums; @@ -106,6 +107,23 @@ static void print_info_flags(enum logcode f) #endif "ACLs", +#ifdef SUPPORT_ACL_FD +#ifdef HAVE_LIBACL_AT + /* The patched libacl *_at calls apply received ACLs race-safely + * on every Linux kernel (compat layer below 6.13). Emit ACL-at (true, so + * the JSON "ACL_at": true lets acl-symlink-race assert) plus a + * distinguishing libacl token. */ + "ACL-at", + "ACL-libacl-at", +#else + /* Runtime: are the *xattrat syscalls usable, so a received ACL is + * applied race-safely (dirfd+leaf, AT_SYMLINK_NOFOLLOW) even when the + * leaf can't be pinned? False => ACLs are applied via the path on an + * un-pinnable leaf, which carries the parent-symlink-race exposure. */ + xacl_at_available() ? "ACL-at" : "no ACL-at", +#endif +#endif + #ifndef SUPPORT_XATTRS "no " #endif