recv_file_list() OR's the wire-supplied end-of-list error value straight
into the local io_error at three sites (the varint-flags path, the
XMIT_IO_ERROR_ENDLIST byte path, and the protocol < 30 int flag). Like
the MSG_IO_ERROR path fixed in io.c, a malicious peer could set arbitrary
undefined bits, which then accumulate in io_error and can be re-forwarded
to other peers.
Mask each with IOERR_VALID_MASK so only the defined IOERR_* bits survive,
matching the io.c MSG_IO_ERROR handler.
(cherry picked from commit 9da5b5450f)
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.
hashtable_create() and the grow path computed the slot-array byte count as
new_array0(char, size * node_size) in 32-bit int arithmetic; for a large
peer/data-driven size the product wrapped to a tiny value, bypassing my_alloc's
--max-alloc guard (which only saw the already-wrapped count), so the table was
under-allocated while tbl->size kept the huge size -- a later node access then
ran out of bounds (heap overflow; ASan-confirmed). Pass size and node_size as
SEPARATE factors so my_alloc checks both before multiplying; guard each *2
doubling against int overflow BEFORE it happens; make HASH_LOAD_LIMIT divide
before multiplying; promote the HT_NODE index multiply to size_t; and test
size < 16 first so a negative req short-circuits the size-1 (INT_MIN UB).
flist_expand()'s int growth math (used+extra and *=4 / *=2 / += FLIST_LINEAR)
could overflow past INT_MAX on a very large file list; guard each operation
before it overflows and refuse rather than under-size the realloc.
(Leonid Bugaev May-2026 re-audit, KI-11/12/13.)
Five error-path/cleanup memory leaks found by an external audit:
- flist.c send_file_name: free the ACL loaded by get_acl() when a later
get_xattr() fails (and on the get_acl error path).
- xattrs.c copy_xattrs: free the xattr datum buffer when the setxattr fails.
- generator.c recv_generator: free real_sx at the cleanup label (the
directory branch loaded its ACL via set_file_attrs but only the
regular-file path freed it); zero-init real_sx so the early gotos are safe.
- uidlist.c send_one_list: free the strdup'd id-0 name after send_one_name.
- clientserver.c start_inband_exchange: free modname on the early error
returns (it was freed only on the success path).
ASan/LSan regression tests cover the generator, uidlist and clientserver
leaks; the flist and xattrs leaks need a forced syscall failure and are
covered by the audit's standalone harnesses.
Reported-by: Leonid Bugaev <leonsbox@gmail.com>
(cherry picked from commit 078f3b99f4004510d418ee9d97d9b775dc8587bc)
recv_file_entry (normal and XMIT_HLINKED abbrev branches) and make_file
left F_RDEV_P / the symlink-name slot uninitialized when the matching
preserve option was off, so a later read walked into adjacent pool
memory. Empty the symlink name when !preserve_links, zero F_RDEV_P when
!preserve_devices, and mirror both in the abbreviated branch. receiver.c
saves/restores the --write-devices S_IFBLK mode flip around receive_data
so dest_mode() never sees the mutated mode.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Route operator-supplied paths (--partial-dir, --backup-dir, alt/copy/link-dest,
--temp-dir, --files-from, --log-file, config and secrets files) and the sender's
own enumeration through the race-safe resolver and an ownership walk that refuses a
target redirected outside the module root. open_tmpfile() now creates the temp via
the cached held dir fd (do_mkstemp_atfd) so an in-tree dir symlink is followed
safely instead of refused. Adds --insecure-links to opt back into legacy following;
the daemon hard-refuses the client flag (use the "insecure links" module param).
In incremental-recursion mode the receiver frees each flist as it is
finished with. When the last one goes, flist_free() sets first_flist
to NULL, flist_cnt to 0 and pool_destroy()s the shared file pool, but
the global dir_flist is left untouched: its files[] array still points
into the now-destroyed pool.
A malicious sender can then send an NDX_FLIST_OFFSET-encoded directory
index. read_ndx_and_attrs() bounds-checks dir_ndx against the *stale*
dir_flist->used and calls recv_file_list(), which:
- reads/writes the freed dir_flist->files[dir_ndx] entry
(FLAG_GOT_DIR_FLIST), and
- because first_flist is NULL, gets ndx_start == 1 from flist_new(),
so re-creates dir_flist as a fresh empty FLIST_TEMP list, and then
- dereferences dir_flist->files[dir_ndx] (uninitialised malloc heap)
in the dirname check, crashing in f_name().
On a stock glibc the 256 KiB realloc is mmap-zeroed, so the uninit
deref is caught by the existing !d guard and turns into a clean
RERR_UNSUPPORTED exit, but the use-after-free write into the destroyed
pool happens regardless.
A sub-flist marker after every flist has been freed is never valid
protocol, so refuse it up front before touching the stale dir_flist.
(cherry picked from commit 382d7df9a23b976ea6f09598ecea66f269aa213d)
The F_HL_GNUM/F_HL_PREV optional-extras slots ("non-dirs") and the
F_DIR_DEV_P slots ("receiver dirs") occupy the same OPT_EXTRA offsets;
the macros assume an entry is one or the other. recv_file_entry()
suppressed FLAG_HLINKED for symlinks and specials/devices on platforms
that cannot hard-link those, but never for directories, so a malicious
sender could mark a directory entry XMIT_HLINKED.
With -H -x and inc_recurse the generator then writes
major/minor(st_dev) into F_DIR_DEV_P(file) (generator.c:1679),
clobbering what the hard-link code reads as F_HL_PREV/F_HL_GNUM. A
forwarded MSG_NO_SEND for that ndx pushes it onto hlink_list
(got_flist_entry_status() only checks F_IS_HLINKED), and
check_prior() reads F_HL_PREV(file) == major(dev) — typically 0.
flist_for_ndx(0) returns first_flist (ndx_start-1 is the parent
sentinel), and flist->files[0 - ndx_start] underflows the files[]
array by one slot, then dereferences the resulting wild pointer.
Directories cannot be hard-linked on POSIX; do not honour XMIT_HLINKED
on an S_ISDIR entry.
(cherry picked from commit 63be23ed575a24430b058d54724a643c89344b5b)
A malicious sender can set XMIT_HLINKED on a file-list entry even when
the receiver was not invoked with -H. recv_file_entry() only reserves
the (inc_recurse+1) hard-link extras slots when preserve_hard_links is
set (the gate at create_object), but it set FLAG_HLINKED on the struct
unconditionally from the peer's xflags. HLINK_BUMP() then offsets
F_SUM() and F_RDEV_P() past the start of the pool_alloc()'d region,
and the subsequent read_buf(f, F_SUM(file), flist_csum_len) writes 16
attacker-controlled checksum bytes 8 bytes below the allocation — into
the adjacent file_struct in the same pool extent.
Vanilla ASAN does not see this because lib/pool_alloc.c bumps inside a
single 32 KiB malloc; it surfaces as use-after-poison once the pool is
instrumented with ASAN_POISON_MEMORY_REGION/ASAN_UNPOISON_MEMORY_REGION.
Mirror the gating already present at the extra_len computation and at
the F_HL_GNUM/F_HL_PREV write site: only set FLAG_HLINKED when
preserve_hard_links is enabled. This also closes the symmetric
F_RDEV_P() underflow for device entries and the F_SUM(first)/
F_RDEV_P(first) reads in the first_hlink_ndx copy path.
(cherry picked from commit adfae919876c9f3454ce12a1b7becbed47415ff7)
Under inc_recurse the receiver appends every received directory entry to
dir_flist before flist_sort_and_clean() runs. If the peer sends entries
that the dedup/prune pass then clear_file()s, the cleared file_struct
(basename and mode zeroed) remains in dir_flist->files[]. A later
sub-flist tagged with that dir_ndx passed the existing >= used bounds
check, and the dirname-validation strcmp() in recv_file_list() then
called f_name() on the cleared entry — which returns NULL — and
dereferenced it: a remote NULL-pointer crash on any anonymous writable
module.
Reject a dir_ndx whose entry is no longer active alongside the existing
range and duplicate-flist checks, and guard the strcmp() against a NULL
f_name() result as defence in depth.
Found by fuzzing the daemon protocol over stdio.
(cherry picked from commit a900b25008151a8a8ec1c34aa7e2914b23416019)
The directory-scan confinement resolves the opendir and the per-entry stat
through the held scan dirfd, but readlink_stat() still read the symlink target
with a path-based do_readlink(): a raced parent symlink could redirect that read
and leak an out-of-tree symlink target for a same-name entry.
Add do_readlink_atfd() (readlinkat via the held dirfd) and scan_readlink(), which
routes the read through scan_dirfd for an entry directly in the scan dir (the
same gate scan_link_stat uses), else falls back to do_readlink(). Completes the
enumeration confinement's symlink-target leg.
The sender/daemon enumerated each source directory with a plain opendir()
on the full accumulated path -- not through the secure resolver -- so the
enumeration itself was unconfined: a parent component raced into a symlink
after the scan let the listing (and per-entry lstat) escape the transfer
root / module.
- secure_opendir() resolves the directory through the secure resolver and
turns the held fd into a DIR* via fdopendir(); the daemon anchors it at
the identity-pinned module_dirfd (captured at module setup while the
daemon was positioned at the module root, before any privilege drop);
- the per-entry scan stats through the opendir fd (scan_dirfd) with
fstatat(), so the listing can't be redirected out of the tree.
Closes daemon-subdir-climb-symlink and sender-scan-dir-escape.
Behaviour-neutral held-dirfd conversion (the substrate the broadened gate
hangs off):
- set_file_attrs() resolves the entry's dir once (held_dfd_for) and issues
chown/chmod/times as single-component *at() calls against that held fd;
- the generator routes per-entry stat/mkdir/chmod through a held dirfd;
- the receiver reads the delta basis via the held dirfd and resets the
one-slot dir-fd cache at flist-chunk boundaries;
- the sender scan stats entries via the opendir fd.
Backport of two 3.5.0 daemon-filter fixes:
- parse_filter_file(): strip module_dirlen off an absolute merge-file
path before the daemon_filter_list check (parse_merge_name() prepended
module_dir), so an anchored module-relative rule actually matches; a
hidden merge file is treated as non-existent rather than tripping the
fatal-vs-silent oracle (Mitchell Benjamin finding 1);
- downgrade implied-parent directories to non-content so a malicious
sender cannot expand --delete scope via a content-dir entry
(Mitchell Benjamin finding 3).
Tests: daemon-filter-merge-bypass, malicious-sender-delete-scope.
Reported-by: Mitchell Benjamin <mitchell@revampstudio.com.au>
--delete-missing-args (missing_args==2) sends a missing --files-from arg as a
mode-0 entry (IS_MISSING_FILE), the generator's delete signal. The mode-type
validation in recv_file_entry() rejected mode 0 as an invalid file type,
aborting the transfer with 'invalid file mode 00 ... code 2' before the
generator could act (a regression from 3.4.1). Allow mode 0 through only when
missing_args==2 (the delete mode -- not --ignore-missing-args, which never
sends a mode-0 entry); all other modes are still rejected.
Thanks to @mgkeeley for the report (#910).
(cherry picked from commit 73fe630a70)
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>
when we open a file that we don't expect to be a symlink use
O_NOFOLLOW to prevent a race condition where an attacker could change
a file between being a normal file and a symlink
When conversing with a protocol 29 or earlier rsync, the modtime values
are arriving as 4-byte integers. This change interprets these short
values as unsigned integers, allowing the time that can be conveyed to
range from 1-Jan-1970 to 7-Feb-2106 instead of the signed range of
13-Dec-1901 to 19-Jan-2038. Given that we are fast approaching 2038,
any old-protocol transfers will be better served using the unsigned
range rather than the signed.
It is important to keep in mind that protocol 30 & 31 convey the full
8-byte mtime value (plus nanoseconds), allowing for a huge span of time
that is not affected by this change.
- Size flist checksum data to hold the active size, not the max.
- Add a negotiated hash method to the daemon auth code.
- Use EVP for all openssl digests. This makes it easy to add more
openssl digest methods and avoids deprecation warnings.
- Support a way to re-enable deprecated digests via openssl conf
file and allow a default file to be configured.
- Supply a simple openssl-rsync.cnf file to enable legacy digests.
In 2004, an allocation optimization has been added to the file
list handling code, that preallocates 32k of file_struct pointers
in a file_list. This optimization predates the incremental
recursion feature, for which it is not appropriate anymore. When
copying a tree containing a large number of small directories,
using the incremental recursion, rsync allocates many short
file_lists. Suddenly, the unused file_struct pointers can easily
take 90-95% of the memory allocated by rsync.
If the receiving side read a hard-linked device, it needs to set the
value of rdev_major to the value it snags from the hard-linked data
because the sender set their rdev_major value for that file entry.
- Use C99 flexible arrays when possible, and fall back on the existing
fname[1] kluges on older compilers.
- Avoid static initialization of a flexible array, which is not really
in the C standard.
- 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.