7980 Commits
Author SHA1 Message Date
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
Samuel Henrique 9371b33320 testsuite: probe the punch granularity each hole assertion relies on (#1062)
The last section of preallocate_test.py writes 256 blocks of
[4 KiB data][24 KiB zeros][4 KiB data] and requires --inplace --sparse to
deallocate at least half of the file.  A punch only frees whole allocation
units, and that interior run sits 4 KiB into each 32 KiB block: with a 4 KiB
unit it covers six whole units (24 KiB freed per block), but with a 16 KiB
unit it covers none at all -- [0,16K) holds live data at [0,4K) and [16K,32K)
holds live data at [28K,32K) -- so st_blocks cannot move whatever rsync does.

That fails the build on Debian loong64, whose kernel uses 16 KiB pages:

    --inplace --sparse left matching interior zero runs allocated: 8388608 of
    8388608 bytes remain allocated after a 8388608-byte matched-block update

i.e. exactly "nothing was freed".  fs_can_punch_holes() does not catch it
because it punches a whole 64 KiB file, which frees blocks at any granularity.

Generalise that helper into punch_frees(offset, length, size) so each
assertion can probe the shape it actually depends on, and gate the interior
one on the exact layout written just above it.  Where the filesystem can free
that run the assertion runs exactly as before; where it cannot, the test says
so rather than reporting a regression.  The content check is unconditional
either way.

While here, call fallocate64() instead of fallocate().  ctypes declares the
offset and length as c_longlong, but glibc's fallocate() takes 32-bit off_t
where off_t is 32 bits, so the callee reads the high half of `offset` as its
`length`, the call fails with EINVAL, and can_punch comes back False -- which
is why none of this file's hole-punching assertions have ever run on a 32-bit
port.  On i386:

    fs_can_punch_holes() as written : before=128 ret=-1 errno=22 -> False
    the same probe via fallocate64  : before=128 ret=0            -> True

fallocate64() takes off64_t on every architecture and is a plain alias of
fallocate() where off_t is already 64 bits.  The probe also writes urandom
rather than a repeated byte, so a filesystem that compresses does not answer
"nothing was freed" merely because nothing was allocated.
2026-08-19 20:26:04 +10:00
Zen Dodd 7f01ae0af1 testsuite: sort platform skip lists (#1061) 2026-08-16 22:02:30 +10:00
Zen Dodd 3d2caaa0cb testsuite: bound the unshare probe (#1049) 2026-08-16 21:55:00 +10:00
Zen Dodd 3b1eb8dd7f syscall: use O_PATH for directory traversal (#1052) 2026-08-16 21:52:11 +10:00
Zen Dodd 324a1f0716 rrsync: support fd pins in user namespaces (#1048)
* rrsync: support fd pins in user namespaces
* rrsync: support /dev/fd pins in user namespaces
2026-08-16 21:51:07 +10:00
Zen Dodd 010aa15dec testsuite: test install-strip tool selection (#1027) 2026-08-16 21:50:52 +10:00
Alessandro Di NepiandClaude Opus 4.8 a49f085a4f Honor $(STRIP) in install-strip for cross-compilation (#1024)
* Honor $(STRIP) in install-strip for cross-compilation

The install-strip target hard-coded `install -s`, which strips via the
install program using the build host's strip and ignores the STRIP
variable. When cross-compiling this runs the host strip against a
target binary and fails.

Pass --strip-program=$(or $(STRIP),strip) so the target strip is used
when STRIP is set (as cross toolchains and build systems provide),
falling back to plain `strip` for native builds. A plain `make install`
is unaffected.

* Make install-strip portable (address review)

- Detect the target strip via AC_CHECK_TOOL([STRIP],[strip],[strip]) in
  configure.ac (picks up the cross-prefixed strip when cross-compiling,
  defaults to plain strip otherwise) and substitute @STRIP@ in Makefile.in.
- Rewrite install-strip to run a normal install then $(STRIP) on the
  installed rsync binary, dropping the GNU Make $(or ...) and the GNU
  install --strip-program extension that broke with install-sh/BSD install.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-16 21:14:14 +10:00
Zen Dodd 4c029dd9d3 ci: stabilise Cygwin tests (#1057)
* ci: stabilise Cygwin tests
* ci: install Cygwin directly
* ci: wait for Cygwin setup
2026-08-15 17:03:31 +10:00
Zen Dodd 5351b53276 ci: run PR checks without labels (#1058) 2026-08-15 16:45:26 +10:00
Anja Lea Brinkmann ec52eed3ed Update COPYING.html (openssl & xxhash exceptions) (#1040) 2026-08-15 16:28:18 +10:00
Zen Dodd 195b4c6d30 testsuite: make basis xname oracle deterministic (#1051) 2026-08-15 16:19:15 +10:00
Zen Dodd bd48f0751b docs: clarify --files-from deletion scope (#1041) 2026-08-15 16:01:09 +10:00
Andrew Tridgell dcef974c7c packaging: fixed an error in release process 2026-08-13 10:42:24 +10:00
Andrew Tridgell 471e17dc0d Preparing for release of 3.5.0 [buildall] v3.5.0 2026-08-13 10:05:47 +10:00
Andrew Tridgell 7a36355a8c NEWS update for 3.5.0
fixed some typos and add in old 3.4.4 release info
2026-08-13 09:50:33 +10:00
Andrew Tridgell 40c93b173e update web pages and news for 3.5.0
ready for security release
2026-08-13 09:16:21 +10:00
Andrew Tridgell 6dfa73fc90 version.h: bump to 3.5.0 for the release
Drops the "dev" suffix on RSYNC_VERSION ahead of the
2026-08-13 00:00 UTC public release.
2026-08-07 15:25:11 +10:00
Andrew Tridgell d0ededae4e NEWS: finalise the 3.5.0 release entry
Date the release and bring the security section up to the full set: it
described 20 CVEs, and the release fixes 33.

The thirteen later items are added in three groups -- the peer-triggerable
memory-corruption findings from the daemon-protocol fuzzing pass, the daemon
availability and access-control issues, and the two client-side ones.

Several changes were previously described here as carrying no CVE and now do,
so those claims are removed rather than left to contradict the advisories:
rsync-ssl's unverified TLS is CVE-2026-70454, the non-positive MSG_IO_TIMEOUT
is part of CVE-2026-70462, and the early-protocol argument-count bound is part
of CVE-2026-70464.  What is left under "no CVE assigned" is only the proxy
header bounds and the xattr expansion cap.
2026-08-07 15:25:11 +10:00
Andrew Tridgell 46ad34f28d runtests: honour the built tree's backport skip list
A stable-backport branch runs a newer suite than its own code.  fleettest
already reads testsuite/skiplist/backport.txt from the tree being built and
excludes those tests; runtests.py did not, so running the suite directly --
which is what the backport branches' CI job does -- tried to run tests that
base cannot support.

Read the same file from tooldir and drop its names from both the run and the
expected-skip set: an excluded test never runs, so leaving it in the expected
set would make the oracle demand a skip that cannot happen.  A stale name is
an error rather than a silent no-op.
2026-08-06 14:28:03 +10:00
Andrew Tridgell 3d10e84266 docs: distinguish accepted breakage from absent features in backport.txt
Also note that backport CI, once it exists, has to consume these lists the
same way fleettest does -- read backport.txt from the branch being built and
pass it as RSYNC_EXCLUDE -- or it will fail on every entry and get switched
off.
2026-08-05 15:12:44 +10:00
Andrew Tridgell 2d1a91eb0f fleettest: drop backport-excluded tests from the expected-skip set too
A test named in a backport's backport.txt never runs, so it cannot skip either.
If the suite's expected-skip list also names it -- the two --compress-threads
tests are declared as expected skips because they need --use-tcp -- the oracle
waits for a skip that can no longer happen and every pipe cell reports a skip
mismatch.

Emit a '-name' removal for those, as the per-target expect_skip_omit already
does.  Only for names the spec actually contains: runtests rejects a '-name'
that removes a name nothing added, and most of a backport's exclusions (a test
for a feature it lacks) are not expected skips at all.  Deciding that needs the
@FILE references expanded locally, which is what _expand_spec_names does.

v3.4.1 with this: 5/5 cells OK on ubuntu-2404, from 3 OK / 36 not OK across the
fleet before the mechanism existed.
2026-08-05 13:38:47 +10:00
Andrew Tridgell 5cd4c682c8 fleettest: let a backport branch declare tests it cannot run
Running the 3.5.0 suite against an older branch (--repo BACKPORT
--testsuite-repo .) reports a wall of failures that are not regressions: tests
for fixes the branch does not carry, and tests whose unit-test helpers its
Makefile cannot build.  Both backport branches came back 3 OK / 36 not OK with
every distinct failure explained that way, which makes the run useless as an
oracle -- a real regression would not stand out.

A backport now declares those in its own testsuite/skiplist/backport.txt, read
from the tree being BUILT rather than the one providing the suite: only the
built tree knows what it lacks.  The names go to runtests.py as RSYNC_EXCLUDE
rather than as an expected-skip declaration, because some of them fail rather
than skip and an expected-skip list cannot describe a failure.

The overlay that puts a newer testsuite/ onto an older tree is a merge with no
delete, so a file that exists only on the backport survives it.  skiplist-spec
exempts the name from its every-list-must-be-referenced rule, since nothing
references this one by design.
2026-08-05 13:09:20 +10:00
Andrew Tridgell e8c79d2d62 testsuite: cover the empty-dir_flist parent_ndx wild-pointer read
Under inc_recurse the first flist (ndx_start == 1) has no parent entry of its
own, so recv_file_list() trusts the peer's "." entry to be the transfer root and
leaves parent_ndx at the flist_new() default of 0 -- dir_flist->files[0].  Only
S_ISDIR entries are appended to dir_flist, so a peer that sends "." with a
NON-directory mode keeps dir_flist->used at 0 while the basename strcmp still
passes: parent_ndx stays 0 and the consumers index a never-written slot.

Drives a real daemon with the pure-Python sender: an inc_recurse push whose only
flist is a regular file "." plus a regular file "a" (no directory anywhere, and
"." sorts lowest; "a" keeps file_total != 1 so the receiver doesn't divert into
recv_additional_file_list).  The file list alone is what does it -- the
generator crashes in generate_files() before any transfer phase -- reproduced on
released 3.2.7, 3.4.0 and 3.4.1.

The oracle needs both halves: a positive control that the daemon logged
"receiving file list", and the condition-specific refusal.  Accepting any
"rsync error:" line is not enough -- with "." sent as a valid directory and a
bogus file index, that form passes on "File-list index 1000000 not in 0 - 2"
without the crafted transfer root ever reaching the parser.

A fixed daemon has already refused the list and exited by the time the ndx-0
token is sent, so that send and the drain can hit a closed socket; Linux and
FreeBSD swallow it, Solaris, the other BSDs, macOS x86 and Cygwin raise
EPIPE/ECONNRESET.  Treat it as an expected outcome, not a result.

The header records what this does not prove: it gates the attack shape rather
than the parent_ndx clause (only the parse-time transfer-root check fires on a
current build), it does not exercise the receiver-side consumer, and the
dereferenced slot is not guaranteed NULL since dir_flist->files[] comes from
realloc(), not calloc().
2026-08-03 21:03:26 +10:00
Andrew Tridgell 9e40ef0eaf NEWS.md: record the safe_arg() uninitialized-byte leak
The fix shipped in the test10 snapshot but was never written up: safe_arg()'s
filename-mode buffer sizing disagreed with the writer, leaving an uninitialized
heap byte in the argument handed to the remote shell when --protect-args is off.
2026-08-03 21:03:26 +10:00
Andrew Tridgell f50d53d056 testsuite: judge the handshake deadline on the daemon's own clock
The daemon sets its deadline with time(NULL) (set_daemon_handshake_timeout,
io.c), and this test measured the elapsed time with CLOCK_MONOTONIC.  Those
agree on a quiet machine and diverge on a stalled one: a virtualised guest
resyncs its wall clock after the host deschedules it, while monotonic keeps
its own count.  The daemon then closes exactly when it meant to and the test
reports it closed early.

That is what a NetBSD CI run showed -- "closed after 39.55s, before the
expected timeout window (58.75s)" -- and it is the same shape as the macOS
failure that turned out to be the machine sleeping mid-test.

Measure the bound on the clock the daemon decides with.  Monotonic still
drives the poll budget, where the job is only "do not hang forever".

On its own that would trade a false failure for a false pass, which is worse:
a refusal or a crash arriving just as the guest's wall clock caught up would
read as a clean timeout, and no diagnostic would fire because the test would
be green.  So when the two clocks disagree -- wall says on time, monotonic
says early -- neither settles it, and the daemon has to have recorded its own
deadline firing.  The offset of its log is taken before each observation, so a
timeout it logged earlier cannot vouch for this one.

Failures now carry both clocks and that window of the daemon's log, bounded
and with control bytes escaped.  The clock note states the discrepancy without
concluding from it: a stalled host produces it, but so does an NTP step, and
either can accompany a real failure.
2026-08-03 14:49:48 +10:00
Andrew Tridgell 11e43daf90 socket: don't let open_socket_out() hang in connect()
The same kernel-side missed wakeup on the other side of the connection: a
blocking connect() can sleep forever on a connection that is already
established, with the 4-tuple ESTABLISHED at both ends and the listener's
greeting queued unread. Without --contimeout nothing breaks it.

Wait for the connect with poll() in slices rather than blocking in the
kernel, re-checking the socket on each pass, and take the result from
SO_ERROR. A finished slice is not a failure -- looping is what re-examines
the socket and recovers a missed wakeup.

--contimeout is unchanged: the alarm still fires and the caller still
reports RERR_CONTIMEOUT. The per-address errno is now stashed before
close()/alarm() can overwrite it.

Measured the same way, against a real loopback daemon: 20 hangs in 48,000
connects before, 0 in 48,000 after, with equal wall clock. This is the half
of the OpenBSD flakiness that the socketpair_tcp() fix does not cover: the
--use-tcp pass talks to a real rsyncd over a port, so it hangs here rather
than in accept().
2026-08-03 13:02:33 +10:00
Andrew Tridgell ea5a3a277f socket: don't let socketpair_tcp() hang in accept()
On OpenBSD a blocking accept() can sleep forever on a connection the kernel
has already completed: the 4-tuple is ESTABLISHED at both ends, the
connection is queued on the listener, and the accept()ing process is still
asleep in netacc. Nothing bounds that wait, so rsync hangs for good.

Poll the listener instead, with a non-blocking accept(), so a missed wakeup
costs another pass rather than the process. The accepted fd is put back
into blocking mode explicitly because BSD accept() gives it the listener's
non-blocking flag. A time(NULL) deadline bounds the whole wait the way
io.c bounds its own, rather than counting passes -- a signal on every pass
must not extend it and a poll() that returns at once must not consume it.
A listener that reports ready without yielding a connection (the peer can
reset first) pauses rather than spinning.

Measured on an OpenBSD 7.8 VM, driving the real binary through
RSYNC_CONNECT_PROG with 8 concurrent workers, alternating stock/patched
rounds: 111 hangs in 120,000 invocations before, 0 in 120,000 after, with
no change in throughput.

Every daemon test reaches socketpair_tcp() through RSYNC_CONNECT_PROG in
the default transport, so the hang landed on whichever daemon test happened
to be connecting. See dev-notes/openbsd-socketpair-accept-wedge.txt.
2026-08-03 13:02:33 +10:00
Andrew Tridgell d4d183c58c NEWS: note the AVX2 rolling-checksum over-read 2026-08-03 08:48:33 +10:00
Andrew Tridgell 9a382baddb fleettest: build one target with the SIMD/asm optimizations
Nothing in CI or the fleet has ever set --enable-roll-simd, --enable-roll-asm
or --enable-md5-asm, which is why the over-read above sat behind a "fixed"
label for two months, and why the fix applied for it went to the wrong
assembly file.

mac-x86-asm is the same host and OS as mac-x86 with all three on.  Mach-O is
the interesting part -- both problems reported against these flags were
macOS-x86-64 -- and it is the only machine in the fleet that can build the
x86-64 assembly at all.

It needs MacPorts clang 19 through CC/CXX, because Apple clang 10 (the ceiling
on macOS 10.13) rejects configure's target("default") multiversioning probe.
mac-x86 keeps the stock Apple compiler, which is what caught #161, so the two
cover different ground rather than one replacing the other.

simd-checksum is a macOS-wide expected skip, since simdtest is only built when
SIMD is enabled; this target subtracts it, because running it is the point.

Also corrects mac-x86's comment, which claimed the probe "cannot compile here
with any clang".  It is a compiler-version limit: clang 19 on that same box
compiles, links and runs it.
2026-08-03 08:48:33 +10:00
Andrew Tridgell 63fcfa399e testsuite: run the SIMD checksums against an unreadable page
simdtest allocated 64 spare bytes so it could test an unaligned buffer, which
is exactly the slack that hid a 64-byte over-read in the AVX2 assembly for as
long as it existed.  Add a pass that places the buffer flush against a
PROT_NONE page, so a read past the end faults in the test rather than in
somebody's transfer.

Every length from 128 to 4096, so each remainder mod 64 and both alignments
are covered, and all four implementations are checked -- the assembly was the
one at fault here, but the intrinsic paths preload too.

It fails closed.  A guard this test cannot set up means it is not testing what
the caller thinks, so a failed sysconf/mmap/mprotect is a failure rather than a
pass that looks identical to a real one.  And because the dispatcher falls back
on a CPU without AVX2 -- where the guard loop proves nothing about the code
under test -- it says which of the two happened rather than letting a fallback
run read as coverage.

Without the fix this segfaults; the suite's simd-checksum test reports the
non-zero exit.
2026-08-03 08:48:33 +10:00
Andrew Tridgell 2979d8eddc simd: stop the AVX2 rolling checksum reading past its buffer
The loop is software-pipelined: each iteration folds in the 64 bytes it
preloaded last time and preloads the next 64.  Nothing stopped the final
iteration doing that preload, so it always read the 64 bytes after the region
it was asked to checksum.

Not an edge case.  The assembly processes len&~63 and leaves the remainder to
the caller, so the remainder is by construction under 64 bytes and the preload
passed buf+len on every call, by 64 minus the remainder.

It normally landed in slack inside the map_ptr() window and nothing noticed.
Where the buffer ended near an unmapped page it was a SIGSEGV in the middle of
a transfer -- reported on macOS x86-64 by Roland Kletzing, whose `partial` run
died with "connection unexpectedly closed" because the generator had crashed.
A guard page reproduces it on Linux too, so it was latent there, not absent.

Run the pipelined loop one block short and finish the last block in .last,
which does the same arithmetic without the preload.  No per-iteration cost, and
checksums are bit-identical -- simdtest compares every implementation against
the C reference.

The earlier fix for that report, "lib: use .balign in md5 x86-64 asm", was to
the md5 assembly.  It addressed the linker alignment warning that appeared
alongside, not this.
2026-08-03 08:48:33 +10:00
Andrew Tridgell bb6329bc8c NEWS: note the --link-dest hard-link fallback 2026-08-03 07:10:00 +10:00
Andrew Tridgell d12bdb1579 testsuite: cover the --link-dest hard-link refusal, and ask the filesystem
An LD_PRELOAD hook refuses linkat() for a symlink source only, so the arm is
reachable on a filesystem that hard-links symlinks perfectly well.  Three
controls keep it from proving less than it looks:

 - the regular file in the same transfer must still be hard-linked, or "it fell
   back" would also be satisfied by --link-dest having been abandoned;
 - the itemised run must emit exactly one "cL... sym -> some-target" line.  A
   plain -a run cannot see a duplicated itemisation, which is how that defect
   reached an HFS+ target before this was added;
 - EPERM and ENOSYS must fall back too, since errno does not separate "cannot"
   from "may not".

itemize picked its expected change-type letter from the build capability, which
is the wrong question -- the link happens on whichever filesystem holds the test
data.  Ask that one too, and drop the XFAIL the old mismatch needed.

The hook is Linux-only, so the test joins the macOS and Cygwin skip lists,
which are required to be sorted.
2026-08-03 07:10:00 +10:00
Andrew Tridgell d09edb85e6 generator: fall back to a copy when the destination cannot hard-link
CAN_HARDLINK_SYMLINK and CAN_HARDLINK_SPECIAL are decided by configure running
linkat() on whatever filesystem the build tree happened to sit on.  The
destination is free to disagree, and one host can hold both answers: macOS
builds on APFS, which can hard-link a symlink, and backs up to HFS+, which
returns ENOTSUP.

A build that said yes had no fallback left.  try_dests_non() reported the
refusal as a transfer error and returned a matched basis, so the caller created
the entry anyway -- correctly -- and the run still exited 23.  Every
neighbouring case copes: a regular file whose link() fails goes to try_a_copy,
and a build compiled without either macro resorts to --copy-dest behaviour.
This was the same situation, discovered a little later, and the only one
treated as fatal.

Take the existing fallback on any refusal.  Singling out the "cannot" errnos is
not possible: link(2) documents EPERM both for a filesystem with no hard-link
support and for an ordinary permission refusal, and FUSE reports ENOSYS for the
same thing.  It is also what the regular-file path next door has always done
(try_dests_reg -> hard_link_one -> try_a_copy), and consistency between the two
was the point.  Where the errno does matter the surrounding transfer says so
anyway: ENOSPC, EDQUOT and EROFS fail the creation independently, EMLINK and
EXDEV mean the link was never possible.  EIO alone goes unremarked; reporting
it would put a line into --link-dest's itemised output, so it is left out on
purpose.

Returning -3 rather than -2 keeps the caller out of the "already up to date,
skip it" arm, which under --link-dest would drop the entry entirely.  Both
callers give -3 the treatment the compile-time fallback already gets -- clearing
itemizing and code -- because try_dests_non() has itemised the match itself and
would otherwise report the entry twice.

The fallback is silent, matching a build that cannot link these at compile
time; documented under --link-dest instead.
2026-08-03 07:10:00 +10:00
Andrew Tridgell 36be3b4e70 NEWS: note the merge-file confinement half of the filter fix 2026-08-03 05:35:03 +10:00
Andrew Tridgell 1bea181096 testsuite: an rrsync restricted dir must bound merge files
Uses the exclude-only merge form, which leaves no diagnostic to assert on: the
escape shows up as a file silently missing from the transfer, so the test reads
the oracle the same way an attacker would.  Pull mode, so no --delete is
involved.  Each case requires the transfer to have succeeded as well, since
refusing outright would hand the peer a denial of service.

A second escape reaches the source through a symlink, which is what makes
rsync's tracked cwd and the real one disagree -- the shape that catches a
lexical seed.  That one drives --confine-root directly: rrsync rejects the
argument spellings that would carry it, so routing it through the wrapper would
pass either way and prove nothing.

Both controls repeat their escape with an in-tree merge target and require it
to be read AND obeyed, since "the transfer failed" and "every merge file is
refused" would otherwise satisfy the escape assertions on their own.
2026-08-03 05:35:03 +10:00