Commit Graph
649 Commits
Author SHA1 Message Date
rajputa-deshawandZen Dodd 7998ccfe65 receiver: add --delay-symlinks (#1124)
* Add --delay-symlinks to create symlinks after all files are in place

The generator creates a symlink as soon as it reaches it in the file
list, while regular files arrive later through the sender and receiver
(and, with --delay-updates, are only renamed into place at the end).
For the rest of the transfer a new symlink can point at a file that
isn't there yet, so anything reading the destination during the sync --
e.g. a loader resolving libfoo.so -> libfoo.so.2 -- can fail.

The new --delay-symlinks option makes the generator defer new and
changed symlinks and create them once the receiver reports that every
file, including any --delay-updates renames, is in place.  Each link is
itemized and logged after it has been created, before the generator
ends the delay-updates phase, and the deletion stats are sent after any
directory that a deferred link replaces.

The option implies --no-inc-recursive on the receiving side and is
passed to a remote receiver, which must also support it.  It is an
error to combine it with --remove-source-files, --hard-links,
--compare-dest, --copy-dest, or --link-dest, or to use it with a
protocol older than 29.  support/rrsync accepts the option and refuses
it under -no-overwrite, as it does --delay-updates.

Add delay-symlinks_test.py, which holds a push partway through the
link's target to check that new and changed links aren't created early,
and rrsync-no-overwrite-delay-symlinks_test.py.

* Document --delay-symlinks limits and test how it is passed to the server

The man page now says the option narrows the inconsistency window rather
than making the update atomic, that an interrupted transfer leaves new
symlinks missing and changed ones at their old targets, and that
--delete-before can also remove an old target early.

Add delay-symlinks-remote_test.py.  Through a remote shell that records
the server command, it checks that the option is passed to a remote
receiver on a push but not to a remote sender on a pull, and that
incremental recursion is off in both directions.  It also checks that
--protocol=28 fails with the protocol error, and that a push to an older
rsync (old_versions/rsync_3.4.1) is refused before anything is
transferred while a pull from it works.

* testsuite: seed delay-symlinks fixture with rsync

* NEWS: document --delay-symlinks

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-11 14:02:12 +10:00
Omar ElsayedandZen Dodd 8b8de5232e options.c: Fix --files-from confinement for local and SSH transfers (#1094)
* Fix --files-from confinement for local and SSH transfers

The recent path confinement patch caused an ELOOP error when local or SSH users tried to use a --files-from list located outside the confined root.

This happened because the code treated the argument as if its always an operator-path peer. CLI arguments provided locally or over SSH are trusted, so the strict boundary check should only apply to untrusted clients connecting to a background daemon.

* tests: assert local files-from copy

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-19 14:56:36 +10:00
Samuel Henrique c529eccff7 options: accept --max-alloc=0 again, resolved to the parser's own ceiling (#1069)
* options: accept --max-alloc=0 again, resolved to the parser's own ceiling

3.5.0 rejected --max-alloc=0 (CVE-2026-53794).  The stated rationale was that a
zero cap "disabled the per-allocation size cap (the defense behind
CVE-2024-12084)".  That was true up to 3.2.7, where the check short-circuited:

	if (max_alloc && num >= max_alloc/size)

but 2f9b963a ("Make `--max-alloc=0` safer", 3.3.0) removed the short-circuit and
mapped 0 to SIZE_MAX at parse time, leaving the guard unconditional.  Since the
guard admits an allocation only when num < max_alloc/size, num*size stays below
max_alloc at every setting, so the num*size overflow check was armed for 0 just
as for any other value.  From 3.3.0 onward, 0 raised the magnitude ceiling and
nothing else -- and an explicit 8191P raises it exactly as far, is accepted, and
is forwarded to the peer, so rejecting 0 removed no capability.

What it did remove is the only portable spelling.  The parser's ceiling is
SIZE_MAX/2, so it tracks the build's word size: on ILP32 the suffix multiplier
alone exceeds the bound, making every P and T value an error whatever the digits
and capping the option at 2047M.  Because max_alloc_arg goes on the wire
un-normalized, a 0 was re-resolved by each side against its own SIZE_MAX; any
literal is resolved once on the client and shipped verbatim, so nothing above
2047M survives a 64-bit client talking to a 32-bit daemon.  There is no number a
user can compute that does what 0 did, which is what #1056 ran into.

So accept 0 again, but resolve it to SIZE_ARG_MAX (SIZE_MAX/2) rather than
SIZE_MAX, so it lands exactly on the largest value that could also be typed and
is no longer a limit only the 0 spelling can reach.  Keep forwarding it
verbatim: that per-side resolution is the property worth having.  This leaves
the substantive half of the 3.5.0 hardening -- bounding parse_size_arg() against
the unbounded `size *= atof(size_arg)` -- untouched.

The residual concern, a 0 forwarded to a <= 3.2.7 daemon that honours it, is not
something a client-side check can address: the client is the attacker's own
code, as daemon-max-alloc-zero_test.py noted in its own docstring.  Operators
who want peers kept off their cap have `refuse options = max-alloc`.

Also drops the now-unreachable rejection message, which left the min-value error
recommending a value the parser refused ("min: 1.00M or 0 for unlimited").

Tests: max-alloc-zero replaces max-alloc-zero-rejected and
daemon-max-alloc-zero, whose assertions are the behaviour being reverted.  It
checks that 0 is accepted, that it reaches the peer as the literal "0" rather
than a resolved number (verified against a negative control that normalizes it),
and that the parser's upper bound still rejects an out-of-range value.

Fixes #1056

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* testsuite: refresh Cygwin expectations
* docs: note max-alloc safety
2026-09-03 07:42:34 +10:00
Andrew Tridgell 3113011218 rsync: add --confine-root, bounding operator path resolution
The ownership walk that resolves operator-supplied paths asks who planted a
symlink, not where the path came out, so a symlink owned by uid 0 or the euid
is followed wherever it points.  A daemon already narrows that with the served
module root; nothing else has a root to narrow it with.

That leaves a wrapper serving a restricted directory over a remote shell with
no way to bound the resolution.  rrsync can vet the argv it is handed, but
filter rules travel over the protocol instead: a dir-merge rule can name a
merge file outside the restricted dir and the server reads it in as rules.
Redacting the resulting diagnostics does not close it, because an exclude-only
merge produces none -- every line becomes a pattern, so nothing fails to parse,
and the client reads the file's contents off which of its own names went
missing from the file list.

--confine-root gives that wrapper the root the daemon has.  The existing module
check becomes a root check that takes its root from module_dir when we are a
daemon and from the option otherwise, so daemon behaviour is unchanged; a
daemon ignores the option outright, since it arrives in a peer-supplied argv
and could only widen the module.

The tracker is seeded from getcwd() rather than curr_dir, which is only the
lexical name change_dir() was given: descend into a source argument through a
trusted symlink and the two sit at different depths, so a ".." that really
escapes looks like it landed inside.  When the cwd cannot be read there is
nothing to measure against and the open is refused -- an empty tracker does not
deny by itself, because a leading ".." pops nothing from it and an empty path
reads as an ancestor of the root.

An fd pin (/proc/self/fd/N, which rrsync uses so no later symlink can redirect
a validated option path) is spelled outside the root by construction, so the
walk transits the pin namespace and the pin is judged by what it points at.
Only a bare ".../fd/<digits>" is resolved that way, and one that will not
readlink to an absolute path is refused; rrsync's ".../fd/N/<leaf>" spelling
resolves through the magic link and has its remaining components checked
normally.

--insecure-links is refused alongside it: that opt-out returns the legacy open
before the walk that enforces the root runs, so the pair would have quietly
meant no confinement at all.
2026-08-03 05:35:03 +10:00
Filipe Casal 06212f112a daemon: cap peer-selected Zstandard workers
A daemon parses the peer-supplied server argv, so a client naming a large
--compress-threads on a pull makes the daemon-side sender materialize
that many Zstandard workers: 256 was measured as 257 threads in one
connection.  No custom client is needed -- a stock rsync forwards it with
-M--compress-threads=N -- and on an anonymous module no authentication
happens first.  Clamp it to 8 on a daemon; local and remote-shell
invocations keep the operator-requested value.

A push parses and clamps it too, but creates no workers there: the
option affects compression, not decompression.

The test asserts the implementation's own bound, 8 workers plus the main
thread, rather than a looser threshold that a build unable to create
workers at all would also satisfy -- so it first requires a worker pool
to be reachable and skips if it is not, then requires it to be bounded.
It needs --use-tcp and is declared in the workflows that enforce a skip
set.

Whether the platform can be counted at all is asked once, before the
answer is folded into a max(): thread_count() returns -1 where it has no
way to look, and max(0, -1) is 0, so the -1 could never reach the check
meant to catch it and an uncountable host looked instead like a sender
that died.  A fleet run had Cygwin failing for exactly that reason.

Left deliberately open: the cap is silent, is not expressible in
rsyncd.conf, and does not bound the total across connections, since
max connections defaults to unlimited.
2026-07-30 16:45:26 +10:00
Filipe Casal c529163ef0 options: refuse aliases for exact option rules
parse_one_refuse_match() marked only the first long_options row whose
long name matched the configured spelling, then broke out for a
non-wildcard rule.  --compress-threads and --zt are separate popt rows
that both write &do_compression_threads, so "refuse options =
compress-threads" disabled the canonical row and left the alias
accepted: the refused capability was still reachable under its other
name.  The same shape covers zc/compress-choice and zl/compress-level.

An exact rule names a capability, not one spelling of it, so mark every
row that does the same thing.  Comparing the raw table fields is not
enough for that: popt's `val` means different things per argInfo.  For
POPT_ARG_VAL it IS the value stored in `arg`, while elsewhere a nonzero
`val` is an action code for the parser's switch, and POPT_ARG_NONE with
a destination stores 1 whatever `val` says.  --del is
POPT_ARG_NONE/&delete_during/0 and --delete-during is
POPT_ARG_VAL/&delete_during/1: the same destination and the same
resulting value, but unequal as table entries, so "refuse options =
delete-during" was still evaded by --del and the mirror held too.

Compare what a row does instead -- the destination and the constant it
assigns, falling back to table-entry equality for rows that store a
runtime value or only dispatch an action.  Enumerating all 258 rows,
this couples exactly one pair the field comparison missed, del and
delete-during, and changes nothing else.  Opposite switches such as
--foo and --no-foo stay distinct because they assign different values.

Two regressions.  The compress-threads one drives the raw daemon
protocol -- not to preserve the spelling, which -M--zt=N would do just as
well, but because it goes on to observe the worker pool the bypass
delivers.  Its oracle is the refusal itself -- the alias connection torn
down and
"configured to refuse --zt" logged -- and deliberately not the resulting
worker count: an accepted --zt is a defeated refuse rule however few
threads it produces, and the daemon worker cap being added alongside
this holds that count to 9, so a count-based assertion passes while the
alias is still accepted.  Run that test against the cap without this
parser fix and it does exactly that; the two changes were covering for
each other.

The delete one needs neither zstd nor a socket: --remote-option puts the
option in the daemon's argv verbatim, which is the reach an ordinary
user already has, so it drives a stock client both ways round against
modules refusing each spelling, with an unrefused module as the control.

The compress-threads test needs --use-tcp, so it skips in every other
column and is declared in the workflows that enforce a skip set, which a
fleet run otherwise reports as an unexpected skip on fourteen cells.
2026-07-30 06:36:12 +10:00
Andrew Tridgell b607369f5f rsync: add --drop-D, refusing device/special creation only
-D and --no-D do two jobs at once: they decide whether devices and
special files are created, and they decide whether those entries carry
their rdev fields on the wire.  send_file_entry() and recv_file_entry()
frame those fields with the same condition, but each end evaluates its
own preserve_devices/preserve_specials, so the two only agree because
both normally parse the same command line.

That makes --no-D unusable for a wrapper that controls one end of a
connection and wants to deny creation.  Give it to the receiver alone
and the client's -D sender writes rdev the receiver never reads: the
file list desynchronises from that entry on.  A FIFO or socket breaks
below protocol 31 -- a hang at 29, "File-list index 0 not in 0 - -1" at
30 -- and a device node breaks at EVERY protocol, current ones included,
because its arm of the condition has no protocol clause at all.

--drop-D separates the two jobs: it refuses the creation and leaves the
encoding alone.  The entry is skipped through the existing non-regular
fall-through, so the visible result matches --no-D, and because it
touches no wire state it can be applied to one end by itself.

It has no effect on a sending rsync, which creates nothing.
2026-07-29 19:58:40 +10:00
Andrew Tridgell 3fe1ed512c rsync: confine the daemon files-from open to the module root
A daemon serving a writable, non-chrooted module reads a client-requested
--files-from=:LIST through open_no_attacker_symlinks(), which follows a
symlink owned by uid 0 or the euid.  The module-root confinement in that
resolver (abspath_excluded_by_module) only fires when operator_path_resolve
is set, and this open left it clear -- so a trusted-owned symlink whose
target escapes the module was followed.

An attacker can obtain such a symlink without owning it: a --backup-dir push
makes the daemon back up the old destination symlink with the daemon's own
(root) ownership, and a parent-swap race can leave that root-owned backup
symlink pointing outside the module.  A later --files-from=:backup/... then
reads out-of-module file content as the file list, bypassing the same-uid
ownership constraint that normally protects files-from.

Set operator_path_resolve around the files-from open so the ownership walk
also refuses a trusted-owned symlink that redirects the list outside the
module root.  A daemon has no rsyncd.conf "files from" of its own, so this
path is always client-requested and confining it is unconditional.  No-op off
a daemon (the module-root check only fires when am_daemon).

The same ownership-walk opener backs the daemon merge/--exclude-from reads in
exclude.c, but those also load the module's own "include from"/"exclude from"
admin files, which on a non-chrooted module may legitimately live outside the
module; confining them there needs a client-vs-admin distinction and is left
to a separate change.
2026-07-23 08:28:36 +10:00
Andrew Tridgell 22efd249ad options: fix safe_arg() uninitialized-byte leak in filename-mode quoting
The escape counter reserved a slot for every backslash, but the writer
suppresses the escaping backslash before a wildcard and -- via the
strchr(WILD_CHARS, '\0') footgun -- on a trailing backslash, so counter and
writer disagreed and left an uninitialized heap byte in the returned string that
is handed to the legacy remote shell (protect_args=0).  Make the counter mirror
the writer and guard the strchr with f[1] (which also correctly doubles a
trailing backslash).

Reported-by: Leonid Bugaev
2026-07-20 14:05:32 +10:00
Andrew TridgellandGreg Kroah-Hartman 81850e8b27 options/token: bound the -v repetition, output level, and suffix token
Cap -v repetition so the argstr[64] global can't overflow, clamp a
negative --info/--debug level out of counts[], and cap a --skip-compress
suffix token at 32 bytes.

Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
2026-07-20 14:05:31 +10:00
Andrew Tridgell dd3e6bfece daemon: treat exclude=/filter as a name filter, not a symlink boundary
The daemon exclude/filter chain is a name-based visibility/tamper filter, as in
stock rsync (verified against 3.2.7): a symlink whose own name is not excluded is
followed to an excluded target, and the documented symlink defense is `munge
symlinks`, not the filter.  Collapse ".." (via sanitize_path) before the daemon
dest / temp-dir / backup-dir / partial-dir / basis filter checks so a "../excluded"
path is matched by name like 3.2.7 (clean_fname's CFN_COLLAPSE_DOT_DOT_DIRS does
not collapse "a/b/c/../../../secret"), and keep a leading "/" for a "path = /"
module so an absolute filter rule still matches.  The module-ROOT confinement of
operator paths is unchanged (previous commit); only the in-module name match is
restored to its 3.2.7 behaviour.
2026-07-20 14:05:30 +10:00
Andrew Tridgell defd7110ac confine operator- and peer-supplied paths to the served module
Route every operator-supplied directory path (--temp-dir / --partial-dir /
--backup-dir / alt-dest basis) and the transfer engine's own dest/source opens
through the secure resolver + ownership walk, so a symlink owned by another uid
can no longer redirect a read, a backup, a staging open, a rename, an unlink or a
new-destination create outside the module.  Covers backup.c, generator.c (alt/
link/in-place basis), receiver.c (basis open), sender.c (remove-source / source
open / copy-links leaf), rsync.c (held-fd attr stat), clientserver.c (pid-file
parent pin), and main.c (relative-basis make-absolute, mkpath dest-arg guard).
--insecure-links / "insecure links = yes" is the local opt-out (a daemon never
honors a peer-forwarded one).
2026-07-20 14:05:30 +10:00
Andrew Tridgell 8118744f2a scan-build: drop dead assignments
Remove stores that are never read before being overwritten or going
out of scope. No behavior change except batch.c write_opt, which now
accumulates the leading-space write error into the return value
(consistent with the arg branch) instead of discarding it.

simd-checksum-x86_64.cpp, options.c, util1.c, batch.c
2026-06-16 08:55:39 +10:00
Andrew TridgellandClaude Opus 4.7 f1c24ab03b syscall+clientserver: am_chrooted and use_secure_symlinks for daemon-no-chroot (CVE-2026-29518)
CVE-2026-29518: an rsync daemon configured with "use chroot = no"
is exposed to a TOCTOU race on parent path components. A local
attacker with write access to a module can replace a parent
directory component with a symlink between the receiver's check
and its open(), redirecting reads (basis-file disclosure) and
writes (file overwrite) outside the module. Under elevated daemon
privilege this allows privilege escalation. Default
"use chroot = yes" is not exposed.

Add secure_relative_open() in syscall.c. It walks the parent
components under RESOLVE_BENEATH (Linux 5.6+) /
O_RESOLVE_BENEATH (FreeBSD 13+, macOS 15+) / per-component
O_NOFOLLOW elsewhere, anchored at a trusted dirfd, so a parent-
symlink swap is rejected by the kernel. Route the receiver's
basis-file open in receiver.c through it when use_secure_symlinks
is set in clientserver.c rsync_module().

Reporters: Nullx3D (Batuhan SANCAK); Damien Neil; Michael Stapelberg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Michal Ruprich 8e11f0c169 Using a correct time in log file 2026-04-22 13:02:10 +10:00
Ronnie Sahlberg c2db921890 options.c: Fix segv if poptGetContext returns NULL
If poptGetContext returns NULL, perhaps due to OOM,
a NULL pointer is passed into poptReadDefaultConfig()
which in turns SEGVs when trying to dereference it.

This was found using https://github.com/sahlberg/malloc-fail-tester.git
$ ./test_malloc_failure.sh rsync -Pav crash crosh

Signed-off-by: Ronnie Sahlberg <ronniesahlberg@gmail.com>
2025-08-23 17:49:03 +10:00
Max Kellermann b6457bbc83 make lots of global variables const
This way, they can live in `.rodata` and the compiler is allowed to do
certain optimizations.
2025-08-23 17:31:40 +10:00
Ethan Halsall b78a841bb0 feat: validate compress threads option 2025-08-23 17:13:49 +10:00
Ethan Halsall f7a2b8a3fa feat: add threads to zstd compression 2025-08-23 17:13:49 +10:00
Wayne Davison bcf0738f98 Indentation tweak. 2024-11-05 11:20:17 -08:00
Wayne Davison 2f9b963aba Make --max-alloc=0 safer.
Always do size checking in my_alloc(), even for `--max-alloc=0`.
2023-06-27 09:01:25 -07:00
Wayne Davison 9a06b2edb0 Preparing for release of 3.3.0pre1 [buildall] 2023-04-29 09:01:43 -07:00
Wayne Davison 0698ea9aeb Fix flist string comparison issue in tr_TR.utf-8 locale. 2023-02-05 19:46:45 -08:00
Wayne Davison 5c93dedf45 Add backtick to SHELL_CHARS. 2023-01-04 21:52:48 -08:00
Wayne Davison 8990ad96de Duplicate argv data before poptFreeContext(). 2022-11-22 22:21:15 -08:00
Wayne Davison 5374994089 Avoid quoting of tilde when it's a destination arg. 2022-11-05 09:22:10 -07:00
Wayne Davison 0efa63f2e6 Use JSON output if --version (-V) is repeated (client side only). 2022-09-10 13:14:42 -07:00
Wayne Davison 0a09df2c5e Rename --protect-args to --secluded-args. 2022-08-23 14:56:23 -07:00
Wayne Davison db4f919ebe Allow ~/remote/./path with -R if the path has /./ in it. 2022-08-15 18:55:05 -07:00
Wayne Davison cff8f04477 Add --trust-sender option. 2022-08-09 11:45:56 -07:00
Wayne Davison db8034f12e Escape leading tilde char when "~" or with -R. 2022-08-09 11:42:32 -07:00
Wayne Davison 38e1b075b4 Fix some issues with backslashed wildcards in args. 2022-08-08 19:26:05 -07:00
Wayne Davison 7e5424b806 More improvements to file-list checking
- Avoid implied rules on generator and (with extra certainty) on server
- Add -R implied-directory path elements as directory includes
- Log about extra file-list checking using a new --debug=FILTER3 level
2022-08-01 07:00:51 -07:00
Wayne Davison 8aa465117f Add new & improved --copy-devices option. 2022-03-27 14:04:59 -07:00
Wayne Davison d9eaffe564 Complain about --old-args with --protect-args. 2022-01-17 18:09:36 -08:00
Wayne Davison 635d8c0632 A repeated --old-args does more escape disabling. 2022-01-09 18:20:23 -08:00
Wayne Davison 6b8db0f644 Add an arg-protection idiom using backslash-escapes
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.
2022-01-09 17:47:24 -08:00
Wayne Davison f1a6998df2 Only send the --no-W kluge to a receiver. 2022-01-02 23:51:35 -08:00
Wayne Davison 3e44bbd313 Preparing for release of 3.2.4pre1 2022-01-02 15:13:19 -08:00
Wayne Davison 4adfdaaf12 Tweak stderr handling for older BackupPC versions
This makes the default for a protocol-28 server process be --stderr=client
instead of --stderr=errors.  See rsync's github issue #95.
2022-01-02 14:48:04 -08:00
Wayne Davison e4669b81ae Add the --info=NONREG setting. 2021-11-03 09:35:50 -07:00
Wayne Davison 7956070f2b Make --chown|--usermap|--groupmap imply -o|-g (as appropriate). 2021-10-19 21:10:12 -07:00
Wayne Davison b774dbc1c0 Improve --omit-dir-times & --omit-link-times
The code now better handles skipping time setting on dirs and/or links
when --atimes and/or --crtimes is specified without --times.
2021-10-10 13:39:09 -07:00
Wayne Davison 82f023d7e3 Add --fsync option (promoted from patches). 2021-09-27 10:30:00 -07:00
Wayne Davison ec57c57baf Help avoid a --sparse --inplace bug in older rsyncs. 2021-09-27 10:16:15 -07:00
Wayne Davison d2a97a7ab4 Various file comparison improvements
- Rename unchanged_file() to quick_check_ok().
- Enhance quick_check_ok() to work with non-regular files.
- Add a get_file_type() function to the generator.
- Use the new functions in the generator code to make the logic simpler.
- Fix a bug where the `--alt-dest` functions were not checking if a
  special file fully matched the non-permission mode bits before
  deciding if we have found an alt-dest match.
- Enhance the `--info=skip --ignore-existing` output to include extra
  info on if the existing file differs in type or passes the standard
  quick-check logic.
- Add `--info=skip2` that authorizes rsync to perform a slow checksum
  "quick check" when ignoring existing files. This provides the uptodate
  and differs info even if we need to checksum a file to get it.
2020-09-22 12:48:02 -07:00
Wayne Davison 25526eb3fe Simplify the compat logic for local_server
Change the logic in compat.c to construct the client_info string value
for a local copy so that the various checks of the string don't need to
make an exception for local_server.
2020-08-27 19:23:13 -07:00
Wayne Davison 6c89f00d1b Move SUPPORT_ATIMES to rsync.h. 2020-08-03 13:27:00 -07:00
Wayne Davison dee0993286 Create usage.c for smaller awk-dep rebuilds. 2020-08-03 12:23:18 -07:00
Wayne Davison 47351c2b16 Use RSYNC_GITVER in more output 2020-08-03 10:46:31 -07:00