A restricted dir must not let a client have the spawned rsync create
devices or special files in the served tree, but -a bundles -D into the
client's short options, so refusing -D outright breaks every ordinary
`rsync -a`. 88cee089 forced --no-D instead. That option also clears
the rdev framing, and rrsync sets it on one end only, so the file list
desynchronised: a FIFO push hung at protocol 29 and corrupted the list
at 30, and a device push failed at every protocol including 32. Use
--drop-D, which withholds the creation without touching the wire.
Only on the receiving side. A sender creates no received device or
special entry in the served tree, so there is nothing to deny and
--drop-D is a no-op there; forcing --no-D on a pull was the same
one-sided change in the other direction, and broke pulls the same way.
3.4.4 forced nothing at all and is the behaviour a pull now gets back.
rrsync-specials-denied asserted the option on a "--server --sender"
command line, which conflated the two directions. It now checks that
the receiving side forces --drop-D and still forwards the client's own
-D -- without which the two ends frame the list differently again --
that the sending side forces neither, and, rather than only what is
forwarded, that a real push cannot create a FIFO while the rest of the
transfer succeeds. An ordinary file alongside is the control, since a
push that failed outright would "deny" the FIFO too.
The device case gets its own push, because a device desynchronises at
every protocol while a FIFO only does so below 31. It needs no mknod
privilege: rsync's fake-super "%stat" xattr is what makes a file a
device to rsync, and running the RECEIVER under --fake-super too lets it
record one without privilege -- so the case asserts that nothing of that
name appears, not merely that the transfer survived.
rrsync-pull-arg-shapes could previously assert only that pulling a FIFO
did not hang, because forcing the option on that side broke the transfer
outright. It now requires the pull to succeed and deliver a FIFO, which
is what a pristine 3.4.4 rrsync does.
(cherry picked from commit 2fbb708f15)
Two shapes a pristine 3.4.4 rrsync transfers, and 88cee089 broke, both
from one cause: the pin opens the argument's CONTENT, when for a sender
rsync often only needs to name or describe it.
* an in-tree FIFO wedged rrsync before exec. O_RDONLY on a FIFO blocks
until a writer appears, so an authorised user naming one could
accumulate stuck processes indefinitely.
* an in-tree dangling symlink failed the transfer. realpath() resolved
it to a missing target and the ENOENT was reported as a detected
race, though a dangling link is an ordinary archive entry that rsync
transmits by its target string without opening anything.
So only a regular file or a directory gets its content opened; anything
else keeps the realpath()-validated name. The sender never opens these,
it only describes them.
The leaf is still spelled beneath a pinned directory. An
earlier form of this commit left the bare name for rsync to re-resolve,
on the reasoning that 3.4.4 passes it that way -- but that puts every
component back in play and reintroduces CVE-2026-53783 for the shape:
with an in-tree "dir/target" that is a dangling symlink, flipping "dir"
to a symlink pointing outside leaked the outside file's content in 3 of
83 raced pulls. With the parent pinned it is 0 in 104 -- but a race only
samples the window, and zero in 104 still leaves a few per cent of
per-attempt risk unmeasured, so rrsync-sender-parent-pin closes it
deterministically instead: a stub standing in for rsync inherits the
pinned descriptor and blocks, the parent is swapped for a symlink out of
the tree while it is blocked, and only then does the stub resolve the
argument. It reports the in-tree leaf with the pin and the attacker's
file without it, so it fails outright if the pin is removed rather than
depending on winning anything. A control first proves the swap really
does redirect the bare name, or the assertions would prove nothing.
Pinning the
parent costs nothing here -- pin_dir() opens it O_PATH, so the special
file itself is still never opened and a FIFO still cannot block, and
whatever the leaf becomes afterwards is reached only from beneath the
held one.
Which directory that is, sender_pinned_arg() already decides, and for
every shape except one it is the immediate parent. The exception is a
--relative argument with no client "/./": there the whole argument is
the transmitted name, so only the anchor it starts from can be pinned
and the components below it stay raceable. That limit predates this
commit and NEWS states it; "the parent is pinned" is not true of that
one shape.
Two boundaries this must NOT cross, each found the hard way:
* a trailing "/" or "/." argument keeps its leaf pin: rsync opens that
one and does follow a symlink there. Declining it made
rrsync-sender-leaf-flip leak the outside directory's content.
* the decision is not gated on HAVE_PROC_SELF_FD. It is about what
rsync does with the argument, not about whether we can pin it, so
gating it left the dangling-symlink failure in place on the BSDs,
macOS, Solaris and Cygwin.
The shape matrix grows fifo, dangling-symlink and symlink-to-file cases,
and now asserts what each delivered entry IS -- kind, symlink target and
content -- on every case rather than spot-checking a couple at the end.
A name-only comparison is satisfied by an empty directory called "f1",
or by the correctly-named but empty symlinks that handing the sender a
magic link produced. It still passes against a pristine 3.4.4 rrsync.
The FIFO case asserts only that the pull does not hang. What a special
file does on the wire is decided by the --no-D that a restricted dir
forces on the remote side alone: the sender then omits the old-protocol
rdev fields that the client's own -D receiver still reads, so protocol
29 and 30 fail regardless of this change. Verified by running the FIFO
case under fakeroot at protocol 29 with and without the parent pin --
it hangs identically either way, so the pinned name is not the cause.
That asymmetry is a pre-existing rrsync bug and is tracked separately.
(cherry picked from commit 1c0bd88f0b)
The HAVE_PROC_SELF_FD probe checked that readlink of a DIRECTORY's entry
returned the right path, which is not evidence of an inode pin, and two
platforms fail that assumption in opposite directions:
* NetBSD makes the entry a symlink for directories only -- readlink of
a regular file's entry fails with EINVAL -- so the probe passed and
then every pull of a file died in the post-pin check with
"post-pin readlink failed (race?): f1 Invalid argument". This is
not new: the same failure reproduces on the branch base.
* Cygwin's readlink returns the right path, but opening the magic link
RE-RESOLVES it. Renaming a directory out from under a held fd lets
the magic link reach the replacement, so the pin protected nothing
while appearing to. rrsync-sender-leaf-flip caught this as a real
outside-content leak, not as flakiness.
Only Linux provides the inode-bound magic link this depends on, so
require that explicitly and keep the runtime probes as a guard for
Linux-like environments where /proc is absent or restricted. Elsewhere
rrsync falls through to the unpinned path, as it already did on the BSDs
and macOS. proc_self_fd_pins() mirrors the rule so the race tests skip
rather than report the intended gap, and Cygwin's workflow expects both
of them to skip -- rrsync-symlink only ran there because the old probe
wrongly reported support.
(cherry picked from commit ce0f6d5a25)
88cee089 rewrote every validated argument to /proc/self/fd/N so the
spawned rsync re-resolves it to the pinned inode. That is right for a
receiver, which open()s its destination, but a sender never opens its
source argument: send_file_list() lstat()s it first, and lstat of a
procfs magic link is always S_IFLNK. So the sender described the
argument as a symlink and sent no data -- silent data loss on
"rsync -a user@host:file dest/", the most ordinary command there is.
Of the argument shapes now covered, only a trailing-slash directory
survived.
Leonid Bugaev reported the regression, diagnosed the lstat-vs-magic-link
mechanism, and supplied the first regression test.
Which pin is usable depends on what rsync does with the argument:
* a trailing "/" or "/." directory is opened, not lstat()ed, and rsync
does follow a symlink there, so it keeps the leaf pin -- verified:
without it a flipped leaf transfers the outside directory's content;
* anything else pins one level up and passes the leaf by name. rsync
will not follow a symlink at that position (it sends the symlink
itself), -L/-k/--copy-unsafe-links are already disabled for a
restricted dir, and rsync's own leaf open is O_NOFOLLOW.
The directory pin resolves normally, including a symlink at its last
component, which is legitimate and which 3.4.4 accepts; the readlink
check afterwards is what proves the held inode is in-tree. It uses
O_PATH because reaching a known name beneath a directory needs only
search permission, and a mode 0111 parent is an ordinary way to publish
a file without letting it be listed.
Under --relative the transmitted name is the whole argument rather than
its basename, so the pin moves up to where that name starts and the rest
is spelled after a /./ marker. The client's own first marker wins if it
supplied one, including when nothing follows it; -R is parsed out of the
short-option cluster rather than sniffed for the letter, so the trailing
capability blob (-e.iLsfxC) cannot be mistaken for it.
Directory pins are keyed by (st_dev, st_ino), so a glob whose matches
share a parent inherits one descriptor rather than one per argument.
Every shape now delivers what a pristine 3.4.4 delivers, which is what
rrsync-pull-arg-shapes asserts -- it passes against 3.4.4 itself, so the
expectations are that behaviour and not this implementation's. The
"-R --no-implied-dirs" case is gated on protocol >= 30: at protocol 29
the receiver rejects it with "invalid path from sender" and transfers
nothing, which 3.4.4 does identically.
Moving the sender's pin off the leaf invalidates rrsync-symlink's oracle,
so it is reworked here rather than left failing. It patches rsync to a
stub that open()s its last argument, which is a faithful model for an
intermediate path component -- whatever rsync does with the final name,
it must not reach it through a flipped parent -- but not for the leaf: a
sender lstat()s its source and transmits a symlink there rather than
reading through it. So it now flips an intermediate directory, and the
leaf is covered against the real binary by rrsync-sender-leaf-flip, which
races both a plain file argument and a trailing-slash directory and
asserts no outside CONTENT is delivered rather than requiring a symptom
from a race that may not be won on a given run. Its trailing-slash half
is RED against a pristine 3.4.4 rrsync, which delivers outside/dir/loot.
rrsync-symlink is now sender-only. Measured over a 5s race, the stub
reached the outside marker 28 times in 98 runs as a sender and 25 in 97
as a receiver against 3.4.4; with the pin it is 0 as a sender but still
7-9 as a receiver, on the branch base as well as here. That residual is
the receiver's not-yet-existing-destination fallback, which predates this
work and is tracked in #139 rather than folded in.
Clearing FD_CLOEXEC goes through F_SETFD rather than os.set_inheritable(),
which prefers ioctl(FIONCLEX) and gets EBADF from an O_PATH descriptor on
older kernels -- every sender pull on Ubuntu 18.04 aborted with "Bad file
descriptor" the moment a directory pin was taken.
Co-authored-by: Leonid Bugaev <leonsbox@gmail.com>
(cherry picked from commit 6edb7dea2a)
A pull with a local --files-from does not send the list file to the
server: it sends the literal "--files-from=-" and streams the names down
the protocol connection. 88cee089 started inode-pinning every checked
option value, so rrsync tried to realpath() and open a file named "-" in
the restricted dir and killed the connection:
post-realpath open failed (race detected): - No such file or directory
Every --files-from pull through a restricted account was broken; 3.4.4
delivers the files. Exempt the exact string "-" only, so a list file
that really is a pathname is still validated and pinned -- which the new
test's control case checks, using the command shape rrsync actually
accepts so that it reaches the pathname check rather than dying earlier
at the syntax check.
(cherry picked from commit 8678d89b2c)
The earlier copy-unsafe-links denial left the rrsync wrapper short of the
3.5.0 restricted-dir hardening, so a daemon-side rrsync still followed a
symlinked --log-file, created device/special files, and had a
realpath-vs-exec TOCTOU. Replace support/rrsync with the fully-hardened
3.5.0 wrapper (version-agnostic; verified functional with this rsync):
- safe_open_logfile(): open the log file O_NOFOLLOW + S_ISREG + inode
re-check so a planted symlink/special is refused;
- force --no-D in a restricted (non-"/") dir so `rsync -a` strips device/
special semantics instead of creating them;
- inode-pin each realpath-validated arg via O_PATH + /proc/self/fd/N
(where available) so the exec'd rsync can't be race-flipped after the
check; fall through unpinned where /proc/self/fd is unavailable.
Tests: rrsync-logfile-symlink, rrsync-specials-denied, rrsync-symlink,
rrsync-copy-unsafe-links-denied, rrsync-archive-mode.
Deny --copy-unsafe-links inside a restricted (non-"/") rrsync directory (it
dereferences a symlink pointing outside the tree and copies the referent
out -- exfiltration), and don't abort the run when flock() is unavailable.
Test: rrsync-copy-unsafe-links-denied.
rsbackup (https://github.com/ewxrjk/rsbackup) uses "ssh <host> true" to
check that the host in question is reachable. I like to configure my
backed-up hosts to force the backup system to go via `rrsync`, but I
always have to add a local tweak to allow `SSH_ORIGINAL_COMMAND=true` to
work. I think this would be safe enough to include in rrsync.
Solve the following problems:
* mishandling of commit message lines similar to committer lines
* UnicodeDecodeError with commit messages that cannot be interpreted
as utf-8
- Add link targets for all option choices, not just the first one.
- Tweak cross-link arg format.
- Add more links, including some in the latest NEWS.
- Split out a few numbered lists.
The new default is to protect args and options from unintended shell
interpretation using backslash escapes. See the new `--old-args` option
for a way to get the old-style splitting. This idiom was chosen over
making `--protect-args` enabled by default because it is more backward
compatible (e.g. it works with rrsync). Fixes#272.
- Convert rrsync to python.
- Enhance security of arg & option checking.
- Reject `-L` (`--copy-links`) by default.
- Add `-munge` and `-no-del` options.
- Tweak the logfile line format.
- Created an rrsync man page.
- Use `configure --with-rrsync` if you want `make install` to install
rrsync and its man page.
- Give lsh more rrsync testing support.
- rsync-no-vanished now avoids joining stdout & stderr, avoids affecting
a non-client run, and gets the rsync status code correctly.
- rsync-slash-strip now avoids affecting a non-client run.
- use `grep -E` and `grep -F` (`egrep` and `fgrep` are non-standard)
- use same hashbang style for all test scripts
- use explicit comparisons in test scripts
- remove redundant ; from test scripts
- make test script not executable, just like all the other scripts
- unify codestyle across all test scripts
- make openssl license exception clearer by having it at the top
- use modern links in COPYING. The text now matches:
https://www.gnu.org/licenses/gpl-3.0.txt
- fix typo