Commit Graph
8016 Commits
Author SHA1 Message Date
Angel JandZen Dodd fd96bc7e8a testsuite: fix timestamp race in exclude test (#1120)
* testsuite: fix timestamp race in exclude test

The same-newness files must have equal modification times, but
separate touch calls can straddle a second boundary and make the
destination newer. This causes --update to skip it unexpectedly.

Copy the source timestamps to the destination explicitly.

* testsuite: only copy update-test mtime

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-26 21:25:30 +10:00
Zen Dodd 26da01aa58 build: support systems without O_CLOEXEC or O_DIRECTORY (#1101) 2026-10-26 21:16:36 +10:00
Tomáš ChvátalandZen Dodd 7472f8dad0 access: match hosts allow/deny hostname tokens against IPv6 peers (#1140)
* access: match hosts allow/deny hostname tokens against IPv6 peers

match_hostname() resolved hostname tokens with gethostbyname(), which
only returns IPv4 addresses, so a hostname token never matched an IPv6
peer: allow entries locked such clients out and deny entries silently
failed open. Resolve with getaddrinfo(PF_UNSPEC) instead, mirroring
match_address() in the same file.

Fixes #563.

* access: compare forward-DNS hostname matches in binary form

Address review feedback on the getaddrinfo conversion: parse the peer
address once and compare each resolved address against it byte-wise
within the same family, as match_address() does, instead of comparing
inet_ntop() strings; keep the IPv6 cases under #ifdef INET6 so a
--disable-ipv6 build compiles the IPv4 path only.

Add daemon-access-hostname regressions: hostname-token allow and deny
over IPv4 and IPv6 (with reverse lookup off, so only the forward path
can match), forward lookup disabled, and unresolvable allow/deny
tokens.

* access: tighten IPv6 hostname matching portability

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-26 20:51:39 +10:00
SAIREX be124601c4 auth: read only the first password-file line (#1137)
* auth: read only the first password-file line

strtok() skips leading delimiters, so an empty first line followed by a
password was interpreted as that password. Stop at the first CR/LF
instead, keeping an empty first line empty and making file and stdin
behavior consistent.

Add daemon-auth regression coverage for LF, CRLF, empty input, an empty
first line, and a final line without a newline for both input sources.

Fixes #1134.

Signed-off-by: Eduard Zateev <hackerowskiy@gmail.com>

* tests: reject password-file fallback for empty first lines

Supply the valid daemon password through RSYNC_PASSWORD in negative
cases, then require exit status 1 and the password read diagnostic.

Signed-off-by: Eduard Zateev <hackerowskiy@gmail.com>

---------

Signed-off-by: Eduard Zateev <hackerowskiy@gmail.com>
2026-10-23 16:34:19 +10:00
SAIREXandZen Dodd 92a2a09e3f rrsync: fix uploads to existing file destinations (#1136)
* rrsync: pin the parent of existing receiver file arguments

Passing an existing destination as the leaf's /proc/self/fd/N makes
rsync lstat the procfs link and try to replace it, failing with EPERM.
Use the held parent fd plus the leaf name, as for a missing destination.
Keep directory arguments pinned directly and verify the parent's leaf
still matches the already validated inode before releasing its fd.

Reuse pin_dir() for the parent checks and execute-only directory support.
Add end-to-end coverage for overwrite, creation, directories, pulls,
rejected paths, an in-place leaf replacement and a parent rename.

Based on the parent-pin approach described by JeanFrancoisGagne in #1107.

Fixes #1107.
Assisted-by: OpenAI Codex
Signed-off-by: Eduard Zateev <hackerowskiy@gmail.com>

* testsuite: expect rrsync file-argument skips

* testsuite: allow slow aligned overflow scans

---------

Signed-off-by: Eduard Zateev <hackerowskiy@gmail.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-22 19:07:42 +10:00
Zen Dodd f62f687fb5 human-readable: scale progress rates by default (#1138) 2026-10-22 16:43:24 +10:00
Omar Elsayed a7404a9a63 match: bound hash_search collision work (#1132)
* Fix hash_search() duplicate-chain regression (#1115) and collision flood

When a hash chain contains > MAX_CHAIN_LEN identical colliding blocks
(such as 0x00 sparse regions colliding with 0xFF blocks at 64K/128K
block sizes), walking every duplicate entry exhausts MAX_CHAIN_LEN before
reaching valid matching blocks further down the chain (#1115).

Track the strong checksum of the last failed chain entry (last_false_sum2)
and unlink subsequent duplicate entries sharing the same strong checksum
(unless protected by SUMFLG_SAME_OFFSET in --inplace mode) so duplicates
are neither re-walked nor counted repeatedly against MAX_CHAIN_LEN.

Additionally, if 4 consecutive 1-byte rolling offsets experience a
weak-checksum collision where the entire chain fails to match, advance
offset by a full block rather than computing a full strong checksum on
every single byte.

* Split hash_search() collision tests and add boundary/--inplace coverage

Split testsuite/collision_test.py into two dedicated regression suites:

1. testsuite/hashsearch-dup-chain_test.py:
  - Verifies forward chains (> MAX_CHAIN_LEN duplicates),
     multi-group interleaved duplicate chains, and --inplace
     SUMFLG_SAME_OFFSET preservation when mutating the chain after a
     subsequent duplicate is overwritten on disk.

2. testsuite/hashsearch-collision-skip_test.py:
- Asserts explicit O(N/B) hash_hits CPU bounds and exact literal/matched
     bandwidth bounds across skip boundaries (3, BLOCK, BLOCK+3, BLOCK+4,
     2*BLOCK+3, 2*BLOCK+6, and 10*BLOCK+137) in both standard and
     --inplace modes, and documents the bunched-perturbation hazard case.

* Unlink redundant SUMFLG_SAME_OFFSET duplicates in hash_search()

In --inplace mode, when > MAX_CHAIN_LEN identical basis blocks match
at their aligned offsets (such as 0x00 sparse runs), every matching
entry acquires SUMFLG_SAME_OFFSET. Skipping deduplication for all
SUMFLG_SAME_OFFSET entries left all pinned duplicates in the chain,
exhausting MAX_CHAIN_LEN, inflating false_alarms, and hiding valid
basis blocks behind them.

Track the flags of the entry that set last_false_sum2 (last_false_flags).
Once a SUMFLG_SAME_OFFSET entry has been retained as last_false_sum2,
unlink subsequent duplicate entries carrying SUMFLG_SAME_OFFSET as well,
while still preventing unpinned entries from unlinking a pinned entry.

Add Case 4 to testsuite/hashsearch-dup-chain_test.py to verify that
> MAX_CHAIN_LEN pinned duplicate blocks are unlinked and trailing valid
blocks behind them are matched with 0 extra literal bytes.
2026-10-20 17:28:03 +10:00
Omar Elsayed 9dc0ff638c Bound aligned_i by s->count in hash_search() (--inplace) (#1133)
When updating_basis_file is active (--inplace), hash_search() advances
aligned_offset by s->blength and increments the 32-bit signed counter
aligned_i until aligned_offset catches up to offset.

If the source file is larger than 2 GiB and the peer supplies a small
s->blength and s->count, continuing to increment aligned_i past s->count
serves no purpose (as no basis blocks exist beyond s->count - 1) and can
overflow INT32_MAX into a negative index that bypasses the one-sided
aligned_i < s->count check.

Stop the catch-up loop once aligned_i reaches s->count so aligned_i
remains strictly within [0, s->count] and cannot overflow.

Reported-by: Mominul Islam Hemal <mominulislam-2021411216@cs.du.ac.bd>
2026-10-19 19:15:10 +10:00
Andrew Tridgell b70a61f394 log: avoid recursive deletion-message overflow (#1130)
* log: avoid recursive deletion-message overflow

A quiet daemon receiver queued one MSG_DELETED per path until the generator hit --max-alloc, so skip names the client did not request. If requested output legitimately fills the queue, keep allocator error reporting from re-entering the same buffer and exhausting the stack. Preserve explicit deletion requests across the daemon max-verbosity clamp.

* NEWS: note daemon deletion-message overflow fix
2026-10-15 10:59:02 +10:00
Alemar ab4b715505 sender: resolve --files-from entries under a module subdirectory (#1127)
* sender: resolve --files-from entries under a module subdirectory

A --files-from pull whose source is a subdirectory of a daemon module
(rsync://host/mod/subdir/) looked up every listed file at the module
root.  A missing file was reported as "file has vanished" and the
transfer ended with code 24; a same-named file at the module root was
sent in its place.  With --remove-source-files, the sent files were
then kept with "Skipping sender remove for changed file".

The sender opens each file from the module root with
secure_relative_open(), rebuilding the path as F_PATHNAME + "/" +
f_name, and secure_sender_parent_fd() rebuilds the parent the same way.
A --files-from entry has no F_PATHNAME: like any relative pathname it
is relative to orig_dir, the transfer's source directory, which here is
a module subdirectory.  Dropping orig_dir made both look under
mod/<entry> instead of mod/subdir/<entry>.

Build the module-relative path in one place, module_relative_path(),
and prefix a missing or relative F_PATHNAME with orig_dir's
module-relative part, as change_pathname() does when it chdirs.
orig_dir is no longer static in flist.c so sender.c can read it.

Add daemon-files-from-subdir_test.py, which fails without the fix.

* sender: keep the --files-from subdirectory fix minimal and test the module filter

* sender, testsuite: shorten the orig_subdir() comment and the test
2026-10-14 18:56:26 +10:00
Peter Korolev 96705c2ae8 cleanup: don't flush the write buffer from a signal handler (#1129)
* cleanup: don't flush the write buffer from a signal handler

When SIGUSR1 aborts an --inplace/--append transfer, _exit_cleanup()
runs inside the handler and flushes the receiver's write buffer.  If
the handler interrupted flush_write_file() after write() returned but
before wf_writeBufCnt was updated, the same bytes are written a second
time, and with --append the file keeps a duplicated 256 KiB region.

Skip that flush when called from a signal handler.  A SIGINT, SIGTERM
or SIGHUP that reaches the receiver itself still exits from normal code
through got_kill_signal, so its flush is unaffected.

* testsuite: abort --append/--inplace in the middle of a write

Hold the receiver right after its first write() to the destination,
with every signal pending, send it SIGUSR1 and check that the
destination is still a prefix of the source.  This fails without the
previous commit.  The hold is an LD_PRELOAD shim, so the test is
Linux-only and is expected to skip on macOS and Cygwin.
2026-10-14 00:46:49 +10:00
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
graysky e6120a6dbc human-readable: group file-list and directory counts (#1125)
* human-readable: group file-list and directory counts

Adds the thousand separators as file-list is computed. Useful on large
dirtrees:

Behavior before: 2523297101 files to consider
After:  2,523,297,101 files to consider (2.523.297.101 with a comma decimal point)

Also covers the file-list counter, "created N directories" and --max-delete.

* NEWS: record human-readable output changes
2026-10-11 07:05:16 +10:00
Zen Dodd 9ca0527510 Revert "NEWS: record human-readable output changes"
This reverts commit 2590887876.
2026-10-11 07:00:40 +10:00
Zen Dodd 2590887876 NEWS: record human-readable output changes 2026-10-11 06:58:36 +10:00
Zen Dodd 4992af01bb docs: correct rsync-ssl verification caveat 2026-10-09 17:12:17 +10:00
Zen Dodd 6eb05b3451 daemon: recognise named Unix stdin sockets (#1121)
* daemon: recognise named Unix stdin sockets
* daemon: distinguish named Unix sockets portably
2026-10-08 16:05:12 +10:00
Christian HesseandZen Dodd 041769d607 human-readable: standardise size and progress-rate formatting (#459)
* human-readable: do not add trailing space in output

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: use dynamic precision length

Let's lower precision for huge numbers.

The output used to be:

  3.45M -> 46.73M -> 523.11M -> 1.24G -> ...

With this change the code always gives the three most significant digits:

  3.45M -> 46.7M -> 523M -> 1.24G -> ...

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: also handle num < mult with the same code

... just make sure no precision is added.

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: add an "i" to unit to indicate binary (1024) base

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: also use it to format rate in progress

Let's also simplify the code for rate in progress, and benefit from
same functionality.

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: append suffix "B" for byte to ofs...

... and also fix the formatting in man page.

Co-authored-by: Zen Dodd <mail@steadytao.com>

* human-readable: unbreak for do_big_dnum() with fract

Co-authored-by: Christian Hesse <mail@eworm.de>

* testsuite: cover human-readable output

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-08 13:25:25 +10:00
Antoine Verheijen 750012c34f Add fchmodat() as an option to set symlink Unix access permissions (#1108)
* Add fchmodat() as an option to set symlink Unix access permissions

syscall.c:

Add fchmodat(), if it exists on the system, as a last-resort attempt to change
the Unix access permissions for a symlink (on those systems that support it).
This is done just prior to finally abandoning such an attempt and after any
other available options for this, such as lchmod() or setattrlist().

This option is required to preserve symlink access permissions when making
copies using rsync on OpenBSD. The patch itself is not OS-specific.

testsuite:

Add a test, symlink-unix-perms_test.py, to testsuite to check on the success
of setting symlink access changes using rsync. The test is skipped on those
systems that do not allow symlink access permission changes. The test checks
at runtime whether or not the system permits symlink access changes. As with
the patch, this test is not OS-specific.

* testsuite: Modify the symlink-unix-perms test to conform to proper standards.

1) Set the proper python shebang
2) Remove the use of random test access values in favour of fixed values.
3) Skip the test only on "not implemented" or "not supported" exceptions. All
   other unexpected errors will fail the test.
4) Test using symlinks to a specifically created directory or file as opposed
   to the '.' directory. The latter could potentially lead to cyclical paths.

Some code tweaking has also been done for readability.

* symlink-unix-perms_test.py: Perform some useful cleanup.

    1) Remove unnecessary import of "random".
    2) Consolidate some unnecessarily verbose and inefficient code.

* testsuite: expect unsupported symlink permission skips
2026-10-06 08:53:27 +10:00
Christian HesseandZen Dodd 40c9a022a5 human-readable: calculate numbers in a loop (#872)
This drops a lot of `else if` blocks and extends units by "E" (Exa),
which is 2^60 and thus the last to fit into int64.

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-10-06 08:04:46 +10:00
Zen Dodd 7b9cedca0f log: escape terminal control characters (#1114)
* log: escape terminal control characters
* log: make filtered output bounds explicit
* rework: honour the active locale when filtering output
* testsuite: contain unsupported locale fixture errors
2026-10-03 18:54:34 +10:00
Zen Dodd 013f76dc8d syscall: preserve Cygwin UNC roots in ownership walks (#1113)
* syscall: preserve Cygwin UNC roots in ownership walks
* syscall: remove duplicate anchor state update
2026-10-03 10:05:25 +10:00
Andrew Tridgell f884cfe689 testsuite: ssh-basic: split the quoted rsh command before exec'ing it
rsh_cmd() returns the lsh.sh path quoted for rsync's own --rsh tokenizer,
so a build directory containing a character shlex.quote() will not pass
bare -- the '~' in a Debian snapshot version such as
rsync-3.5.1~git20260921 -- comes back wrapped in quotes.  The ssh-basic
probe passed that string straight to subprocess.run() as argv[0], where
no shell strips the quotes, and failed with FileNotFoundError.  Split it
with shlex first.

Found via the rsync-latest PPA, where every snapshot build fails
`make check` on exactly this test while the release builds pass.
2026-09-21 16:48:04 +10:00
Zen Dodd f83f5b9c5f website: announce rsync 3.5.1 2026-09-21 14:14:51 +10:00
Zen Dodd 04355d27b7 Preparing for release of 3.5.1 [buildall] v3.5.1 2026-09-21 13:41:01 +10:00
Zen Dodd a584c1365d Update NEWS for 3.5.1 2026-09-21 08:49:03 +10:00
Omar ElsayedandZen Dodd 6cef370422 Add 4 KiB logical block footprint tracking to stats (#1070)
* Add 4 KiB logical block footprint tracking to stats

This adds a 'Number of 4 KiB logical blocks touched' metric to the --stats output to decouple network delta payload from actual local file modifications. Previously, a small amount of literal data scattered across a file (especially with --inplace) could result in a massive number of local file write operations with no visibility, and large sparse files masked their true write opreations (ignoring punch holes).

Technical details:
- Implemented a stateful block tracker in the receiver that calculates touched 4K boundaries using file offsets and lengths, including strict lseek awareness to accurately skip sparse file holes.
- Enforced strict per-file lifetime state with a reset hook inside receive_data(), successfully mitigating POSIX file descriptor (FD) recycling state leaks.
- Created MSG_BLOCK_STATS multiplex message to tunnel the block footprint safely out of the isolated receiver process and relay it over the network.
- Bumped PROTOCOL_VERSION to 33 and SUBPROTOCOL_VERSION to 8392 for safe PR testing.
- Added test suite covering contiguous, scattered, zero-byte, sparse file (hole-skipping), multi-file (FD reuse), batch mode, and maximum-I/O boundary conditions.

* io: fix block stats integration

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-21 08:29:39 +10:00
Andrew Tridgell 26a2984094 fix fleettest for recent changes (#1096)
* testsuite: fix fleettest example json for IDN

match CI for IDN tests

* testsuite: fix fleettest example skip extras and pin Cygwin python

#1069 replaced daemon-max-alloc-zero with max-alloc-zero, which no
longer needs the old static client, but the per-box expect_skip_extra
entries still named the old test, so runtests refused the skip list
("no such test") on every Linux target.  Drop those entries.

Pin python3.9 on the cygwin target to match the workflow, which installs
only python39: the box's default python3 (3.12) has a working AF_UNIX
socketpair, so daemon-stdin-local-socket ran where cygwin.txt expects a
skip.  Note that python39 must be 3.9.25+ for os.getxattr, which the
xattr tests need to run as they do in CI.

* testsuite: run bash without POSIXLY_CORRECT; tolerate Cygwin symlink copystat

runtests.py exports POSIXLY_CORRECT=1 to every test, and bash before 5.1
disables process substitution in POSIX mode, so pseudo-paths,
pseudo-paths-daemon and read-batch-pipe silently skipped on bash 4.4/5.0
hosts (Ubuntu 18.04/20.04, AlmaLinux 8).  Strip the variable from bash's
environment: the tests require bash precisely for that syntax.

That was also why they skipped in the AlmaLinux 8 CI container, so its
skip list (which held only those three) goes away along with the
workflow's reference to it.  The macOS list keeps them: there they skip
as Linux-only.

malicious-sender-delete-scope copies the source tree with symlinks=True.
Cygwin's python 3.9.25 gained os.listxattr, and copystat() on a fresh
symlink then fails with EACCES, although the link itself was created.
Ignore copytree errors that are only about symlink pairs; anything else
still raises.

* testsuite: update max-alloc entry
2026-09-21 08:28:02 +10:00
d9a1cfd067 receiver.c: Tighten alt-dest path resolution and partial-dir state validation (#1077)
* Tighten alt-dest path resolution and partial-dir state validation

This introduces architectural best-practices to harden the receiver's state machine
and harmonize symlink handling across the delta-basis engine, addressing protocol
edge-cases reported by Fyyre (James).

- receiver.c (recv_files): Added explicit validation to ensure one_inplace is
  only triggered when partial_dir is configured and the FNAMECMP_PARTIAL_DIR
  token is legitimate.
- receiver.c (secure_basis_open): Enforced O_NOFOLLOW on leaf components when
  resolving operator-supplied paths, aligning it on operator-path behavior.

Co-authored-by: Fyyre <fyyre@fyyre.net>

* receiver: validate partial-dir basis state

* fix: reject alt-dest leaf symlinks

* fix: preserve basis open flags

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-21 07:51:55 +10:00
Zen Dodd 7c7e1bee86 syscall: follow trusted sender ancestor links (#1088)
* syscall: follow trusted sender ancestor links
* testsuite: accept BSD symlink errors
* syscall: anchor sender paths at explicit source roots
* flist: pin explicit file source parents
* flist: preserve search-only source traversal
* sender: follow trusted files-from ancestors
* sender: enforce files-from symlink ownership
* sender: harden files-from path resolution
2026-09-20 23:36:00 +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
Zen Dodd f615240601 zlib: avoid zero-bit shift in inflatePrime (#1093) 2026-09-16 22:56:45 +10:00
Zen Dodd bf626f3f7c build: normalise amd64 optimisation object names (#1092) 2026-09-16 21:20:55 +10:00
Aleksandr Burlakovanda.burlakov ad79af2f0c zlib: avoid signed shift in inflatePrime - #1091
Problem:
The value of an arithmetic expression 'value << state->bits' is a subject to overflow because its operands are not cast to a larger data type before performing arithmetic
Solution: Cast value to unsigned long before the left shift to prevent signed integer overflow during shift evaluation.
Signed-off-by: a.burlakov@fobos-nt.ru

Signed-off-by: a.burlakov@fobos-nt.ru
Co-authored-by: a.burlakov <a.burlakov@aburlakov.ru>
2026-09-16 21:19:58 +10:00
Zen Dodd 4a65e70dfd rrsync: restore restricted-root paths (#1055) 2026-09-16 20:19:31 +10:00
Omar Elsayed 7cc29215a6 Fix handling of /dev/std{in,out,err} pseudo-paths and namespace overflow UID checks (#1085)
Using --log-file or --files-from with /dev/stdin, /dev/stdout, or /dev/stderr
attached to a pipe previously failed in two ways:

1. On standard hosts, it failed with ENOENT because the symlink target
   (pipe:[N]) was treated as a relative file path rather than a secure
   kernel pseudo-path.
2. Inside user namespaces (e.g., rootless podman, unshare), it aborted
   with ELOOP ("refusing to follow a symlink owned by an untrusted user").
   The symlink ownership reported the kernel overflow UID (65534), which
   completely blocked the walker from following the pseudo-paths.

This patch resolves the issues by refining the symlink path walker and
trust mechanisms:

* Updated fd_pin_tail() to natively recognize /dev/stdin, /dev/stdout,
  and /dev/stderr, parsing them directly to their corresponding /0, /1,
  and /2 descriptor tails.
* Introduced the is_anchored variable to strictly enforce absolute paths,
  ensuring malformed paths cannot bypass the check using relative forms
  like dev/fd/ or proc/self/.
* Removed pin_transit from the namespace_pin check and replaced it by
  adding the is_anchored variable to the check. Checking for root confinement
  or daemon status is unnecessary when we are already validating and restricting
  traversal to known safe paths (Daemon also refuses any symlinks pointing to /).
* Added new cases to the test suite to validate standard stream pseudo-path
  handling and ensure namespace overflow UID bypasses work correctly
  without regression.
2026-09-16 20:17:17 +10:00
Zen Dodd 7ee931c7b9 daemon: distinguish inetd from local stdin sockets (#1075)
* daemon: require IP stream for inetd mode
* daemon: preserve bidirectional stdio mode
* daemon: honour explicit no-detach mode
* daemon: preserve inetd socket detection
* ci: fix daemon socket checks
2026-09-11 09:10:18 +10:00
Samuel Henrique 43a5fa44a8 docs: list --password-file among the confined operator paths (#1082) 2026-09-09 13:30:01 +10:00
Zen Dodd 7fef1c827e docs: correct devices and specials behaviour (#1078) 2026-09-04 16:54:28 +10:00
Zen Dodd 59be391641 syscall: explain untrusted symlink refusal (#1074)
* syscall: explain untrusted symlink refusal
* syscall: emphasise insecure-links warning
2026-09-03 17:05:03 +10:00
Omar Elsayed a93490077d batch.c: Allow FIFO pipes in batch file processing. Regression fix (#1060) 2026-09-03 17:03:54 +10:00
Omar ElsayedandZen Dodd a68b32cd5c Fix ENOENT when resolving kernel pseudo-paths in ona_open (#1054)
* Fix ona_open to safely resolve bash process substitution pseudo-paths
Bash process substitution (e.g., `<(...)` or `>(...)`) exposes file
descriptors as symlinks under `/proc/self/fd/X` pointing to kernel
pseudo-paths such as `pipe:[12345]`. Previously, `ona_open()` would read
this target and attempt to resolve it as a literal file path on disk,
causing the operation to fail with `ENOENT` and breaking legitimate local
process substitution.

This patch safely intercepts and resolves these pseudo-paths while
maintaining strict confinement boundaries and averting TOCTOU risks:

- Detects kernel pseudo-paths (`pipe:[`, `socket:[`, `anon_inode:`)
  only when `fd_pin_tail` confirms the path resolves precisely to a
  direct child of a valid FD directory.
- Categorically rejects pseudo-path resolution if `confine_root` is
  active (yielding `ENOENT`).
- Strips `O_NOFOLLOW` for legitimate leaf pseudo-paths, allowing
  `openat()` to correctly delegate resolution.
- Reverts `fd_pin_tail` to its upstream signature, as manual PID
  validation is no longer required due to the secure `openat()` design.

* testsuite: expect pseudo-path skip on Alma
* syscall: reject trailing pseudo-path components

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-03 08:11:16 +10:00
LIChengGang a4bfa170fa main: allow --contimeout for daemon connections over --rsh (#1071)
* main: allow --contimeout for daemon connections over --rsh

The --contimeout guard rejected the option whenever connect_timeout was
set and the connection was not being made by a socket, so a daemon
reached through a remote shell (daemon_connection == 1) was refused the
same way as a plain non-daemon remote-shell transfer.  That is how
rsync-ssl talks to a daemon: it runs rsync with --rsh pointing at its
helper, so "rsync-ssl --contimeout=60 host::mod" died with "The
--contimeout option may only be used when connecting to an rsync
daemon" before it could even connect.

Only reject --contimeout when there is no daemon connection at all, so
the option is accepted for a daemon-via-rsh connection (and still works
unchanged for the socket daemon path, which returns earlier through
start_socket_client).

Add a regression test that drives a daemon-via-rsh invocation and
asserts the option is not rejected, while a plain remote-shell
destination is still refused.

* main, io: honor --contimeout for daemon-via-rsh connections

The previous commit made --contimeout legal for a daemon reached
through a remote shell, but rsync still did not act on it: the socket
path times its connect() via an alarm, while the daemon-via-rsh path
spawned the helper and then blocked in start_inband_exchange() waiting
for the daemon greeting with no connect timeout at all.  A hung helper
-- e.g. rsync-ssl's openssl connect to an unreachable host -- would hang
the transfer forever.

Give the client a second wall-clock deadline, client_connect_deadline,
set from --contimeout around do_cmd() + start_inband_exchange() and
cleared once the greeting exchange finishes.  handshake_poll_timeout_ms()
now also checks it and exits with RERR_CONTIMEOUT ("timeout waiting for
daemon connection"), matching the socket path's exit code, instead of the
daemon-handshake deadline's RERR_TIMEOUT.

This covers the helper spawn, its connect/TLS handshake, and the greeting
exchange as one connection-establishment phase, so it works uniformly for
openssl, stunnel, and gnutls rsync-ssl backends.

Extend the contimeout-rsh test to drive a helper that never connects and
assert rsync aborts with exit 35 within the timeout, and that a working
daemon-via-rsh connection still completes.
2026-09-03 08:10:05 +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
Zen Dodd 270202affb ci: adjust Cygwin common skip expectations (#1073) 2026-09-03 07:22:38 +10:00
Zen Dodd 90992088d6 ci: apt-update before install (#1072) 2026-09-03 06:43:29 +10:00
Zayd Rajab 240bd9df96 syscall: use O_PATH for held directory traversal (#1065)
Use O_PATH on Linux for directory descriptors used only for path
traversal, fchdir(), or as *at() parents. Reopen final directory
endpoints with the caller-requested access mode and retain the existing
O_RDONLY fallback elsewhere.

Add coverage for exact sources and readable descendants beneath
search-only directories, known-file creation beneath write/search-only
destinations, and retained refusal to enumerate unreadable directories.

Fixes #1064.
2026-08-28 19:37:01 +10:00
LIChengGang 7c20b077c9 rsync-ssl, testsuite: accept --type=SSL_TYPE anywhere in the args (#1047)
* rsync-ssl, testsuite: accept --type=SSL_TYPE anywhere in the args

The --type=... option was only recognized as the first argument, so a
command such as "rsync-ssl --dry-run --type=stunnel host::mod" passed
the option through to the underlying rsync, which rejected it as
unknown.  Scan the full argument list for --type=..., export
RSYNC_SSL_TYPE, and drop the option before handing the remaining args
to rsync.  The manpage no longer says the option must be first.

Adds a test that runs rsync-ssl with a fake rsync in PATH and checks
that --type= is consumed in first, middle, and last positions.

* fix(rsync-ssl): stop interpreting wrapper options at --

rsync-ssl consumed a --type=... operand that appeared after a --
argument, even though -- explicitly protects the rest of the command
line from option parsing.  Stop scanning for --type=... at a --
argument: preserve the -- and every subsequent argument verbatim,
passing them through to rsync unchanged.  Add regression coverage for
this case and document the behavior in the manpage.

* test(rsync-ssl): assert RSYNC_SSL_TYPE and exit status in the type-option test

The rsync-ssl-type-option test only checked that --type= tokens were
removed from the rsync argv, so it would also pass if the wrapper
silently discarded the requested SSL implementation instead of
exporting RSYNC_SSL_TYPE.  The fake rsync now records both the argv it
receives and the RSYNC_SSL_TYPE value it observes; each invocation
asserts the expected value (or UNSET when the wrapper must not consume
anything), and run() fails the test unless rsync-ssl exits
successfully.
2026-08-20 19:27:55 +10:00
ᴄʜʀɪsᴛᴏᴘʜᴇʀ ᴍᴇɴɢ f0177d82a8 Add IDN support (#986)
* Add IDN support

rsync can now connect to IDN (internationalized domain name) hosts, and
IDN names are recognized in a daemon's hosts allow/deny.

* idn: convert host names label by label

The IDNA mapping folds some non-ASCII characters onto ASCII ones, so
running a whole hosts allow/deny token through idn2_to_ascii_8z() could
hand back a pattern the admin never wrote: a "*" (U+FF0A FULLWIDTH
ASTERISK) entry came back as "*" and let every host in.

Convert label by label instead, keeping an ASCII label byte for byte and
using a converted label only when it comes back as a bare A-label. An
ASCII-only config now behaves as it did before there was IDN support, and
a token that cannot be converted is left alone and so matches nothing. The
client side shares the same helper, and neither side truncates a name at
its 1024-byte buffer any more. strlower() folds only ASCII now, since its
one caller is the hosts allow/deny list, which can hold UTF-8.

Adds testsuite/daemon-access-idn and extends testsuite/idn to cover
Unicode, punycode, mixed-case and invalid input on both sides.

* idn: hand libidn2 the same flags on both sides

The client path called idn2_lookup_ul() without IDN2_NFC_INPUT while the
daemon path passed it to idn2_to_ascii_8z(). Both normalize either way --
idn2_lookup_ul() ors the flag in itself, and TR46 normalizes as it maps --
but there is no reason for the two calls to read differently, so pass one set
of flags from one place. The flag asks libidn2 to normalize the label rather
than promising that it already is: it gates the u32_normalize() call, and
without it a decomposed label comes back IDN2_NOT_NFC.

Adds composed/decomposed cases to testsuite/idn, which sees the exact host
name rsync hands out, and a decomposed hosts allow token to
testsuite/daemon-access-idn.
2026-08-20 18:51:23 +10:00
Samuel Henrique 0145b9128d testsuite: interpose the large-file spellings of open/openat/fstatat (#1063)
Which symbol names this test's LD_PRELOAD hook exports is decided by the
compiler that builds it, and which names rsync imports is decided by
configure -- and the two do not have to agree.

Where off_t is not already 64 bits, configure's AC_SYS_LARGEFILE adds
-D_FILE_OFFSET_BITS=64 (i386 and alpha in Debian), so glibc redirects every
open()/openat()/fstatat() call in rsync to open64(), openat64() and
fstatat64().  The hook is compiled by a bare "cc", so on those architectures
it defines only the unsuffixed names: the receiver's opens never reach it, no
EACCES is injected, the marker the positive control looks for is never
written, and the test fails with

    positive control failed: receiver did not open the existing partial file
    with O_CREAT (rc=0, output='')

It happens to work on Debian's 64-bit time_t ports (armhf, hppa, powerpc,
...) only by luck: their gcc predefines -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64,
so glibc's __REDIRECT renames the hook's own DEFINITIONS as well and it ends
up exporting exactly the *64 names rsync imports.

Define both spellings explicitly so the hook interposes whichever set the
rsync under test was linked against, and #undef the two macros at the top of
the hook so that renaming cannot happen -- otherwise, on precisely those ports
whose compiler predefines them, open() would be emitted as open64() and
collide with the explicit wrapper ("symbol `open64' is already defined"),
leaving the hook unbuildable and the test skipped.

The new pointers are resolved through hook_resolve(), so the existing
nested-dlsym recursion guard covers them as well.

Failing build logs:
  i386  https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=i386&ver=3.5.0%2Bds1-1&stamp=1786938290&raw=0
  alpha https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=alpha&ver=3.5.0%2Bds1-1&stamp=1786973432&raw=0
2026-08-19 20:37:09 +10:00