Only the current release of the software is actively supported. If you need help backporting fixes into an older release, feel free to ask.
Email your vulnerability information to rsync's maintainer:
Rsync Project rsync.project@gmail.com
rsync hardens its security-sensitive operations — path resolution, metadata
application, file/socket creation — against local attacks such as parent-symlink
TOCTOU races. Some of these operations can only be made race-safe with a
primitive the underlying OS provides (an *at() syscall on a held directory fd,
an fdescfs-style /proc/self/fd magic symlink, mknodat(), the *xattrat
syscalls, and so on), and that primitive is not available on every supported
platform.
The guiding rule for those cases is:
On a modern Linux system every issue described in this document is fully addressed. Where an operation can be secured on some platforms but cannot be secured on others, and the residual risk is a local privilege-escalation or data-disclosure class (an attacker who already has write access inside the transferred tree), rsync prefers keeping the operation functional on the platforms that lack the primitive over disabling a long-standing feature for everyone on those platforms.
So a hardened operation takes the race-safe path wherever the platform offers one
and falls back to the historical (path-based, unconfined) behaviour only where it
does not — rather than refusing the operation outright. Each such fallback is an
accepted residual, documented under "Known residuals" below, and on the daemon it
can be turned off per feature with refuse options = .... The residuals are
therefore confined to non-Linux platforms (the BSDs, macOS, Solaris/illumos),
Cygwin, and — for a few features — pre-6.13 Linux kernels; a current, normally
configured Linux deployment carries none of them. (The /proc/self/fd-based
fallbacks assume a mounted /proc, which every standard Linux provides; a
deliberately /proc-less container is the one Linux case that can still hit a
residual.)
The one deliberate exception is an operation whose unconfined fallback would
create a new filesystem object at an attacker-influenceable path rather than set
metadata on the object rsync already transferred: the nested-socket bind() on
platforms without a race-safe socket-create (no bindat()). There the unsafe path
is an out-of-tree write/create primitive, not a same-object metadata race, and a
transferred socket inode is a worthless placeholder, so rsync refuses (skips) it
rather than keeping it functional. A leaf permission change is likewise failed
closed rather than applied through a raced symlink, but only as a rare backstop:
the common file/dir/FIFO case is secured on every platform via fchmod on a held
fd, so no real functionality is lost.
This trade-off applies only to these local-attacker residual classes. Remotely reachable defects — memory safety, authentication bypass, protocol parsing, input bounds — are fixed unconditionally on all platforms, never left as a residual.
rsync treats everything the peer sends — the file list, checksum headers, multiplexed messages, forwarded daemon arguments, filter rules — as untrusted, and bounds-checks it before use. A peer-triggerable crash of a connection's worker process is treated as a defect to be fixed, even though the daemon's fork-per-connection model confines such a fault to that one connection rather than the whole service.
Alongside the issues enumerated elsewhere in this document, the code is hardened continuously through protocol fuzzing (driving the daemon protocol against a writable module) and static analysis, with a CI gate. This release closes a batch of peer-triggerable faults found that way: NULL-dereference and reachable-assert crashes from crafted file lists or indices, reads past a file-list allocation (mostly bounded over-reads of an entry's extra slots), unbounded merge-file and suffix-list recursion, and several bounded out-of-bounds writes driven by peer-supplied lengths or option arguments. Each is fixed at the root with a bounds or validity check plus a defence-in-depth guard at the use site, and carries a regression test.
Two further peer-input hardenings in this release: a peer-supplied I/O-error
value (the MSG_IO_ERROR message and the file-list trailer) is masked to the
defined IOERR_* bits, so a peer cannot set arbitrary error flags in the local
io_error that would be stored and re-forwarded upstream; and control
characters in a (peer-controlled) filename written to the log file are escaped,
so a name carrying C0/C1 terminal-escape bytes cannot inject sequences into an
administrator's terminal when the log is viewed (CWE-117). The number of
equal-weak-checksum blocks hash_search() examines per offset is also bounded
(issue #217), so a crafted or degenerate checksum set with a very long
equal-checksum chain cannot drive the sender's per-offset match-verify into a
quadratic walk and pin one connection's CPU.
Contributors adding code that consumes peer input should validate it at the point of receipt rather than relying on a downstream check.
This section documents how rsync defends against parent-directory symlink races (a TOCTOU / confused-deputy class) and the per-platform approach it takes, so that contributors and automated agents extend the code consistently rather than reintroducing the weakness.
Many rsync operations resolve pathnames that an unprivileged party can partially
control: a receiver writing into a destination tree, a sender reading a source
tree, and temp and partial files, and so on. (The operator-chosen directory
paths — --link-dest/--compare-dest/--copy-dest/--backup-dir/--temp-dir/
--partial-dir — may legitimately point outside the tree, so they are resolved
by the ownership walk described under Symlink defense for operator-supplied
paths below rather than the strict transfer-path resolver here.) If someone who
can write inside that tree races a
parent directory component between a real directory and a symlink ("symlink
flipping"), a path-based syscall — open, stat, chmod, chown, utimes,
rename, unlink, mkdir, mknod, symlink, hard-link creation — can be
redirected to a target outside the intended tree. When rsync resolves that
path with more authority than the component's controller and without a
confinement boundary, this is a confused-deputy bug (e.g. a root nightly backup
capturing /etc/shadow, or a root receiver chmod/chown/unlink-ing a system
file).
O_NOFOLLOW on the final component is not sufficient: the parent
components must be resolved safely.
The boundary that matters is authority plus confinement, not "daemon vs
non-daemon". A non-chroot daemon module, a root-run local transfer, and a
two-user transfer are all unconfined privileged path resolvers. Where a real
confinement boundary already exists (e.g. a per-module chroot) that is the
strongest protection; otherwise rsync must resolve paths defensively.
A chroot is only a boundary for the outer path it confines. A daemon module
written as path = /outer/./inner (use chroot = yes) chroots to /outer but
treats /inner as the module root, so a symlink inside the module that points to
a sibling of /inner is still inside the chroot yet outside the module — the
inner module therefore needs the same defensive resolution as a non-chroot
module. The single gate that decides when hardened resolution applies is
"unconfined privileged resolver": am_daemon && (!am_chrooted || module_dirlen)
for the daemon (any non-chroot module, plus a /./ inner-module chroot), and any
non-chroot receiver. The local sender's content open is confined the same way for
default symlink handling; only the symlink-following modes (-L/--copy-links/
--copy-unsafe-links/-k) and --insecure-links are excluded, so those keep
following symlinks by design.
Resolution of attacker-influenceable paths goes through secure_relative_open()
and the do_*_at() wrappers in syscall.c, never a raw open()/rename()/
chmod() on a full path string. The principle is: trust the operator-named
transfer root, and confine all resolution beneath it, rejecting escapes via
.. above the anchor, absolute symlinks, or out-of-tree symlinks.
secure_relative_open() resolves the parent directory by walking it one
component at a time on a stack of held directory fds, then operates on the final
component with an at-style call on the resulting directory fd.
For per-entry work the receiver and generator go one step further and hold the
parent directory open: open_dir_secure() resolves an entry's directory once
(via secure_relative_open()), held_dfd_for() caches that descriptor for the
duration of the entry, and every operation on the entry — lstat, the temp-file
mkstemp, the temp->final rename, chmod/chown/utimes, mkdir, special-
file and symlink creation, the delta-basis open, and the recursive delete — runs
through that one held fd via an *at() call (do_*_atfd()). Because the
descriptor is pinned to the directory inode, a parent component flipped to a
symlink after the open cannot redirect any of those operations. The alternate-
destination lookups are confined the same way (basis_link_stat() in
generator.c and secure_basis_open() in receiver.c), so a peer-chosen
--link-dest/--compare-dest/--copy-dest basis index cannot reach an
out-of-module file through a symlinked parent.
The sender's source-directory enumeration is confined the same way as its
content open. send_directory() opens each scanned directory through
secure_opendir() — which resolves it via secure_relative_open() /
secure_relative_open_at() and turns the held fd into the DIR* with
fdopendir() — so a parent component raced into a symlink, or (for a daemon
following mode) an in-module symlink pointing outside the module, cannot redirect
the scan to enumerate an out-of-tree directory and leak its entry names, metadata
and symlink targets. For a daemon, both the enumeration and the content open
anchor at the served module root pinned by identity: module_dirfd is opened
(open(".")) the moment the daemon chdirs into the module, while still
privileged, and module-relative paths resolve beneath that fd via
secure_relative_open_at(). Anchoring at the held fd rather than re-resolving the
absolute module path keeps the confinement working after the daemon drops to the
module uid even when the module sits under a directory that uid cannot traverse
(e.g. a 0700 home — re-resolving the absolute path would EACCES), and is
immune to the logical-path-versus-real-cwd skew a followed in-tree directory
symlink would otherwise introduce.
secure_relative_open() resolves a path with a single portable mechanism on
every platform: a per-component walk on a stack of held directory fds. Each
component is opened relative to the held parent with openat(parent_fd, "component", O_NOFOLLOW); descending into a real subdirectory pushes its fd, a
.. pops back to the already-held parent (a pop at the anchor is refused), and an
in-tree directory symlink is followed by reading its target and walking that off
the same stack (absolute targets refused, symlink hops bounded). The final
component is opened O_NOFOLLOW.
Because every component is opened relative to a pinned fd under O_NOFOLLOW,
and .. is resolved by the held-fd stack rather than by the kernel, the walk is
race-free by construction: no rename or symlink swap of any path name can redirect
resolution outside the anchor subtree, and no kernel "beneath" primitive
(openat2(RESOLVE_BENEATH) / openat(O_RESOLVE_BENEATH)) is required. The
confinement is therefore uniform across Linux, the BSDs, macOS and
Solaris/illumos, on old and new kernels alike, with nothing to probe or fall back
to at runtime (and so no openat2/seccomp interaction to worry about in sandboxed
environments). Cygwin is the exception, because its directory descriptors and
symlink emulation do not give the held-fd walk the same inode pinning — see the
Cygwin residual below.
Legitimate in-tree directory symlinks are followed, so --keep-dirlinks /
--copy-links and a symlinked module path keep working. A relative alternate-dest
such as --compare-dest=../01 may legitimately climb to a sibling still inside the
module; such a .. path is re-anchored at the module root and its in-module climb
adjudicated by the walk (the .. pops to the held parent), while escapes above the
anchor are still rejected.
The final operation is hardened as well, following cp: reads use O_NOFOLLOW
so a flipped leaf symlink is not followed, and new or destination files are
created with O_CREAT|O_EXCL (rsync's temporary files use mkstemp) so a
planted symlink at the target cannot be written through. A leaf chmod is the
one operation with no portable no-follow form: it is closed by opening the leaf
O_RDONLY|O_NOFOLLOW and fchmod-ing the held fd (refusing a symlink leaf with
ELOOP), falling back to fchmodat(AT_SYMLINK_NOFOLLOW) and then the
fchmodat2() syscall, and failing closed with a warning rather than ever
chmod-ing through a raced leaf symlink.
- When adding code that performs a path-based syscall on a path that can be
influenced by the remote peer or by another local user, use a
do_*_at()wrapper (orsecure_relative_open()), not a raw full-path syscall. - When introducing a new operation, add a matching
do_<op>_at()wrapper that resolves the parent withsecure_relative_open()and acts via an at-style call on the returned dirfd. - Do not assume a non-daemon transfer is safe; the question is whether rsync has more authority than whoever controls the path components.
- On platforms whose API lacks an at-style equivalent (e.g.
setattrlist()), follow the residuals policy at the top of this document: for a metadata operation on the already-transferred object (ACLs, xattrs, crtimes, permissions) fall back to the path-based call to keep the feature functional and document the residual; but where the unsafe fallback would create a new object on an unconfined path (the nested-socketbind()case), refuse it instead — that is an out-of-tree write/create primitive, not a same-object metadata race, and the lost functionality is negligible.
rsync opens several operator-supplied paths during normal operation. These fall into two groups, both governed by the same ownership-walk policy below:
- operator files:
--log-file,--password-file,--early-input(a client read whose contents are forwarded to the daemon's early-exec),--files-from,--include-from,--exclude-from,--filter=. file,--write-batch,--read-batch, per-directory filter merge files (-C/-F/dir-merge), and on the daemon sidemotd file =,secrets file =,lock file =, andrsyncd.confitself. - operator directories:
--backup-dir,--temp-dir/-T,--partial-dir, and the--link-dest/--compare-dest/--copy-destbasis lookup. These take a directory the operator chose, which may legitimately point outside the transfer tree (--backup-dir=/var/backups), so they are resolved with the ownership walk rather than the strict transfer-path resolver.
The daemon module-root chdir() under
use chroot = no and the non-daemon receiver's chdir() into the
operator-named destination directory are in the same class: both follow
the operator's/root's own symlinked target (the /backup -> /mnt/disk
admin pattern) but refuse one an attacker raced in from another uid,
unless --insecure-links restores the legacy plain chdir().
Each of these reads or writes a path the operator or sender chose, which
may transit attacker-influenceable parent directories (the /tmp/somedir/
class) or be planted directly (the /home/$user/.cvsignore class when
root runs rsync -a /home /backup).
rsync's defense, applied uniformly to all of the above, is a
component-by-component path walk (open_no_attacker_symlinks in
util1.c) that allows symlinks only when the symlink itself is owned
by uid 0 or the running process's effective uid. Symlinks owned by any
other uid are refused with ELOOP at any path component (parent or leaf).
Plain O_NOFOLLOW would be leaf-only and would not defend the
/tmp/somedir/log parent-component plant; this walk does.
The trust model preserves legitimate setups such as /var/log -> /data/log
(root-owned dir-symlink) and a non-root user's own ~/log -> /data/me
symlink; it refuses an attacker's /tmp/somedir -> /attack/path plant.
For --read-batch an additional fstat() check refuses non-regular
files (FIFOs, devices) at the batch path, since the batch content drives
the receiver's protocol parser.
Policy. A symlink at any path component (parent or leaf) is followed
iff it is owned by uid 0 or the process's effective uid, and refused (ELOOP)
otherwise, identically for absolute and relative operator paths. The trust
signal is authority (ownership), not location: an operator path may
legitimately point outside the transfer tree, so it cannot be confined by
location the way a transfer path is. This is deliberately distinct from the
transfer-path resolver secure_relative_open() (see Symlink-race-safe path
resolution above), which refuses all symlinks and anchors beneath the
transfer root — correct for peer-named paths, which never legitimately escape.
For the operator directory paths, a refused symlink simply makes the target look
absent (no backup/temp/basis is taken through it) and the transfer proceeds
normally; the operator's own symlinked target keeps working.
The daemon exclude/filter chain is not a symlink boundary. The daemon
filter chain (exclude, exclude from, filter, …) matches the logical
module-relative name of each item, not the physical file it resolves to. It
is a visibility/tamper filter — a peer cannot name a daemon-excluded path to
pull, push to, or delete it — but it is not a security boundary against
symlinks: an in-module symlink whose own name is not excluded can be followed to
an excluded target (the name the filter sees, e.g. link, is not the excluded
name, e.g. secret). This is by design and is the long-standing behaviour of
stock rsync; the defense for a writable module against symlink trickery is
munge symlinks (enabled by default for a writable, non-chrooted module), not
the filter. Do not rely on exclude/filter to confine a peer who can introduce
or traverse a symlink; see rsyncd.conf(5) ("filter" and "munge symlinks").
What is enforced for a peer-supplied operator path (--partial-dir,
--backup-dir, the alt-dest basis) is confinement to the module root: the
ownership walk refuses a foreign-uid symlink (the symlink-race defense) and
refuses a resolved target outside the module. That module-boundary confinement
is independent of exclude/filter — it holds whether or not the module sets an
exclude — and is what the operator-path tests cover.
--insecure-links. This flag is a local opt-out that restores the legacy
follow-any-symlink behaviour for the paths above. It is not forwarded to the
remote (a remote-shell peer that wants the opt-out must set it on its own side,
e.g. via --rsync-path), and a daemon never honors it: the opt-out predicate
reads the client-controllable flag only off a daemon, so a peer-forwarded or
-M-injected --insecure-links cannot weaken a daemon's confinement — the daemon
additionally hard-refuses it (drops the connection) via the refused-options path.
A daemon admin who wants the legacy behaviour for one isolated/trusted module
sets insecure links = yes in that module's rsyncd.conf stanza (see
rsyncd.conf(5)); this is admin-only and re-opens the symlink-escape
vulnerabilities for that module on purpose. The operator-path-* and
insecure-links-* tests enforce this consistency across every path-taking
option and across absolute/relative, leaf/parent, and same-uid/cross-uid plants.
For support/rrsync (the SSH-restricted-rsync wrapper), the same TOCTOU
class is closed in Python by opening each validated path component with
O_RDONLY|O_NOFOLLOW, verifying via readlink('/proc/self/fd/N') that the
pinned inode is still in-tree, and passing /proc/self/fd/N as the exec'd
rsync's argument (so the kernel routes the child's open through the pinned
inode rather than re-resolving the path). A receiver-side new destination
has no inode of its own yet, so its existing parent directory is pinned the
same way and the leaf is created at /proc/self/fd/<parent>/<leaf>. This pin
relies on an fdescfs-style magic symlink and is not available on every
platform -- see the rrsync residual below.
The following are documented as out of scope for this release:
-
The source-directory enumeration confinement needs
fdopendir()(to form aDIR*from the securely-resolved held fd) anddirfd(); on a platform lacking either,send_directory()falls back to the legacyopendir()on the path, so the scan is unconfined there — the same resolver-fallback shape as the other*at()-less residuals. Every current target provides both; the per-entry operations and the content open remain confined regardless. -
On Cygwin, the per-component held-fd walk does not provide the same inode-pinning guarantee as on a POSIX kernel: Cygwin tracks a process's current directory and resolves directory descriptors by path name rather than by a pinned inode, and emulates symlinks as special files. Static out-of-tree symlinks are still refused (the walk sees and rejects them), and a daemon module path anchored at an absolute
module_diris confined; but an entry whose parent component is raced from a directory to a symlink mid-resolution can still slip past confinement that is anchored at the process CWD (e.g. the sender's content open), because the descriptor is not bound to the original inode. The parent-component symlink-race tests are therefore not enforced on Cygwin (seeRSYNC_EXPECT_SKIPPEDin.github/workflows/cygwin-build.ymland the Cygwin-only xfail insymlink-race-source_test.py). Cygwin is a development/interoperability target, not a privilege boundary host, so this is accepted for this release. -
On a platform with no
mknodat()at all -- macOS before 13 is the supported example, wheremknod()andmkfifo()exist but neithermknodat()normkfifoat()does -- creating a device node or FIFO falls back to plaindo_mknod(), which resolves the whole path by name. What is lost is the pinned parent: the directory components are re-resolved by the kernel at create time, so an attacker who can swap a parent component races the create and can place the node outside the transfer. The final component is not at risk --mknod()andmkfifo()do not follow a symlink at the leaf, they failEEXIST. WhereAT_FDCWDexists -- which is every platform rsync 3.5.0 supports, macOS 10.13 included -- fake-super placeholders still return throughopenat(..., O_NOFOLLOW), reached before either*atprimitive is tested, so ordinary in-tree placeholder creation stays confined; fake-super loses parent confinement and theO_NOFOLLOWleaf only on the paths that reach plaindo_mknod()(the cache-declined/cross-tree wrapper and the backup paths). On a build with noAT_FDCWDat all there is no fd-relative primitive of any kind, so nothing above applies and every special-file create, fake-super included, is unconfined. Transferring specials there (--devices,--specials) carries the parent-component race.symlink-mknod-fakesuper-symlink-raceskips itself on such a build, since the property it asserts is one the build deliberately does not have. -
On platforms where
mknod()/mknodat()cannot create a socket inode (the BSDs, macOS, Solaris), a transferred socket is recreated withsocket()+unlink+bind(path), which cannot be confined (there is no portablebindat()). Linux creates it race-safely withmknodat()on a held dirfd; on the others a nested socket is skipped with a warning rather than bound on an unconfined path, leaving only a top-level, operator-named socket binding by path. -
support/rrsync's race-free inode-pin -- of both existing path components and a new destination's parent -- depends on materialising a held fd as a path that the exec'd rsync re-resolves to the same inode. rrsync validates and pins in its own process, but it then execs a separate rsync that re-resolves the paths fromargv, so the confining reference must be expressible as an argument. A held dirfd is not: it is usable as a path only through an fdescfs-style magic symlink. rrsync implements this for Linux only, via/proc/self/fd/N; it does not use the/dev/fd/Nequivalent that macOS/FreeBSD expose withfdescfsmounted. So on every non-Linux platform (the BSDs, macOS, Solaris -- whose/proc/self/fdentries are not magic symlinks -- and Cygwin), and on a/proc-less Linux namespace, rrsync falls through to the realpath-validated path unpinned, so a parent-component or between-pin-and-exec flip remains possible there; a deeper-Rnew path whose parent does not exist yet is likewise unpinned. The portable closure is an rsync-side fd-passing API -- rrsync hands rsync the confined dirfd (inherited acrossexec) and rsync resolves that argument relative to it with the samesecure_relative_openresolver the daemon uses, needing no magic-symlink filesystem -- a protocol/CLI addition under discussion on the rsync-security list. -
The operator-directory ownership walk refuses a foreign-owned symlink on a
--backup-dir/--temp-dir/--partial-dir/--link/compare/copy-destpath, so a statically planted symlink is rejected and the dependent operation does not escape. Both the data writes and the source-metadata reads of those operations are now confined to held no-follow fds: the--copy-destcopy_file()/copy_xattrs()source read goes through the held basis content fd (sys_fgetxattr), andmake_backup()reads the backed-up file's ACL/xattrs through abackup_source_fd()-pinned fd -- so a parent-component flip can no longer redirect them to disclose an out-of-module value. The cross-tree metadata apply on those leaves (the%stat/ACL/xattr write on a--temp-dir/--backup-dirstaging file) is fd-pinned the same way, now including under--fake-super: theset_file_attrs()no-follow leaf fd was previously opened only whenam_root >= 0, so afake super = yesdaemon fell back to a path-basedsys_lsetxattr()/chmod a raced parent could redirect; the pin is now opened for fake-super too (a raced leaf is refused, not redirected). Two narrow follow-ons re-resolve the (now-validated) operator path by name and remain a post-validation parent-component race:- the in-place backup (
--inplace --backup) writes the backup file's data through a confined create, but itsset_file_attrs()metadata set (chmod/chown/times) re-resolves the--backup-dirpath by name afterwards (it is not placed under operator mode, which would force the sharedset_file_attrs()path off its held-O_NOFOLLOW-fd xattr write and re-open the very parent-symlink xattr racecopy-xattrs-symlink-racepins closed); and - the abbreviated-xattr optimisation reuses a basis xattr value for the
destination only when its checksum matches the digest the sender sent; that
basis read (
rsync_xal_set()) re-resolves the basis path by name. This is a constrained checksum-oracle, not a disclosure: it confirms that some raced out-of-module xattr hashes to a value the sender already chose, rather than copying an unknown value onto a readable file, and needs a colluding sender plus a local racer. An attacker who flips a parent component in the window after the confined data write/stat can thus still affect those narrow metadata/oracle operations. This is the same local-attacker post-confinement TOCTOU class as the ACL/crtimes residuals below; the data-write and direct source-read escapes are closed, and--insecure-links(or a module'sinsecure links = yes) is orthogonal to it.
- the in-place backup (
-
POSIX ACL application (
-A/--acls) is race-safe on every Linux kernel — 6.13+ via the*xattratsyscalls (or a patched libacl's*_atbindings), and older kernels via the/proc/self/fdcompat that pins the same inode, providedprocfsis mounted — and a transferred file/dir/FIFO has its xattrs (-X) applied through the held no-follow fd, so the apply cannot be redirected by a raced parent component. Where neither primitive is available — the BSDs, Solaris and macOS (no*xattratsyscalls and no/proc/self/fdmagic symlinks), plus the edge case of a Linux instance with no usable/proc(a/proc-less container/namespace) — the ACL apply falls back to the path-basedacl_set_file()/sys_acl_*file()calls — the long-standing 3.4.x behaviour — to keep--aclsfunctional rather than silently skipping it, so a parent-component flip can have the received ACL written onto an object outside the module/destination boundary (and, because the attacker controls the ACL bytes, granted to a chosen uid). As with the macOS crtime tier below, this is an accepted residual under the functionality-over-refusal policy; a daemon operator who does not want it can disable the feature withrefuse options = acls. -
macOS creation-time (
--crtimes) preservation uses the path-basedsetattrlist()/getattrlist()withFSOPT_NOFOLLOW, which protects only the final component; there is nosetattrlistat()targetingATTR_CMN_CRTIME. As with POSIX ACLs where the OS offers no race-safe primitive,--crtimesis kept functional (daemon and non-daemon) and the parent-component symlink race is an accepted residual: an attacker who flips a parent component can have a crtime read/write target an object outside the module/destination boundary. The mtime/atime path is not affected --set_times()resolves it race-safely throughutimensat()on a held dirfd in hardened mode. A daemon operator who does not want the crtime residual can disable the feature withrefuse options = crtimesinrsyncd.conf. -
Pulling with
-o/-g(or-a) as root from an untrusted sender is by design a trust relationship, not a confinement boundary: the sender dictates each received file's owner/group, including uid/gid 0. rsync maps the sender's id/name pairs through the local id database; an empty or unknown sender name falls back to the sender's numeric id (the value--numeric-idswould use), and a sender can equally request root via the literal nameroot. A root receiver must therefore only pull with-o/-gfrom a trusted source (or use a non-root receiver / a uid-gid policy). The daemon name-converter path is guarded separately — an unknown name there maps to the sender's numeric id rather than 0 (seeclientserver.c).
Daemon authentication is a secret-prefix challenge-response: the client returns
base64(H(secret || challenge)), where H is a digest the two sides negotiate.
The negotiation is unauthenticated and ordered by the connecting side, and the
md5/md4 digests remain available for backward compatibility, so a peer that
sends no digest list (any rsync before 3.2.0, including the openrsync that ships
with macOS) falls back to md5 (or md4 below protocol 30), and an on-path
attacker can rewrite the negotiation to force md5/md4 even between two modern
peers. This is not an authentication bypass — md4/md5 have no practical
preimage break — but a weak digest makes a captured (challenge, response)
pair far cheaper to brute-force offline, recovering a guessable shared secret.
The challenge itself is seeded from the kernel CSPRNG (/dev/urandom), so it is
an unpredictable per-connection nonce. An earlier time/pid-based challenge was
low-entropy enough (~35 bits) that recovering the (sec, usec, pid) tuple from
one observed challenge let an on-path observer predict every subsequent challenge
from that daemon process and pre-compute a dictionary against a captured
response. (If /dev/urandom is unavailable the daemon logs a warning and falls
back to the legacy time-based challenge rather than a constant.)
A daemon operator whose clients are all modern (rsync 3.2.7+ built with openssl,
when the SHA digests were added) can require a strong digest with the auth digest module parameter, e.g. auth digest = sha256, which refuses any
connection that negotiates — or falls back to — a weaker digest (see
rsyncd.conf).
Residual: there is no default floor, because requiring one would break every
pre-3.2.0 client (notably the macOS-bundled openrsync, which authenticates only
with md4). An operator who cannot raise the floor should run the daemon behind
a verified TLS transport (rsync-ssl/stunnel) or over ssh — which removes the
on-path capture/downgrade vector at the transport layer — and should use a
high-entropy shared secret, which is infeasible to brute-force regardless of the
digest.