Solaris has facl(2), which performs ACL operations on an already-open file
descriptor. This adds Solaris fd-based helpers for the ACL operations that
rsync needs while keeping the existing path-based fallback for callers without
a held fd.
On Solaris, setting an ACL on a directory replaces the combined access and
default ACL set. The path-based sys_acl_set_file() already handled this by
reading the other half of the directory ACL, merging access and default entries,
and then calling acl(..., SETACL, ...). The new sys_acl_set_fd_type() preserves
that behavior with fd-based operations: it uses fstat() to identify directories,
reads the other ACL half with sys_acl_get_fd_type(), combines the access and
default entries, marks default entries with ACL_DEFAULT, and finally writes the
combined ACL with facl(2). Deleting a default ACL similarly fetches the access
ACL through the fd and rewrites only that access ACL with facl(2), which removes
the default ACL without re-resolving the path.
Driving the apply off the held fd also closes a symlink-race on the Solaris ACL
apply (the path-based sys_acl_*file re-resolves the path -- the CVE-2026-53799
class, unfixed on Solaris until now). Added on integration: for a root receiver
a missing held fd on a confined receiver means the leaf was raced to a symlink
(acl_set_file follows it), so refuse the path-based set/delete rather than apply
the ACL to a redirected inode; a plain non-root receiver keeps the path-based
fallback for a legitimately un-pinnable owned leaf (e.g. a 0300 dir), matching
the operator-path op_pin rule.
The hardened receiver confined the destination side of an xattr/ACL copy (the
fsetxattr/acl_set_fd through a held O_NOFOLLOW fd) but still read the SOURCE side
by path: copy_xattrs() did get_xattr_names/get_xattr_data on the source path, and
make_backup() cached the backed-up file's ACL/xattr via get_acl()/get_xattr() by
path. A local module writer could race the source/basis parent to a symlink
after the confined content/stat open and before that path-based metadata read,
so out-of-module xattrs/ACLs got copied onto an in-module destination or backup.
Thread a source fd through the read side, mirroring the existing dest-fd plumbing:
- get_xattr_data(), get_xattr() and get_xattr_acl() gain an fd arg (get_xattr_names
already had one) and use sys_fgetxattr/sys_flistxattr when fd >= 0; this also
covers the --fake-super ACL-as-xattr read in get_rsync_acl().
- copy_xattrs() gains a source_fd; copy_file() passes its held source fd (ifd)
and keeps it open across the xattr copy (closing it on the fsync error path
too); gen_entry_copy_xattrs() O_NOFOLLOW-opens the basis leaf under the
confined resolver (with O_DIRECTORY for a directory basis) and passes it.
- make_backup() pins the source leaf with a confined O_NOFOLLOW fd
(backup_source_fd, like set_file_attrs's op_leaf_fd) and reads its ACL via
get_acl_fdat() and its xattrs via get_xattr(fd); the in-place delta-backup in
the generator pins fname the same way. On a hardened receiver a raced/absent
leaf skips the cache rather than reading through a flippable path.
Non-hardened receivers (fd < 0) keep the path-based behaviour unchanged. The
basis COMPARE reads (the generator deciding a match) stay path-based: they never
copy out-of-module metadata onto a file, so they are not part of this sink.
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.
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.
When the receiver cannot pin an entry with a held fd and the kernel has no
*xattrat syscalls (pre-6.13 Linux, the BSDs, Solaris, ...), the hardened ACL
path previously skipped the apply with a warning and reported success -- it
silently left the destination ACL stale, so a changed or revoked source ACL
did not propagate yet rsync exited 0.
Per project policy, prefer the documented --acls functionality over refusing it
on platforms that cannot offer the race-safe primitive (Linux 6.13+ takes the
secure xacl_*_at() path, and an operator who needs the guarantee can move
there). The get / default-ACL-delete / set fallbacks now fall through to the
path-based sys_acl_*file() calls -- the long-standing 3.4.x behaviour -- which
restores correct ACL application at the cost of the parent-symlink race that is
unavoidable on those platforms.
Tests: a new acls-unpinnable test asserts a received ACL updates (and removes a
stale grant from) a no-owner-read (0300) destination directory -- validated on
the path-based fallback via a forced-no-xattrat build and on the xattrat path;
its non-Linux / proto-29 skips are added to the per-platform expect-skip lists.
acl-symlink-race PASSES with a note (rather than skipping) where -VV reports no
race-safe primitive, so its skip set stays kernel-independent -- the same
workflow's RSYNC_EXPECT_SKIPPED is shared by no-xattrat and 6.13+ boxes.
set_file_attrs() holds an O_RDONLY|O_NOFOLLOW fd (held_fd) for the entry's
regular-file/dir/FIFO inode so metadata is applied to the pinned inode rather
than by re-resolving the path, which a parent-symlink race could redirect.
- xattrs: the set/remove/list of -X xattrs use the f-variant calls on held_fd
(lib/sysxattrs gains the wrappers); a raced FIFO can't block the open
(O_NONBLOCK).
- ACLs (real root): get_acl_fdat()/set_acl_fdat() apply the ACL via lib/acl.c's
fd path (xacl_*_fd), or setxattrat(AT_SYMLINK_NOFOLLOW) on the dirfd+leaf for
a socket/device, covering both the access and default ACL. pack_smb_acl()+
change_sacl_perms() still build the entries, so the bytes written match
acl_set_file() exactly. With no safe primitive we skip rather than re-resolve
the path. The legacy path-based libacl calls remain only for the cross-tree
(no held dirfd) and non-Linux cases.
- --fake-super ACL-as-xattr writes/deletes also go through held_fd, closing the
last path-based metadata write under fake-super.
Multiple receiver-side fields read from the wire were trusted
without upper-bound checks. A hostile peer could either request
extreme allocations (DoS via --max-alloc) or, on platforms where
read_varint returned a negative value, push ~SIZE_MAX through the
size_t conversion to wrap downstream length checks.
Introduce read_int_bounded(), read_varint_bounded() and
read_varint_size() in io.c so wire-derived integer ranges are
checked at the read site rather than scattered across each
caller, with RERR_PROTOCOL on out-of-range input.
Apply the bounded primitives to:
- sum->count (checksum count -- previously could overflow
(size_t)count * xfer_sum_len on 32-bit with raised max-alloc)
- xattrs: count, name_len, datum_len, plus rel_pos overflow
detect to stop chain wrapping the num accumulator
- acls: ida-entry count
- flist: file mode S_IFMT validation, modtime_nsec range check
- delete-stat counters in main: per-summand cap so the total
can't overflow a signed 32-bit accumulator
Reporters include Joshua Rogers (checksum-count overflow finding).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since 05278935 (- Call mkdir_defmode() instead of do_mkdir(). - Define
orig_umask in this file, not options.c. - Made orig_umask a mode_t, not an
int., 2006-02-24), the type for the global was changed, and therefore on
systems where sizeof(mode_t) != sizeof(int), writes or reads to them will
overflow to adjacent bytes.
Change the type to the one used everywhere else and avoid this problem.
While at it, silence again a warning that is being triggered by
Apple's clang 15.
- All the memory-allocation macros now auto-check for failure and exit
with a failure message that incudes the caller's file and lineno
info. This includes strdup().
- Added the `--max-alloc=SIZE` option to be able to override the memory
allocator's sanity-check limit. It defaults to 1G (as before).
Fixes bugzilla bug 12769.