Commit Graph

7586 Commits

Author SHA1 Message Date
Andrew Tridgell
bc63ea82f2 ci: run the OpenBSD --use-tcp test step at -j2
The OpenBSD job runs inside a nested VM. At -j8 the --use-tcp run starts
many concurrent loopback daemons, and under that resource pressure the
daemon connection handshake occasionally loses a timing race and one test
hangs to the 300s runner timeout. It is an environment artifact, not an
rsync defect: the daemon handshake writes-then-reads with unbuffered early
I/O (no flush/mutual-wait deadlock), the indefinite wait is the documented
no-timeout daemon behaviour, and it does not reproduce off OpenBSD even with
the full suite pinned to a single CPU at -j8.

Drop just this job's --use-tcp parallelism to -j2 so the nested VM stops
over-subscribing; the pipe `make check` and every other platform are
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:44:12 +10:00
Andrew Tridgell
0d4fb1bc89 testsuite: cover more path/file-operation code (syscall.c, util1.c, delete.c)
Target previously-uncovered functions in the path/file-operation files the
resolver restructure touches, confirmed hit under coverage:

  preallocate   --preallocate (syscall.c do_fallocate) and sparse hole-punching
                via --preallocate --sparse and --inplace --sparse (do_punch_hole),
                on a file several levels deep.
  fuzzy-basis   --fuzzy basis selection with similar-named candidates and no
                exact match, so the generator scores them (util1.c fuzzy_distance).
  delete-deep   add a --backup --delete case so removing an extraneous
                backup-suffixed file consults delete.c is_backup_file.

preallocate probes --preallocate support up front and skips where it is
unavailable: macOS, the *BSDs and Solaris build without fallocate/posix_fallocate
(and FALLOC_FL_PUNCH_HOLE is Linux-only), and reject the option outright. It runs
on Linux and Cygwin. fuzzy-basis and delete-deep are plain local transfers with
no skips. All green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:44:12 +10:00
Andrew Tridgell
52480aaac2 runtests: compare expected-skipped order-insensitively; register daemon-access-ip
The --expect-skipped check compared the skip list as an ordered string, so the
per-platform RSYNC_EXPECT_SKIPPED lists had to match runtests' collection order
(sorted filenames) exactly -- a subtle, easy-to-break ordering dependency.
Compare the skipped SET instead; which tests skipped is what matters.

Register the new require_tcp test daemon-access-ip in the per-platform
expected-skipped lists (it skips in the pipe-transport make check, like
daemon-chroot-acl and proxy-response-line-too-long).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:44:12 +10:00
Andrew Tridgell
702a8f61b7 testsuite: cover daemon access-control, config includes, --stop-at
Target the lowest-coverage rsync files identified from a merged (pipe + proto29/30
+ tcp) gcov report:

  daemon-access-ip  hosts allow / hosts deny with exact-IP and CIDR patterns over
                    --use-tcp, exercising access.c make_mask/match_address/
                    match_binary (19% -> 62% lines), plus client --address
                    (socket.c try_bind_local). require_tcp.
  daemon-config     the &include rsyncd.conf directive (params.c include_config/
                    parse_directives, 48% -> 60%) and a module with a missing path
                    (clientserver.c path_failure).
  stop-time         --stop-at future/past (options.c parse_time) and --stop-after
                    (options.c 59% -> 64%).

Merged scoped coverage: lines 67.3%->68.3%, functions 87.5%->88.4%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:44:12 +10:00
Andrew Tridgell
2928b2742e build: scope gcov report to rsync's own source; add coverage-all
The coverage report counted bundled third-party code (zlib/, popt/, and the
PostgreSQL/ISC lib/ imports getaddrinfo/getpass/inet_ntop/inet_pton) that rsync
ships but does not own, muddying the percentages. Add a COVERAGE_EXCLUDE gcovr
filter (shared by all coverage targets) so the report reflects rsync's own code:
on the same data, lines 63.9%->65.5%, functions 81.4%->85.0%, branches
55.0%->56.5% (rsync's own md5/mdfour/wildmatch/etc. stay in the report).

Add 'make coverage-all': run the suite under pipe + --protocol=30 + --protocol=29
+ --use-tcp, accumulating into the shared .gcda (not cleared between runs), then
one merged scoped report -- covers the daemon/TCP and protocol-compat paths a
single pipe run misses (lines 67.6%, functions 87.6%, branches 58.6%). Also add
'make coverage-fallback' for a separate --disable-openat2 build (different .gcno,
so it can't merge with the openat2 report). CI is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:44:12 +10:00
Andrew Tridgell
f1d5a3c815 ci: declare new metadata-coverage test skips for macOS and Cygwin
acls-depth skips where ACLs/setfacl are unavailable (macOS, Cygwin) like the
existing acls tests, and sparse skips on APFS (macOS), where a seek-written
hole isn't allocated sparsely. Add them to the per-platform RSYNC_EXPECT_SKIPPED
lists so the skip-set assertion stays accurate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
3086dbc0fd ci: add an Ubuntu gcov coverage job
Builds with --enable-coverage and runs the suite under both transports
(make coverage, then make coverage-tcp). gcovr's line/branch/decision totals
are printed to the step log and also written to the GitHub step summary, so the
coverage numbers are visible directly in the CI output; the HTML reports are
uploaded as an artifact. make coverage exits with the suite's status, so a test
regression fails the job.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
63e599b921 build: add 'make coverage-tcp' and drop deprecated gcovr --branches
coverage-tcp reuses the coverage recipe with --use-tcp (daemon tests over a real
loopback rsyncd, which also runs the require_tcp-only tests) and a separate
report directory, via COVERAGE_RUNFLAGS / COVERAGE_DIR. Verified end to end:
pipe run reports 63.9% lines, the TCP run 64.5% (it exercises more code).

Also drop gcovr's --branches flag: it is deprecated in gcovr 8 and branch +
decision coverage still appear in --print-summary and the HTML without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
340238421d testsuite: assert absolute --partial-dir delta resume now works
partial_test.py sub-test 5 deterministically asserts a delta (--no-whole-file)
resume from an absolute, outside-tree --partial-dir reproduces the source and
consumes the basis -- the regression guard for the receiver fix. Sub-test 4
keeps asserting the cross-directory partial WRITE on interrupt. Drop the
--whole-file workaround and the 'broken on master' notes in the docstring and
COVERAGE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
31fbb17d23 receiver: fix absolute --partial-dir delta resume (false verification)
A delta (--no-whole-file) resume whose basis is an absolute --partial-dir
looped forever on exit code 23 ("failed verification -- update put into
partial-dir"), stranding the correct data in the partial-dir and never
populating the destination.

Cause: an absolute --partial-dir makes the basis path absolute, but the
receiver opened it with secure_relative_open(NULL, fnamecmp, ...), which by
design rejects an absolute relpath (EINVAL). The basis fd was then -1, so
receive_data() mapped no basis and (because the matched-block sum_update() is
guarded by "if (mapbuf)") computed the whole-file verification checksum over
the literal data only -> a spurious mismatch every run. (The data itself was
correct, since the in-place update leaves the matched basis bytes in place.)
Under a non-chroot daemon the in-place write went through the same call and
failed outright.

Fix: add secure_basis_open(), which treats an operator-trusted absolute basis
path as (trusted directory + confined leaf) -- the same way secure_relative_open
already trusts an absolute basedir while keeping O_NOFOLLOW on the leaf -- and
use it for both the basis read and the inplace-partial write. The strict
"reject absolute relpath" contract of secure_relative_open is left intact.

Defense-in-depth: receive_data() now treats a block-match token with no mapped
basis as a protocol inconsistency (it can only arise from a basis that the
generator opened but the receiver could not), failing cleanly instead of
silently dropping those bytes from the verify checksum or the output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
edf298ace5 testsuite: add COVERAGE.md matrix and -u/--force coverage
COVERAGE.md is the living checklist mapping every CLI option (~142) and daemon
parameter (~54) to its test(s), with depth / cross-dir status and remaining
gaps, so the path-resolution restructure can see exactly what is guarded.

update_test.py closes two of the documented gaps: -u/--update (keep a newer
destination, update an older one) and --force (replace a non-empty destination
directory with a file), both at depth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
1d5b5ab83a build: add gcov coverage and --disable-openat2 knobs for the test suite
Two test-coverage build knobs (both behaviour-neutral by default):

  --enable-coverage  appends '--coverage -fprofile-update=atomic -O0' and adds
                     a 'make coverage' target (whole suite, run serially, then
                     gcovr HTML with branch + decision coverage). rsync forks
                     and its children exit without running the gcov atexit
                     flush -- the generator via its SIGUSR1 handler
                     (_exit_cleanup) and the receiver via the SIGUSR2 handler
                     -- so under GCOV_COVERAGE we call __gcov_dump() at both, or
                     receiver.c/generator.c record no coverage at all.

  --disable-openat2  gates the Linux openat2(RESOLVE_BENEATH) sites in syscall.c
                     on HAVE_OPENAT2 (defined by default), so disabling it forces
                     the portable per-component O_NOFOLLOW resolver to run as the
                     primary on ordinary Linux -- exercising and
                     coverage-counting that fallback tier without a pre-5.6
                     kernel. NOTE: coordinate with the parallel syscall.c
                     path-resolution restructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
b0ba699031 testsuite: probe RESOLVE_BENEATH support functionally for the #715 test
Add resolve_beneath_supported() to rsyncfns: it functionally probes whether the
rsync binary can follow an in-tree directory symlink under its secure resolver
(an initial transfer plus a delta update through a dir-symlink, the operation
issue #715 is about). This tracks the actual binary instead of a platform name.

Use it in symlink-dirlink-basis_test.py in place of the SunOS/OpenBSD/NetBSD/
Cygwin name check: it skips on those platforms too, and additionally on
Linux < 5.6, a seccomp-blocked openat2, and the new --disable-openat2 build,
where the portable O_NOFOLLOW fallback rejects the in-tree symlink.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
e57c7f5d87 testsuite: output, comparison and algorithm-selection option coverage
Breadth pass for options not yet exercised:

  output-options    output shape of --version/--help/-i/-n/--stats/
                    --out-format/--list-only/-q/--progress/-h/-8 (these control
                    output, not path handling, so they're checked for shape).
  compare           -c and -I catch a stealth change (same size+mtime, new
                    content) deep in the tree; --size-only skips a same-size
                    change; --modify-window absorbs a 1s mtime difference.
  compress-options  --compress-choice for every advertised compressor,
                    --compress-level, --skip-compress, --checksum-choice for
                    every advertised checksum, and --checksum-seed -- each a
                    clean byte-identical transfer at depth.

Green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
05f30c05c9 testsuite: daemon parameter coverage (loopback)
Drive a loopback daemon (secure stdio-pipe transport by default, also green
under --use-tcp) via the new write_daemon_conf helper and assert the behaviour
of the security-relevant rsyncd.conf parameters, transferring >=3-deep trees:

  daemon-access  path / read only / write only / list, incl. a deep sub-path
                 pull and that a list=no module is hidden yet usable by name.
  daemon-filter  daemon exclude hides matching files everywhere; incoming /
                 outgoing chmod rewrite modes of every transferred file.
  daemon-auth    auth users + secrets file accept the right password, reject a
                 wrong one and an unauthenticated request; strict modes rejects
                 a world-readable secrets file.
  daemon-exec    pre-/post-xfer exec run with RSYNC_MODULE_NAME /
                 RSYNC_EXIT_STATUS; a failing pre-xfer exec aborts the transfer
                 (marker files polled for, since post-xfer exec runs after the
                 client disconnects under TCP).
  daemon-munge   munge symlinks stores incoming links with the /rsyncd-munged/
                 prefix and strips it on the way out.
  daemon-refuse  refuse options: a named option, a wildcard, and the '* !a !v'
                 allow-list idiom.

Green on master under pipe and --use-tcp transports and under --protocol=29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
922681e140 testsuite: filtering coverage at depth
Assert exactly which entries are/aren't transferred, deep in the tree:

  filter-depth      --exclude/--include precedence on files at every level, and
                    a -F per-directory .rsync-filter loaded from a deep dir that
                    applies to that subtree only (not above it).
  cvs-exclude       -C built-in cruft patterns (*.o, *~) at every level plus a
                    deep per-directory .cvsignore scoped to its subtree.
  size-filter       --max-size / --min-size select the right files all the way
                    down.
  files-from-depth  --files-from selects only the listed deep paths (implied
                    parents created); --from0 NUL-delimited; --exclude-from /
                    --include-from filter at depth.

(--existing / --ignore-existing are covered in delete-deep_test.py.)
Green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
273b9f265f testsuite: metadata preservation coverage at depth
Set each attribute distinctively on a file AND a directory at every level of a
>=3-deep tree and verify it per entry after transfer (metadata is applied as a
single-component op on an entry whose parent chain the resolver restructure
rewrites):

  metadata-depth   -p preserves exact file/dir modes; -t preserves file
                   mtimes; --chmod=D710,F600 rewrites them.
  omit-times       -O omits directory times (files still preserved); -J omits
                   symlink times.
  sparse           -S preserves a deep file's hole (allocated << size);
                   --no-sparse fills it.
  xattrs-depth     -X reproduces a user xattr on every entry (gated on xattr
                   support).
  acls-depth       -A reproduces a POSIX ACL on every entry (gated on ACL
                   support + setfacl/getfacl).
  ownership-depth  --groupmap and --chown=:GROUP remap the group of every
                   entry (non-root, to a secondary group); -o/--usermap gated
                   on root.

All green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
0d546ee3b4 testsuite: structure / recursion / link coverage at depth
Cover the structure and link options at >=3 levels and across directories,
asserting each option's specific effect:

  links            -l keeps a symlink, -L dereferences it, -k follows a
                   directory symlink -- all on a symlink several levels deep.
  dirs             -d copies the top layer (file + empty dir) without recursing.
  prune-empty-dirs -m drops empty chains and chains emptied by an exclude,
                   keeps populated ones.
  hardlinks-deep   -H preserves a hard link whose names live in different
                   directories at depth; without -H they become separate inodes.
  delete-deep      --delete removes a deep extraneous file/subtree; the four
                   delete-timing variants agree; --max-delete caps deletions;
                   --existing / --ignore-existing select/skip correctly.
  relative-implied -R mirrors an implied directory's mode at depth;
                   --no-implied-dirs does not (proto 30+).

Green on master and under --protocol=29/30 (the --no-implied-dirs sub-case is
gated to protocol >= 30, where multi-component sender paths are accepted).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
d6124a82a4 testsuite: cross-directory/temp/backup/dest coverage at depth
Fill the highest-restructure-risk gap: options that do two-directory / rename /
outside-tree work, asserted at >=3 levels deep with the aux tree kept outside
the main tree, and asserting the option's specific property rather than just
tree equality (which the ported tests already cover).

  alt-dest-deep  --link-dest hardlinks unchanged files (same inode), --copy-dest
                 copies (never links), --compare-dest omits unchanged files;
                 ref tree outside both src and dest.
  temp-dir       cross-dir temp->final rename at depth; temp dir left clean; a
                 missing --temp-dir fails (so the option is proven consulted).
  partial        --partial keeps the partial in the dest file; relative
                 --partial-dir stages per-directory at depth (pre-seed +
                 interrupt/resume); absolute --partial-dir writes the partial
                 outside the tree.
  inplace        --inplace keeps the destination inode across a delta update;
                 the default temp+rename path replaces it.
  append         --append completes truncated files tail-only; --append-verify
                 repairs a corrupted prefix (protocol >= 30).
  backup-deep    --suffix saves <name>S beside the new file; --backup-dir
                 relocates old files to a parallel deep tree outside the dest
                 and captures deletions under --delete.

All green on master and under --protocol=29/30.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
1d828f35ca testsuite: add depth/cross-dir/daemon coverage helpers to rsyncfns.py
Add helpers for the option-coverage expansion (the path-handling restructure
changes parent-component resolution, so options must be exercised at depth and
across directory boundaries):

  * make_tree() builds a tree with a regular file at every level so a property
    can be checked at the tree root and >=3 levels deep;
  * walk_files()/walk_dirs() iterate entries for per-level assertions;
  * assert_same/assert_mode/assert_mtime_close/assert_is_symlink/
    assert_hardlinked/assert_not_hardlinked/assert_exists/assert_not_exists
    assert the concrete property an option controls (not just dest == src);
  * write_daemon_conf() writes an arbitrary rsyncd.conf (globals + modules)
    for daemon-parameter tests, beyond build_rsyncd_conf's fixed four modules;
  * forced_protocol() lets protocol-sensitive tests gate sub-cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:31:52 +10:00
Andrew Tridgell
7bba25e675 start on 3.5.0 2026-05-23 07:52:55 +10:00
Andrew Tridgell
6e3140d5ba testsuite: read xattrs natively instead of shelling out to getfattr
xattr_set() sets attributes with the native os.setxattr(), but
xattr_dump() read them back by running "getfattr -d". That asymmetry
breaks "make check" on any system where rsync is built with xattr
support (libattr headers present) but the attr package's CLI tools are
not installed -- common on Android/Termux and minimal CI images: setting
succeeds via os.setxattr, then xattr_dump's getfattr raises
FileNotFoundError, which crashes the test (reported FAIL) instead of
running or skipping it. That's why "make check" was failing here on
xattrs / xattrs-hlink.

Read the xattrs natively with os.listxattr()/os.getxattr() on Linux,
symmetric with xattr_set(), so the suite needs no external getfattr; the
output still mimics "getfattr -d" and only has to be self-consistent
between the source and destination dumps. Cygwin keeps the CLI path
(Python there lacks os.*xattr). make check now passes with no attr
package installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:15:22 +10:00
Andrew Tridgell
1d8f47cc71 testsuite: generate predictable fixture files instead of reading /etc, /bin, /
The Python rewrite of the suite carried over the shell habit of
populating the test tree by capturing "ls -l /etc" / "ls -l /bin"
(falling back to "ls /"): hands_setup() built etc-ltr-list / bin-lt-list
that way, and longdir_test.py did the same for its leaf files. That ties
the fixtures to the host filesystem layout -- those directories are
absent or unreadable on Android/Termux and other minimal environments,
where "ls /" fails outright -- and the captured content was never
reproducible from run to run.

Add a deterministic make_text_file() helper to rsyncfns.py and use it for
hands_setup()'s two fixture files and longdir's leaf files. The names
etc-ltr-list / bin-lt-list are unchanged (chmod, chmod-temp-dir and
alt-dest reference them by name); only the content source changes, so the
fixtures are now self-contained and identical on every platform. This
also drops longdir_test.py's date(1) and ls(1) subprocess calls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:15:22 +10:00
Andrew Tridgell
743d715d43 docs: add rsync Discord server link
Add a link to the rsync Discord server (https://discord.gg/Avfvy9zhdp)
below the mailing lists section in README.md and on the lists.html web
page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 15:06:21 +10:00
Andrew Tridgell
4b862306e5 testsuite: restore non-Linux xattr/fake-super coverage
The Python rewrite had gated the xattr / fake-super tests (xattrs,
xattrs-hlink, chown-fake, devices-fake) to Linux because it used the
Linux-only os.*xattr. Restore them on macOS, FreeBSD, Cygwin and Solaris
via a per-OS xattr surface in rsyncfns.py (xattrs_supported / xattr_set /
xattr_dump):
  * Linux   -- os.*xattr
  * macOS   -- xattr
  * FreeBSD -- setextattr / lsextattr / getextattr
  * Cygwin  -- getfattr / setfattr (from the `attr` package; CPython on
               Cygwin has no os.*xattr)
  * Solaris -- runat(1), with the script on stdin and the attr name/value
               passed via the environment (the runat -c form mangles args)

Test attribute names are logical; the "user." namespace prefix is added
only on the Linux-style platforms (Linux, Cygwin). RSYNC_PREFIX/RUSR vary
per OS (macOS and Solaris use rsync.nonuser to avoid rsync's reserved
rsync.* space). The macOS and Cygwin workflows no longer skip these tests;
the FreeBSD/Solaris jobs use IGNORE skip-checking so need no change.

Verified on real Linux, macOS, FreeBSD, Cygwin and Solaris hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
70948a9dc3 testsuite: post-review fixes and lock-file hardening
* chmod-option: pin umask to the suite-wide 022 baseline (mirroring the
    old rsync.fns) so rsync's --chmod `D+w` is computed and applied under
    the same umask -- fixes failures under a different ambient umask (077).
  * daemon module-list test: assert the `list = no` module does NOT leak
    into the listing (the substring check alone missed regressions).
  * claim_ports() lock file: open with O_NOFOLLOW and only fchmod a file we
    O_EXCL-created, rejecting a symlink OR hard link planted at the
    well-known /tmp path -- which, with the TCP tests running under sudo in
    CI, could otherwise chmod an arbitrary root-owned target. Require a
    pristine (regular, nlink==1) file.
  * CI: extend the Linux/Cygwin expected-skip lists for the gated tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
951bf0a446 socket: enforce socketpair_tcp()'s anti-hijack guarantee
socketpair_tcp() fakes a connected socket pair via a loopback TCP
self-connect (socket -> bind 127.0.0.1:0 -> listen -> connect ->
accept), used by sock_exec() for RSYNC_CONNECT_PROG. Its comment has
long promised that "nobody else can attach to the socket, or if they
do that this function fails", but nothing actually verified it: the
code accept()ed whatever connection arrived first without checking it
was the one our own connect() made.

Between listen() and accept() the ephemeral loopback port is
connectable by any local user. With backlog 1 a same-host attacker who
races a connection in before our connect() lands could have their
socket returned by accept(), handing them one end of the rsync
protocol stream. The exposure is small (loopback only, random
ephemeral port, sub-millisecond window, local users only), but the
promised guarantee was simply not enforced.

Enforce it: after the connection is established, require that the peer
address of the accepted end (fd[0]) equals the local address of our
connecting end (fd[1]), and that both are 127.0.0.1. A hijacked
connection has a different source port and is rejected (errno EPERM,
fail closed). The legitimate self-connect always matches, so there is
no behaviour change for the normal path.

Verified: rebuilds clean with -Wall -W; the full testsuite still
passes in both transports (pipe `make check` 57/3, `runtests.py
--use-tcp` 59/1) -- the pipe transport exercises this code path on
every daemon test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
bea8a3a16f testsuite: secure stdio-pipe daemon transport by default, opt-in TCP
Daemon-mode tests default to the stdio-pipe transport (RSYNC_CONNECT_PROG),
which opens no listening socket -- so `make check` never exposes a network
service. Real TCP is opt-in via `runtests.py --use-tcp`, with the daemon
bound to loopback (127.0.0.1) on a claim_ports()-reserved port; CI runs the
suite both ways.

start_test_daemon() is the single seam every daemon test uses: the secure
pipe by default, a real rsyncd on a claimed loopback port under --use-tcp.
Tests with no pipe equivalent (the fake-proxy listener and the reverse-DNS
hostname-ACL daemon test) are gated behind require_tcp().

`make check` also now runs the suite in parallel by default (CHECK_J=8);
the claim_ports() byte-range locks make that safe across concurrent runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
bf8aab51e8 testsuite: add claim_ports() for parallel-safe TCP-port coordination
rsyncfns.claim_ports(*ports) takes exclusive POSIX byte-range locks on
/tmp/rsync_test.lck (offset = port number) so any number of test
processes can run concurrently without colliding on a TCP port: a test
asking for a port already held blocks until the holder exits. The
kernel drops the locks automatically when the holding process dies, so
a crashed test releases its ports with no manual cleanup.

Ports are claimed in sorted order so two callers requesting the same
set in different orders can't deadlock. The lock file is forced to
mode 0o666 after creation (the umask would otherwise trim it and lock
out a second user on a shared CI runner; EPERM when we're not the
owner is fine).

proxy-response-line-too-long is the first user: it switches from an
ephemeral port to a claimed fixed port (12873).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
1f689ec0c2 testsuite: rewrite the shell testsuite in Python
Replace the entire shell-based testsuite with Python. runtests.py
already drove the suite (it had replaced runtests.sh earlier); this
converts all 60 test scripts from *.test shell to *_test.py and adds
testsuite/rsyncfns.py as the shared helper module -- the Python
counterpart of the now-removed rsync.fns.

runtests.py:
  * Discovers and runs both *.test and *_test.py; dispatches the
    Python tests via the same python3 that runs the harness.
  * Extends PYTHONPATH so tests can `import rsyncfns`.

testsuite/rsyncfns.py provides everything the ports need:
  * environment wiring (scratchdir / srcdir / TOOLDIR / RSYNC /
    TLS_ARGS, and HOME pointed at the per-test scratch dir);
  * result reporting -- test_fail / test_skipped / test_xfail mapping
    to the 0 / 1 / 77 / 78 exit-code convention;
  * the transfer-and-verify helpers checkit, checkdiff, verify_dirs,
    rsync_ls_lR, check_perms and the v_filt output filter;
  * fixture builders hands_setup, build_symlinks, build_rsyncd_conf,
    make_data_file, cp_p / cp_touch, makepath / rmtree.

All 60 tests are converted, including the four split-variant tests
that share one source via a Makefile-built symlink (chown/chown-fake,
devices/devices-fake, xattrs/xattrs-hlink, exclude/exclude-lsh);
Makefile.in's CHECK_SYMLINKS now points at the *_test.py names.

The dead rsync.fns shell library is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:34:52 +10:00
Andrew Tridgell
8839314025 ci: add static Android NDK build workflow
Cross-compiles statically-linked rsync binaries with the Android NDK for
arm64-v8a (all modern phones) and armeabi-v7a (older 32-bit devices), and
uploads them as workflow artifacts for adb push / Termux use.

The build is self-contained (optional external libraries disabled; keeps
md5/md4 and the bundled zlib) and forces a few configure cache values
that can't be probed when cross-compiling: lchmod()/lutimes() off (Bionic
doesn't declare them until API 36 though the symbols link), and
socketpair / mknod-FIFO / mknod-socket on (Android runs a Linux kernel,
so these match the native result). IPv6 is enabled explicitly.

Since the binaries are cross-compiled the test suite can't run; the job
instead asserts each binary is static and the correct architecture, and
smoke-tests `--version` under qemu-user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:09:47 +10:00
Andrew Tridgell
47e087d8eb testsuite: portable make_data_file helper; drop hard /dev/urandom dependency
symlink-dirlink-basis.test and chdir-symlink-race.test both
require a multi-kilobyte non-trivial-content source file for the
rsync delta algorithm to exercise.  Both used dd / head against
/dev/urandom directly, which fails on platforms that don't ship
/dev/urandom (e.g. HPE NonStop).  The dd error gets swallowed by
'2>/dev/null' and the test then fails with a misleading 'failed
to create test file' that hides the real cause.

Add make_data_file <path> <size> to testsuite/rsync.fns.  Prefers
/dev/urandom when readable (kernel-provided randomness, fast),
falling back to a deterministic awk LCG seeded from PID and a
POSIX cksum of the destination path.  Output is constrained to
printable ASCII (33..126) so the helper survives two awk-portability
quirks:

  - printf '%c', 0 terminates the string in some awks, emitting
    fewer than sz bytes;
  - gawk in UTF-8 locales encodes printf '%c', N for N > 127 as
    a 2-byte UTF-8 sequence, emitting more than sz bytes.

The tests don't need 8-bit binary entropy -- they just need
non-trivial bytes for rsync's block-matching algorithm.

Update both call sites to use the helper.  Linux/FreeBSD/macOS
still take the /dev/urandom fast path; NonStop and any other
platform missing the device get the awk fallback transparently.
Both paths verified locally with the symlink-dirlink-basis test.
2026-05-21 07:40:30 +10:00
Andrew Tridgell
e1c5f0e93a t_chmod_secure: probe kernel RESOLVE_BENEATH at runtime; drop test skip
The chmod-symlink-race test was previously a no-op on Solaris,
OpenBSD, NetBSD, and Cygwin via a case 'uname -s' skip.  The skip
was too broad: of the four scenarios the helper exercises, only
the 'legitimate within-tree dir-symlink' one actually needs
RESOLVE_BENEATH-equivalent kernel support.  The other three
(attack rejection, plain relative path, top-level file) behave
identically on the per-component O_NOFOLLOW fallback and would
have caught the t_stub.c max_alloc=0 bug fixed in the previous
commit if the test had been allowed to run.

Make the helper probe the running kernel for either
openat2(RESOLVE_BENEATH) on Linux 5.6+ or openat(O_RESOLVE_BENEATH)
on FreeBSD 13+ / macOS 15+ by opening '.' under the requested
confinement.  Honour the result:

  - If RESOLVE_BENEATH-equivalent confinement is available, the
    within-tree symlink scenario must succeed (status quo).
  - If not, the per-component O_NOFOLLOW fallback rejects every
    symlink including legitimate ones; expect the within-tree
    symlink scenario to be rejected (rc != 0) and the file mode
    to remain unchanged.

The attack-rejection, plain-path and top-level scenarios are
unchanged: they expect the same outcome on both code paths.

Drop the case-based skip from chmod-symlink-race.test so the test
runs everywhere and the per-component fallback gets the CI
coverage that the SunOS/OpenBSD/NetBSD/Cygwin runners can
provide.  HPE NonStop -- which lacks RESOLVE_BENEATH but isn't in
the existing skip list -- is also covered by this change.
2026-05-21 07:40:30 +10:00
Andrew Tridgell
cfdc27c613 t_stub.c: raise max_alloc default so test helpers can allocate
The t_stub.c shim defined max_alloc = 0 as a placeholder to satisfy
the link against util2.o.  This was harmless when the test helpers
made no allocations, but the secure_relative_open() implementation
in 3.4.0+ calls my_strdup() in its per-component O_NOFOLLOW
fallback (syscall.c around line 1857), and the 3.4.3 do_*_at()
hardening series added more such calls.  With max_alloc=0, every
allocation in that path trips the 'exceeded --max-alloc=0' check in
util2.c's my_alloc(), and t_chmod_secure (which exercises
do_chmod_at via secure_relative_open) fails on the very first
my_strdup.

The failure is invisible on Linux 5.6+ / FreeBSD 13+ / macOS 15+ /
recent Cygwin because those platforms take the kernel-enforced
openat2(RESOLVE_BENEATH) or openat(O_RESOLVE_BENEATH) branch and
never reach the per-component fallback.  It also goes unobserved
on the SunOS/OpenBSD/NetBSD/CYGWIN* CI runners because the
chmod-symlink-race.test script case-skips on those platforms (the
legitimate dir-symlink scenario the test exercises can't pass on
the per-component fallback).  HPE NonStop is the first platform
that lacks RESOLVE_BENEATH support AND isn't in the skip list AND
has someone actually running the test suite, so it surfaced the
latent bug.

Raise max_alloc to SIZE_MAX so the helpers can allocate freely.
A follow-up patch makes t_chmod_secure adapt at runtime so the
skip list can be removed and the per-component fallback gets real
CI coverage.
2026-05-21 07:40:30 +10:00
Andrew Tridgell
7e7372a0c5 packaging: add ftp.filt, the FTP mirror filter file
The .filt file in /home/ftp/pub/rsync on samba.org controls which
subtrees release.py's FTP mirror excludes (currently /binaries/
and /generated-files/).  Without it, step-10-push-ftp's
'rsync --del' would propagate local deletions to the server even
for those archive subtrees.

Until now the only copy of this two-line file lived on the server.
Bundle it in source at packaging/ftp.filt so it survives a disaster
on samba.org, and have step_1_fetch seed FTP_DIR/.filt from the
bundled copy on every run (with --exclude=/.filt on the rsync pull,
so the server's copy can't silently drift the bundled one).
step-10-push-ftp then propagates any in-source updates to the
filter back to the server.
2026-05-20 15:36:44 +10:00
Andrew Tridgell
8cad2097e9 packaging: remove obsolete samba-rsync and send-news scripts
Both scripts were pre-release.py legacy helpers:

  * samba-rsync rsync'd ~/samba-rsync-{ftp,html}/ to the samba.org
    server.  release.py step-10-push-ftp and step-11-push-html now
    do exactly this, using ../release/rsync-{ftp,html}/ as the
    local mirrors.

  * send-news copied README/INSTALL/NEWS .md + .html files into
    ~/samba-rsync-ftp/ and rsync'd them to samba.org.
    release.py step-8-update-ftp already does this
    (./md-convert --dest=FTP_DIR README.md NEWS.md INSTALL.md and
    the surrounding rsync of html files into FTP_DIR), and
    step-10-push-ftp pushes the result.

Update the trailing instructions printed at the end of
step-12-push-git to drop the now-obsolete 'run packaging/send-news'
suggestion, and tighten the comment in step_1_fetch that referred
to samba-rsync as a current sibling tool.
2026-05-20 15:36:44 +10:00
Andrew Tridgell
d039cfa829 packaging/release.py: rsync-web is now an in-tree subdirectory
Track the move of rsync-web from sibling git checkout to a regular
subdirectory of the rsync source tree:

  * HTML_SRC: '../rsync-web' -> 'rsync-web'.
  * step_1_fetch: drop the .git-presence probe and the 'make sure
    it's up to date' reminder.  Both made sense when rsync-web was
    a separate repo the maintainer had to clone and pull, but the
    directory is now part of the same checkout as this script.
  * rsync invocation no longer needs --exclude=/.git: there is no
    .git inside rsync-web/ (it is just a subdir of the parent
    rsync-git checkout).
  * Header comment block and step-1 help text rewritten to describe
    the new layout.
2026-05-20 15:36:44 +10:00
Andrew Tridgell
0af88421dc import rsync-web website content as a subdirectory
Fold the standalone rsync-web repo into the rsync source tree as
rsync-web/, eliminating the sibling-checkout convention and the
drift it causes between the release-time HTML snapshot in
../release/rsync-html and the source of truth in ../rsync-web.

Flat-copy import (no git history merge).  The standalone repo at
github.com/RsyncProject/rsync-web is retained for historical
reference and will be archived once the in-tree copy proves itself.

Add /rsync-web/ to .gitattributes with export-ignore so the
website content does not bloat the release source tarball
produced by 'git archive' in packaging/release.py step_7_tarball.

A follow-up commit repoints HTML_SRC in packaging/release.py at
the new in-tree location.
2026-05-20 15:36:44 +10:00
Andrew Tridgell
9d014670df INSTALL.md: point Ubuntu users at the ppa:rsyncproject/rsync PPA
Most Ubuntu users landing on INSTALL.md want to install rsync, not
build it.  Add a short section near the top that offers the
Launchpad PPA as the one-line path for the four currently supported
series (jammy 22.04 LTS, noble 24.04 LTS, questing 25.10,
resolute 26.04 LTS), and clarify that the rest of the file is about
building from source.
2026-05-20 15:36:44 +10:00
Andrew Tridgell
647a00a278 start on 3.4.4 2026-05-20 11:50:33 +10:00
Andrew Tridgell
2c7777aaa6 Preparing for release of 3.4.3 [buildall] v3.4.3 2026-05-20 10:07:26 +10:00
Andrew Tridgell
6af41d2357 version.h: bump to 3.4.3 for the release
Drops the "dev" suffix on RSYNC_VERSION ahead of the
2026-05-20 00:00 UTC public release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew Tridgell
a0b9a8e989 NEWS: prepare 3.4.3 release entry with six CVEs
Set the date to 20 May 2026, add a SECURITY FIXES section listing
all six May 2026 CVEs (CVE-2026-29518, -43617, -43618, -43619,
-43620, -45232) with reach, root cause, fix and reporter for each,
plus a note on the defence-in-depth hardening that goes with them.
Also list the new symlink-race regression tests under DEVELOPER
RELATED.
2026-05-20 10:01:22 +10:00
Andrew Tridgell
ac692b199c util1: handle out-of-range times in timestring 2026-05-20 10:01:22 +10:00
Andrew Tridgell
147e9bea8c main: reject hyphen-prefixed remote-shell hostnames 2026-05-20 10:01:22 +10:00
Andrew Tridgell
a5fc5ebe7a socket: reject over-long proxy response line
fixes a one byte stack overflow when using RSYNC_PROXY with a
malicious proxy.

Reach: only when RSYNC_PROXY is set and a malicious or MITM'd
proxy returns the pathological response.  The byte written is
always '\0' and the attacker doesn't choose the offset, so impact
is corruption of one adjacent stack byte and possible later
misbehaviour or crash -- no information disclosure beyond the
existing rprintf of buffer contents.

Reported by Aisle Research via Michal Ruprich
2026-05-20 10:01:22 +10:00
Andrew Tridgell
c79cb81a4f rsync.h: lower MAX_WIRE_DEL_STAT to avoid signed-int overflow in read_del_stats
read_del_stats() in main.c accumulates 5 wire-supplied counts into
the int32 stats.deleted_files field:

    stats.deleted_files  = read_varint_bounded(..., MAX_WIRE_DEL_STAT, ...);
    stats.deleted_files += stats.deleted_dirs     = ...;
    stats.deleted_files += stats.deleted_symlinks = ...;
    stats.deleted_files += stats.deleted_devices  = ...;
    stats.deleted_files += stats.deleted_specials = ...;

With the previous MAX_WIRE_DEL_STAT = 2^30 (1.07 GB) the worst-case
sum is 5 * 2^30 = 5.37 GB; three maximal values already exceed
INT32_MAX = 2.15 GB on the third "+=", triggering signed integer
overflow (C99 6.5/5 -- undefined behaviour, the compiler may assume
it cannot happen and elide subsequent checks).

The bound was introduced in f0155902 ("defence-in-depth: bound
wire-supplied counts and lengths") with a commit message claiming
"per-summand cap so the total can't overflow", but 2^30 * 5 does
overflow.  Lower the per-summand cap to 2^28 (= 268M) so the worst
case is 5 * 2^28 = 1.34 GB < INT32_MAX with margin.  2^28 deletions
per category is still vastly above any plausible real transfer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew Tridgell
650643109e defence-in-depth: receiver block-index bounds + read_delay_line null check
Two assorted audit findings:

  - receive_data() never bounds-checked the block index returned
    by recv_token() against sum.count before computing offset2
    and feeding it to map_ptr(). An out-of-bounds index from a
    hostile sender produces invalid memory access. Add a
    sum.count bounds check.

  - read_delay_line()'s strchr() call could return NULL when no
    space was found, but the code unconditionally added 1 to the
    result before dereferencing. Low impact (just a disconnect on
    exit of the client-specific forked process) but the NULL
    deref is real. Guard the NULL.

Both reported by Joshua Rogers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew Tridgell
4cf08983e8 defence-in-depth: guard cumulative snprintf against length underflow
Two cumulative-snprintf patterns in log.c (rsyserr) and main.c
(output_itemized_counts) had the shape

    len = snprintf(buf, sizeof buf, ...);
    len += snprintf(buf+len, sizeof buf - len, ...);

with no guard between calls. snprintf returns the would-have-been
length on truncation, so a truncated first call leaves
"sizeof buf - len" as a negative-then-promoted-to-size_t value,
underflowing into a huge size_t and writing past buf.

Realistic exposure is small in both cases (log header well under
buffer, only ~5 itemized iterations writing ~25 chars each into a
1024-byte buffer) but the defect class matches bb0a8118 and the
fix is cheap. Guard before each subsequent call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew Tridgell
8112445318 defence-in-depth: bound wire-supplied counts and lengths
Multiple receiver-side fields read from the wire were trusted
without upper-bound checks. A hostile peer could either request
extreme allocations (DoS via --max-alloc) or, on platforms where
read_varint returned a negative value, push ~SIZE_MAX through the
size_t conversion to wrap downstream length checks.

Introduce read_int_bounded(), read_varint_bounded() and
read_varint_size() in io.c so wire-derived integer ranges are
checked at the read site rather than scattered across each
caller, with RERR_PROTOCOL on out-of-range input.

Apply the bounded primitives to:
  - sum->count (checksum count -- previously could overflow
    (size_t)count * xfer_sum_len on 32-bit with raised max-alloc)
  - xattrs: count, name_len, datum_len, plus rel_pos overflow
    detect to stop chain wrapping the num accumulator
  - acls: ida-entry count
  - flist: file mode S_IFMT validation, modtime_nsec range check
  - delete-stat counters in main: per-summand cap so the total
    can't overflow a signed 32-bit accumulator

Reporters include Joshua Rogers (checksum-count overflow finding).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00