Compare commits

...

23 Commits

Author SHA1 Message Date
Andrew Tridgell
f26f747b80 Preparing for release of 3.4.4 [buildall] 2026-06-08 13:53:52 +10:00
Andrew Tridgell
ed2950f867 version.h: bump to 3.4.4 for the release 2026-06-08 13:17:32 +10:00
Andrew Tridgell
37d0080e92 t_stub: give test helpers an unlimited max_alloc
Helpers link util2.o but not options.c, so they used the stub's
max_alloc = 0, which makes every my_alloc()/my_strdup() in util2.c abort
with "exceeded --max-alloc=0". CI didn't catch it because the openat2
path avoids those allocations, but the secure_relative_open() fallback
hits my_strdup() and aborts. Set max_alloc = (size_t)-1, matching the
v34-stable-testsuite fix. Reported by steadytao on PR #980.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
5073e6a575 ci: run the v34-stable-testsuite regression suite against this build
The stable branch keeps the old shell test suite, so the modern Python
suite lives on the v34-stable-testsuite branch. Build rsync here and run
that suite against the built binary (helpers/config.h as tooldir from
this build, test scripts via --srcdir), giving regression coverage for
3.4.x without importing the full master suite.

Runs on ubuntu-latest and ubuntu-22.04 (older-LTS coverage for backports).
Each does a pipe-transport pass (with the same RSYNC_EXPECT_SKIPPED list
the v34-stable-testsuite ubuntu jobs use) and a --use-tcp pass for the
daemon tests the pipe run skips. Addresses review on PR #980.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
bb8d1c14c5 NEWS: add the 3.4.4 release entry
Add the NEWS entry for rsync 3.4.4 (8 June 2026): the backported
regression fixes, the PORTABILITY note documenting the #915 alt-basis
platform limitation, the openat2 autodetect/mknodat fallback build
notes, the stable-testsuite CI addition, and a CREDITS section for the
contributors, reporters, and the PR #980 review.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
517c35e2db ci: also run the build workflows on *-stable release branches
The workflows triggered only on 'master', so PRs targeting a release branch
(e.g. v3.4-stable for 3.4.4) got no CI. Add a '*-stable' branch wildcard to
the push and pull_request filters.
2026-06-08 13:17:32 +10:00
pterror
ee4f668f29 receiver: fix NULL deref on the delta discard path
receive_data() crashed a receiver that was merely DISCARDING a file's
delta stream. discard_receive_data() calls receive_data() with
fname == NULL and fd == -1, so size_r == 0 and mapbuf == NULL. A normal
block-MATCH token (against a block the basis and source share) then
reaches the !mapbuf branch added in 31fbb17d ("receiver: fix absolute
--partial-dir delta resume"), which calls full_fname(fname). full_fname()
dereferences its argument unconditionally (util1.c: `if (*fn == '/')`),
so fname == NULL faults there -> receiver SIGSEGV.

This is a normal-operation crash with a stock cooperating sender, not an
adversarial one. The generator hands the sender real block sums whenever
the basis is readable and we're in delta mode; the receiver only decides
to discard afterwards, when its output cannot be produced -- e.g. the
destination directory is not writable (mkstemp fails), the basis turns
out to be a directory, or a --partial-dir resume is skipped. A MATCH
token arriving during that discard hit the NULL deref.

The 31fbb17d branch is correct only for a REAL output transfer (fd != -1,
fname valid): there, a block match with no mapped basis is a genuine
protocol inconsistency (the generator promised a basis the receiver could
not open), and honoring it would silently omit those bytes from the
verification checksum or leave a hole, so hard-erroring -- and
full_fname(fname) -- is right. It conflated that with the discard path.

The discriminator is fd, not mapbuf: on the discard path fd == -1 always;
on the real-output inconsistency fd != -1. Scope the "no basis file"
protocol error to fd != -1 (where fname is non-NULL and full_fname is
safe) and, on the discard path (fd == -1), absorb the matched bytes
benignly (offset += len; continue) -- symmetric with the literal-token
handling just above, and restoring the pre-31fbb17d behavior. The
real-transfer inconsistency check is preserved unchanged.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
c14e2258b5 build: openat2 autodetect + android probe (R1 #924/#905/#900, R10 #904)
configure now probes for <linux/openat2.h> + SYS_openat2 and defines
HAVE_OPENAT2 only when both are present; syscall.c gates the openat2 include
and the openat2(RESOLVE_BENEATH) tier on HAVE_OPENAT2, so the build no longer
fails on kernels/headers that lack the openat2 header (3.4.3 included it
unconditionally on Linux).  android.c probes openat2 usability behind a SIGSYS
handler so the Android/Termux seccomp sandbox falls back to the portable
resolver instead of killing the process.

Backport combining c73e0063, 83a24c21, the syscall.c guards from 1d5b5ab8, and
4634b0ad; the --disable-openat2/gcov coverage knobs and test changes are omitted.

Thanks to @mmayer (#924), @fda77 (#905), @darkshram (#900) and @ketas (#904) for the reports.
2026-06-08 13:17:32 +10:00
Zen Dodd
499ed5e1ab fix: update skips different file type 2026-06-08 13:17:32 +10:00
Mike-Goutokuji
c7ca5217a7 Always clear st out and validate nanoseconds before using it
Otherwise we get errors.
Fixes: https://github.com/RsyncProject/rsync/issues/927
2026-06-08 13:17:32 +10:00
Andrew Tridgell
20cc824592 main: fix --mkpath + --dry-run file-to-file copy (#880)
A single-file --mkpath copy whose destination parent does not exist
failed under --dry-run: make_path() only *reports* the directories it
would create in a dry run, so change_dir#3 then tried to chdir into a
parent that isn't there and aborted with "change_dir#3 ... failed".

When the parent is genuinely missing in a dry run, skip the chdir and
mark the destination as not-yet-present (dry_run++), exactly as the
multi-file/dir-creation path already does, so the generator doesn't
probe the missing tree.  Gating it on the missing-parent case keeps an
ordinary file-to-file dry run chdir'ing into and itemizing against an
existing destination.

Fixes: #880

Thanks to @pkzc for the report (#880).

Co-authored-by: Stiliyan Tonev (Bark) <stiliyan21@gmail.com>
2026-06-08 13:17:32 +10:00
Zen Dodd
f86309f230 fix: daemon upload delete stats 2026-06-08 13:17:32 +10:00
Andrew Tridgell
ee7c8a5783 token: drain the matched-block insert deflate (#951)
send_deflated_token() adds a matched block to the compressor history with
deflate(Z_INSERT_ONLY).  Our bundled zlib implements Z_INSERT_ONLY (it
produces no output and consumes the input in one call), but a build
against a system zlib lacks it and falls back to Z_SYNC_FLUSH (see the top
of the file), which emits a flush block into obuf.  For a large
incompressible matched token that block exceeds AVAIL_OUT_SIZE(CHUNK_SIZE),
so deflate returned with avail_in != 0 and the transfer aborted:

    "deflate on token returned 0 (N bytes left)"  at token.c

The insert output is never sent -- the receiver rebuilds the matching
history itself in see_deflate_token() -- so loop, resetting the output
buffer, and discard it.  Drain with the same condition as the data loop
above: until the input is consumed AND avail_out != 0.  Stopping at
avail_in == 0 alone can leave pending output in the deflate stream (a
full output buffer with bytes still buffered), which would then be emitted
by the next real deflate send and corrupt the stream.  A bundled-zlib
build still finishes in one iteration.

Thanks to @brabalan for the report (#951).

Fixes: #951
2026-06-08 13:17:32 +10:00
Zen Dodd
b29c149529 fix: install generated manpages out of tree 2026-06-08 13:17:32 +10:00
Andrew Tridgell
7811f2b1b9 daemon: un-backslash escaped option args (#829)
Without --secluded-args, the client's safe_arg() backslash-escapes shell
and wildcard chars in option values before sending them to the server, so
--chown's --usermap=*:user is transmitted as --usermap=\*:user.  Over ssh a
remote shell removes the backslashes before rsync parses the args, but a
daemon has no shell and read_args() stored option args verbatim -- so the
receiver saw the literal "\*", the usermap/groupmap wildcard never matched,
and the module's configured uid/gid won instead.  A regression from the
secluded-args hardening; rsync 3.2.3 (protocol 31) worked.

Un-backslash option args in read_args() on the daemon's first
(non-protected) read, mirroring what the ssh-side shell does.  File args
after the dot are already handled by glob_expand(); the protected (NUL,
already-unescaped) re-read and the server's stdin read pass unescape=0 so
their raw args are left untouched.

Thanks to @elcamlost for the report (#829).

Fixes: #829
2026-06-08 13:17:32 +10:00
Andrew Tridgell
f3757a470a build: fall back to do_mknod() when mknodat() is unavailable (#896)
do_mknod_at() (the symlink-race-safe variant used by a non-chrooted
daemon receiver) calls mknodat()/mkfifoat(), but the at-variant was
gated only on AT_FDCWD.  Older Darwin declares AT_FDCWD without
mknodat(), so the build failed with "mknodat undeclared".

Probe mknodat()/mkfifoat() in configure and require HAVE_MKNODAT for the
at-variant; without it do_mknod_at() falls back to do_mknod(), exactly
as it already does where AT_FDCWD is missing.  Linux keeps the mknodat
path since HAVE_MKNODAT is defined there.

Thanks to @debohman for the report (#896).

Fixes: #896
2026-06-08 13:17:32 +10:00
Andrew Tridgell
6c8295fd62 alloc: revert "zero all new memory from allocations" (#959)
Commit d046525d made my_alloc() calloc every fresh allocation and made
expand_item_list() memset the freshly grown tail, to hand out predictably
zeroed memory.  But that forces the kernel to back pages callers never
touch: each per-directory file_list pre-allocates a FLIST_START-entry
(32768) pointer array -- 256KB -- and calloc now zeroes the whole array
even for an empty directory.  With incremental recursion over many
directories the resident set explodes; 80000 empty dirs went from ~336MB
to ~10.8GB.

Restore the pre-d046525d malloc/calloc split: fresh allocations use
malloc (so untouched tails stay lazy) and only explicit do_calloc
requests (new_array0) are zeroed.  Callers that need zeroed memory
already ask for it, and the full test suite passes.

Thanks to @guilherme-puida for the report (#959).

Fixes: #959
2026-06-08 13:17:32 +10:00
Andrew Tridgell
a8f80f5a12 generator: cap block s2length at the negotiated checksum length
sum_sizes_sqroot() capped the strong-sum length at SUM_LENGTH (16), the
legacy MD4/MD5 digest size.  Since 0902b52f the sum2 array elements are
xfer_sum_len bytes and the sender rejects a sums header whose s2length
exceeds xfer_sum_len.  When the negotiated transfer checksum is shorter
than 16 bytes -- xxh64 (8), used when the build's libxxhash lacks
xxh128/xxh3 (e.g. Ubuntu 20.04) -- the generator still emitted s2length
up to 16, so --append-verify and other full-checksum (redo) transfers
died with "Invalid checksum length 16 [sender]" (protocol incompatibility).

Cap s2length at MIN(SUM_LENGTH, xfer_sum_len): unchanged for any checksum
>= 16 bytes (md5/xxh128/sha1), corrected for short ones.  Also closes a
latent over-read of the xfer_sum_len-sized digest buffer.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
d8847ff7a8 syscall/receiver: honour a relative alt-basis dir on a daemon receiver (#915)
The symlink-race hardening routed the receiver's basis open through
secure_relative_open(), which rejects any '..' -- so a sibling
--link-dest=../01 on a use-chroot=no daemon was silently ignored and every file
re-transferred (#915/#928, a regression from 3.4.1).

Narrow the confinement to the sanitizing daemon (am_daemon && !am_chrooted) and
re-anchor it at the module root, the real trust boundary: secure_relative_open()
prefixes the cwd's module-relative path (from rsync's logical curr_dir[], a
guaranteed lexical prefix of module_dir) and resolves beneath module_dir, so
RESOLVE_BENEATH permits an in-module '..' climb while still rejecting one that
escapes the module.  secure_basis_open() opens with a bare do_open() in the
non-sanitizing cases.  t_stub.c gains weak curr_dir[]/curr_dir_len for the
helpers (via #pragma weak on non-GNU compilers, where rsync.h erases
__attribute__).

Two tests: link-dest-relative-basis asserts the in-module '..' is honoured;
link-dest-module-escape asserts a --link-dest=../../OUTSIDE climb that leaves
the module is refused (not hard-linked to an outside file).  See upstream
PR #930.

Thanks to @fufu65 (#915) and @JetAppsClark (#928) for the reports.
2026-06-08 13:17:32 +10:00
Andrew Tridgell
51c5f05771 sender: open a module-root-absolute path for a path = / module (#897)
A daemon module with path=/ makes F_PATHNAME absolute, so the secure_path built
for the content open starts with '/'.  secure_relative_open() rejects an
absolute relpath with EINVAL, so a use-chroot=no daemon with path=/ could not
send any file ('failed to open ...: Invalid argument (22)') -- a regression
from 3.4.2.  Strip leading slashes to a module-relative path; resolution stays
confined beneath module_dir.

Thanks to @moonlitbugs for the report (#897).
2026-06-08 13:17:32 +10:00
Andrew Tridgell
f68facd22f flist: accept the missing-args mode-0 entry in recv_file_entry (#910)
--delete-missing-args (missing_args==2) sends a missing --files-from arg as a
mode-0 entry (IS_MISSING_FILE), the generator's delete signal.  The mode-type
validation in recv_file_entry() rejected mode 0 as an invalid file type,
aborting the transfer with 'invalid file mode 00 ... code 2' before the
generator could act (a regression from 3.4.1).  Allow mode 0 through only when
missing_args==2 (the delete mode -- not --ignore-missing-args, which never
sends a mode-0 entry); all other modes are still rejected.

Thanks to @mgkeeley for the report (#910).
2026-06-08 13:17:32 +10:00
Andrew Tridgell
9e2e9f3362 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.

Thanks to @sylvain-ilm for the report (#724, #725).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-08 13:17:32 +10:00
Andrew Tridgell
3786926703 build: add check-progs target for fleettest
Build the test-helper programs without running the suite, so an external
harness (fleettest.py) can invoke runtests.py with its own options.
2026-06-08 13:17:32 +10:00
28 changed files with 641 additions and 92 deletions

View File

@@ -8,12 +8,12 @@ name: Test rsync on AlmaLinux 8
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on Cygwin
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on FreeBSD
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on macOS
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on NetBSD
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on OpenBSD
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on Solaris
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'

76
.github/workflows/stable-testsuite.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: Stable testsuite
# Regression coverage for the 3.4.x stable branch. The stable branch keeps the
# old shell test suite, so the modern Python suite is maintained separately on
# the v34-stable-testsuite branch. This job builds rsync from this branch and
# runs that suite against the freshly-built binary (the same "testsuite from one
# branch, code from another" split fleettest uses). Helper programs and
# config.h come from this branch's build (tooldir); the test scripts come from
# the stable-testsuite checkout (--srcdir).
on:
push:
branches: [ v3.4, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/stable-testsuite.yml'
pull_request:
branches: [ v3.4, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/stable-testsuite.yml'
workflow_dispatch:
schedule:
- cron: '23 6 * * *'
jobs:
stable-testsuite:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, ubuntu-22.04 ]
name: Stable testsuite on ${{ matrix.os }}
steps:
- name: checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: checkout stable testsuite
uses: actions/checkout@v4
with:
repository: RsyncProject/rsync
ref: v34-stable-testsuite
path: stable-testsuite
fetch-depth: 1
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y gcc g++ gawk autoconf automake \
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev \
python3-cmarkgfm openssl
echo "/usr/local/bin" >>$GITHUB_PATH
- name: configure
run: ./configure --with-rrsync
- name: make check-progs
run: make check-progs
- name: info
run: ./rsync --version
# Pipe transport (the secure stdio default). The TCP-only daemon tests
# (daemon-access-ip, proxy-response-line-too-long) skip here and are run in
# the --use-tcp pass below; crtimes/daemon-chroot-acl/recv-discard-nullderef
# skip on the runner's filesystem / under root.
- name: run stable testsuite (pipe)
run: |
sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef \
./stable-testsuite/runtests.py \
--srcdir="$GITHUB_WORKSPACE/stable-testsuite" \
--rsync-bin="$GITHUB_WORKSPACE/rsync" \
-j16
# TCP transport over loopback, exercising the daemon paths the pipe run skips.
- name: run stable testsuite (tcp)
run: |
sudo ./stable-testsuite/runtests.py \
--srcdir="$GITHUB_WORKSPACE/stable-testsuite" \
--rsync-bin="$GITHUB_WORKSPACE/rsync" \
--use-tcp -j8

View File

@@ -6,12 +6,12 @@ name: Test rsync on Ubuntu 22.04
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'

View File

@@ -2,12 +2,12 @@ name: Test rsync on Ubuntu
on:
push:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
pull_request:
branches: [ master ]
branches: [ master, '*-stable' ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'

View File

@@ -44,7 +44,7 @@ LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \
zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o
util1.o util2.o main.o checksum.o match.o syscall.o android.o log.o backup.o delete.o
OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@
@@ -53,7 +53,7 @@ popt_OBJS= popt/popt.o popt/poptconfig.o \
popt/popthelp.o popt/poptparse.o popt/poptint.o
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
TLS_OBJ = tls.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
# Programs we must have to run the test cases
CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
@@ -83,12 +83,19 @@ install: all
$(INSTALLCMD) -m 755 $(srcdir)/rsync-ssl $(DESTDIR)$(bindir)
-$(MKDIR_P) $(DESTDIR)$(mandir)/man1
-$(MKDIR_P) $(DESTDIR)$(mandir)/man5
if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi
if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi
if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi
for fn in rsync.1 rsync-ssl.1; do \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
done
for fn in rsyncd.conf.5; do \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man5; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man5; fi; \
done
if test "$(with_rrsync)" = yes; then \
$(INSTALLCMD) -m 755 rrsync $(DESTDIR)$(bindir); \
if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \
fn=rrsync.1; \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
fi
install-ssl-daemon: stunnel-rsyncd.conf
@@ -171,19 +178,19 @@ getgroups$(EXEEXT): getgroups.o
getfsdev$(EXEEXT): getfsdev.o
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS)
TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o
TRIMSLASH_OBJ = trimslash.o syscall.o android.o util2.o t_stub.o lib/compat.o lib/snprintf.o
trimslash$(EXEEXT): $(TRIMSLASH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS)
T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
T_UNSAFE_OBJ = t_unsafe.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_CHMOD_SECURE_OBJ = t_chmod_secure.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
T_SECURE_RELPATH_OBJ = t_secure_relpath.o syscall.o android.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
@@ -319,6 +326,12 @@ test: check
# catch Bash-isms earlier even if we're running on GNU. Of course, we
# might lose in the future where POSIX diverges from old sh.
# Build the test-helper programs (CHECK_PROGS) without running the suite, so
# an external harness (e.g. fleettest.py) can invoke runtests.py with its own
# options.
.PHONY: check-progs
check-progs: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
.PHONY: check
check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
$(srcdir)/runtests.py --rsync-bin=`pwd`/rsync$(EXEEXT)

93
NEWS.md
View File

@@ -1,3 +1,95 @@
# NEWS for rsync 3.4.4 (8 Jun 2026)
## Changes in this version:
This is a conservative point release that backports regression fixes
on top of 3.4.3. No new features are included.
### BUG FIXES:
- Honour a relative alt-basis directory (e.g. `--link-dest=../sibling`,
`--copy-dest`, `--compare-dest`) on a daemon receiver running with
`use chroot = no`. Such a path is re-anchored at the module root but
was then rejected by the receiver's secure open; it now works where
kernel-enforced confinement is available. See the PORTABILITY note
below for the platform limitation. Fixes #915.
- sender: open a module-root-absolute path for a `path = /` module so a
daemon serving the filesystem root can satisfy absolute request
paths again. Fixes #897.
- flist: accept the missing-args mode-0 entry in recv_file_entry.
Fixes #910.
- receiver: fix a false "failed verification -- update discarded" when
resuming a delta transfer with an absolute `--partial-dir`.
- receiver: fix a NULL dereference on the delta discard path.
- generator: cap the block s2length at the negotiated checksum length.
- main: fix `--mkpath` with `--dry-run` for a file-to-file copy.
Fixes #880.
- daemon: un-backslash escaped option args. Fixes #829.
- token: drain the matched-block insert deflate. Fixes #951.
- Fix the "update skips a file of a different type" case and the
daemon upload delete stats.
- alloc: revert "zero all new memory from allocations". Fixes #959.
- Always clear the stat buffer and validate nanoseconds before use.
### PORTABILITY / BUILD:
- The relative alt-basis fix for daemon receivers (#915) relies on
kernel "stay below dirfd" path resolution -- `openat2(RESOLVE_BENEATH)`
on Linux 5.6+, or `openat()` with `O_RESOLVE_BENEATH` on FreeBSD 13+
and macOS 15+. On platforms that lack it (Solaris, OpenBSD, NetBSD,
Cygwin and older Linux) `secure_relative_open()` deliberately rejects
any path with a `..` component, so relative alt-basis directories
remain unavailable there -- function traded for safety, matching the
trade-off already documented for the #715 fix. Absolute alt-basis
paths are unaffected on every platform.
- openat2 is now autodetected at configure time (HAVE_OPENAT2): the
`openat2(RESOLVE_BENEATH)` resolver is compiled in only when both
`<linux/openat2.h>` and the `SYS_openat2` syscall number are present,
fixing the build on older kernels/headers. Fixes #924, #905, #900,
#904.
- Fall back to do_mknod() when mknodat() / mkfifoat() are unavailable.
Fixes #896.
- Install generated manpages correctly in an out-of-tree build.
### DEVELOPER RELATED:
- Added a CI workflow that builds this stable branch and runs the
`v34-stable-testsuite` regression suite against the built binary,
giving regression coverage without importing the full master test
suite into the stable branch.
- Added a check-progs target for fleettest and extended the build
workflows to run on `*-stable` release branches.
### CREDITS:
Thanks to everyone who helped with this release:
- Code contributions from Zen Dodd (steadytao), Mike-Goutokuji,
pterror, and Stiliyan Tonev (Bark).
- Zen Dodd (steadytao) also reviewed the 3.4.4 backport set (PR #980).
- Bug reports from @mmayer (#924), @fda77 (#905), @darkshram (#900),
@ketas (#904), @pkzc (#880), @brabalan (#951), @elcamlost (#829),
@debohman (#896), @guilherme-puida (#959), @fufu65 (#915),
@JetAppsClark (#928), @moonlitbugs (#897), @mgkeeley (#910), and
@sylvain-ilm (#724, #725).
# NEWS for rsync 3.4.3 (20 May 2026)
## Changes in this version:
@@ -5169,6 +5261,7 @@ to develop and test fixes.
| RELEASE DATE | VER. | DATE OF COMMIT\* | PROTOCOL |
|--------------|--------|------------------|-------------|
| 08 Jun 2026 | 3.4.4 | | 32 |
| 20 May 2026 | 3.4.3 | | 32 |
| 28 Apr 2026 | 3.4.2 | | 32 |
| 16 Jan 2025 | 3.4.1 | | 32 |

82
android.c Normal file
View File

@@ -0,0 +1,82 @@
/*
* Android-specific helpers.
*
* openat2() usability probe
* -------------------------
* openat2(2) is invoked directly via syscall() because the C library lacked a
* wrapper for it for years. Under a seccomp filter that uses
* SECCOMP_RET_TRAP -- as the Android application sandbox does -- a disallowed
* syscall raises SIGSYS and *kills the process* rather than failing with
* ENOSYS, so inspecting errno after the call is too late. We therefore probe
* openat2() once, behind a temporary SIGSYS handler, so a trapped syscall is
* caught and secure_relative_open_linux() can fall back to the portable
* per-component O_NOFOLLOW resolver instead of the whole process dying.
*
* This is only needed on Android, so the probe body is compiled only there.
* __ANDROID__ is defined by Bionic's headers and reflects the *target*, not
* the build host: it is set both for NDK cross-compiles (from a Linux/macOS
* host) and for native Termux builds, and is unset on every other platform.
* That makes it a reliable compile-time switch for cross builds -- there is
* nothing to detect in configure. Everywhere else openat2() is never
* seccomp-trapped to SIGSYS (a missing syscall simply returns ENOSYS), so
* openat2_usable() collapses to a constant 1 with no run-time cost.
*/
#include "rsync.h"
#if defined(__ANDROID__) && defined(HAVE_OPENAT2)
#include <setjmp.h>
#include <sys/syscall.h>
#include <linux/openat2.h>
static sigjmp_buf openat2_probe_env;
static void openat2_probe_handler(int signo)
{
(void)signo;
siglongjmp(openat2_probe_env, 1);
}
#endif
int openat2_usable(void)
{
#if defined(__ANDROID__) && defined(HAVE_OPENAT2)
static int cached = -1;
struct sigaction sa, old_sa;
if (cached >= 0)
return cached;
memset(&sa, 0, sizeof sa);
sa.sa_handler = openat2_probe_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGSYS, &sa, &old_sa) != 0)
return cached = 0;
if (sigsetjmp(openat2_probe_env, 1) != 0) {
/* SIGSYS delivered: openat2 is blocked by a seccomp filter. */
cached = 0;
} else {
struct open_how how;
int fd;
memset(&how, 0, sizeof how);
how.flags = O_RDONLY | O_DIRECTORY;
how.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
fd = syscall(SYS_openat2, AT_FDCWD, ".", &how, sizeof how);
if (fd >= 0)
close(fd);
/* Usable only if the probe actually succeeded. Any failure --
* ENOSYS (kernel < 5.6), a seccomp SECCOMP_RET_ERRNO denial
* (EPERM/EACCES), or EINVAL (RESOLVE_BENEATH unsupported) --
* means we must fall back to the portable O_NOFOLLOW walk. */
cached = fd >= 0;
}
sigaction(SIGSYS, &old_sa, NULL);
return cached;
#else
return 1;
#endif
}

View File

@@ -1070,7 +1070,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
io_printf(f_out, "@RSYNCD: OK\n");
read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request);
read_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request);
orig_argv = argv;
save_munge_symlinks = munge_symlinks;
@@ -1080,7 +1080,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
if (protect_args && ret) {
orig_early_argv = orig_argv;
protect_args = 2;
read_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request);
read_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request);
orig_argv = argv;
ret = parse_arguments(&argc, (const char ***) &argv);
} else

View File

@@ -331,6 +331,28 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])],
CFLAGS="$OLD_CFLAGS"
AC_SUBST(NOEXECSTACK)
dnl Only define HAVE_OPENAT2 when both the <linux/openat2.h> header and the
dnl SYS_openat2 syscall number are present. syscall.c uses openat2(RESOLVE_BENEATH)
dnl for the secure resolver on Linux 5.6+; defining it unconditionally broke the
dnl build on older kernels/headers that lack the header (#924, #905, #900).
AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[
AC_COMPILE_IFELSE([
AC_LANG_PROGRAM([[
#include <sys/syscall.h>
#include <linux/openat2.h>
]], [[
struct open_how how;
how.resolve = RESOLVE_BENEATH;
return SYS_openat2 + (int)how.resolve;
]])
],
[rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no])
])
if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then
AC_DEFINE([HAVE_OPENAT2], 1,
[Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])
fi
# arrgh. libc in some old debian version screwed up the largefile
# stuff, getting byte range locking wrong
AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[
@@ -888,7 +910,7 @@ AC_FUNC_UTIME_NULL
AC_FUNC_ALLOCA
AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \
fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \
chflags getattrlist mktime innetgr linkat \
chflags getattrlist mktime innetgr linkat mknodat mkfifoat \
memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \
strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \
setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \

54
flist.c
View File

@@ -132,6 +132,18 @@ static int64 tmp_dev = -1, tmp_ino;
#endif
static char tmp_sum[MAX_DIGEST_LEN];
#ifdef ST_MTIME_NSEC
/* Return st_mtim nsec if it is in the wire-valid range, else 0. */
static inline uint32 wire_mtime_nsec_from_stat(const STRUCT_STAT *stp)
{
unsigned long nsec = (unsigned long)stp->ST_MTIME_NSEC;
if (nsec > MAX_WIRE_NSEC)
return 0;
return (uint32)nsec;
}
#endif
static char empty_sum[MAX_DIGEST_LEN];
static int flist_count_offset; /* for --delete --progress */
static int show_filelist_progress;
@@ -865,13 +877,18 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
mode = from_wire_mode(read_int(f));
/* Reject modes whose type bits are not one of the standard
* file types; otherwise garbage mode values propagate through
* the file-type checks below unpredictably. */
if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
&& !S_ISCHR(mode) && !S_ISBLK(mode)
&& !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
* the file-type checks below unpredictably. mode 0 is the one
* legitimate exception: --delete-missing-args (missing_args==2)
* sends a missing arg as a mode-0 entry (IS_MISSING_FILE), the
* generator's delete signal (#910). */
if (mode != 0 || missing_args != 2) {
if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
&& !S_ISCHR(mode) && !S_ISBLK(mode)
&& !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n",
(unsigned)mode, lastname, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
}
}
if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) {
@@ -1250,7 +1267,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
int extra_len = file_extra_cnt * EXTRA_LEN;
const char *basename;
alloc_pool_t *pool;
STRUCT_STAT st;
STRUCT_STAT st = {0};
char *bp;
if (strlcpy(thisname, fname, sizeof thisname) >= sizeof thisname) {
@@ -1412,8 +1429,12 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
}
#ifdef ST_MTIME_NSEC
if (st.ST_MTIME_NSEC && protocol_version >= 31)
extra_len += EXTRA_LEN;
{
uint32 nsec = wire_mtime_nsec_from_stat(&st);
if (nsec && protocol_version >= 31)
extra_len += EXTRA_LEN;
}
#endif
#if SIZEOF_CAPITAL_OFF_T >= 8
if (st.st_size > 0xFFFFFFFFu && S_ISREG(st.st_mode))
@@ -1468,9 +1489,13 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
file->flags = flags;
file->modtime = st.st_mtime;
#ifdef ST_MTIME_NSEC
if (st.ST_MTIME_NSEC && protocol_version >= 31) {
file->flags |= FLAG_MOD_NSEC;
F_MOD_NSEC(file) = st.ST_MTIME_NSEC;
{
uint32 nsec = wire_mtime_nsec_from_stat(&st);
if (nsec && protocol_version >= 31) {
file->flags |= FLAG_MOD_NSEC;
F_MOD_NSEC(file) = nsec;
}
}
#endif
file->len32 = (uint32)st.st_size;
@@ -2070,10 +2095,9 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)
}
if (name_type != NORMAL_NAME) {
STRUCT_STAT st;
if (name_type == MISSING_NAME)
memset(&st, 0, sizeof st);
else if (link_stat(fbuf, &st, 1) != 0) {
STRUCT_STAT st = {0};
if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) {
interpret_stat_error(fbuf, True);
continue;
}
@@ -2205,7 +2229,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
static const char *lastdir;
static int lastdir_len = -1;
int len, dirlen;
STRUCT_STAT st;
STRUCT_STAT st = {0};
char *p, *dir;
struct file_list *flist;
struct timeval start_tv, end_tv;

View File

@@ -66,6 +66,7 @@ extern int inplace;
extern int append_mode;
extern int make_backups;
extern int csum_length;
extern int xfer_sum_len;
extern int ignore_times;
extern int size_only;
extern OFF_T max_size;
@@ -697,6 +698,11 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
{
int32 blength;
int s2length;
/* The strong sum can be no longer than the negotiated checksum digest:
* a short checksum (e.g. xxh64 = 8 bytes, when xxh128/xxh3 are absent)
* makes xfer_sum_len < SUM_LENGTH, and the sender rejects an s2length
* larger than xfer_sum_len (io.c). */
int max_s2length = MIN(SUM_LENGTH, xfer_sum_len);
int64 l;
if (len < 0) {
@@ -731,7 +737,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
if (protocol_version < 27) {
s2length = csum_length;
} else if (csum_length == SUM_LENGTH) {
s2length = SUM_LENGTH;
s2length = max_s2length;
} else {
int32 c;
int b = BLOCKSUM_BIAS;
@@ -740,7 +746,7 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
/* add a bit, subtract rollsum, round up. */
s2length = (b + 1 - 32 + 7) / 8; /* --optimize in compiler-- */
s2length = MAX(s2length, csum_length);
s2length = MIN(s2length, SUM_LENGTH);
s2length = MIN(s2length, max_s2length);
}
sum->flength = len;
@@ -1712,7 +1718,8 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
goto cleanup;
}
if (update_only > 0 && statret == 0 && file->modtime - sx.st.st_mtime < modify_window) {
if (update_only > 0 && statret == 0 && stype == ftype
&& file->modtime - sx.st.st_mtime < modify_window) {
if (INFO_GTE(SKIP, 1))
rprintf(FINFO, "%s is newer\n", fname);
#ifdef SUPPORT_HARD_LINKS
@@ -2384,7 +2391,7 @@ void generate_files(int f_out, const char *local_name)
write_ndx(f_out, NDX_DONE);
if (protocol_version >= 31 && EARLY_DELETE_DONE_MSG()) {
if ((INFO_GTE(STATS, 2) && (delete_mode || force_delete)) || read_batch)
if (delete_mode || force_delete || read_batch)
write_del_stats(f_out);
if (EARLY_DELAY_DONE_MSG()) /* Can't send this before delay */
write_ndx(f_out, NDX_DONE);
@@ -2429,7 +2436,7 @@ void generate_files(int f_out, const char *local_name)
if (protocol_version >= 31) {
if (!EARLY_DELETE_DONE_MSG()) {
if (INFO_GTE(STATS, 2) || read_batch)
if (delete_mode || force_delete || read_batch)
write_del_stats(f_out);
write_ndx(f_out, NDX_DONE);
}

20
io.c
View File

@@ -1292,8 +1292,21 @@ int read_line(int fd, char *buf, size_t bufsiz, int flags)
return s - buf;
}
/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a
* remote shell un-escapes args for the ssh transport. In place; \X -> X. */
static void unbackslash_arg(char *s)
{
char *f = s, *t = s;
while (*f) {
if (*f == '\\' && f[1])
f++;
*t++ = *f++;
}
*t = '\0';
}
void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
char ***argv_p, int *argc_p, char **request_p)
int unescape, char ***argv_p, int *argc_p, char **request_p)
{
int maxargs = MAX_ARGS;
int dot_pos = 0, argc = 0, request_len = 0;
@@ -1335,6 +1348,11 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
glob_expand(buf, &argv, &argc, &maxargs);
} else {
p = strdup(buf);
/* An option arg the client escaped with safe_arg() (no
* remote shell un-escapes it for a daemon). File args
* after the dot are handled by glob_expand() below. */
if (unescape)
unbackslash_arg(p);
argv[argc++] = p;
if (*p == '.' && p[1] == '\0')
dot_pos = argc;

13
main.c
View File

@@ -832,7 +832,16 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
dest_path = "/";
*cp = '\0';
if (!change_dir(dest_path, CD_NORMAL)) {
if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) {
/* --mkpath would have created this parent dir, but a dry run did
* not, so don't chdir into it; flag the destination as not yet
* present (as the dir-creation path above does) so the generator
* doesn't try to compare against the missing tree (#880). Only
* the missing-parent case is touched, so an ordinary file-to-file
* dry run still itemizes against an existing destination. */
dry_run++;
change_dir(dest_path, CD_SKIP_CHDIR);
} else if (!change_dir(dest_path, CD_NORMAL)) {
rsyserr(FERROR, errno, "change_dir#3 %s failed",
full_fname(dest_path));
exit_cleanup(RERR_FILESELECT);
@@ -1840,7 +1849,7 @@ int main(int argc,char *argv[])
if (am_server && protect_args) {
char buf[MAXPATHLEN];
protect_args = 2;
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL);
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL);
if (!parse_arguments(&argc, (const char ***) &argv)) {
option_error();
exit_cleanup(RERR_SYNTAX);

View File

@@ -1,6 +1,6 @@
Summary: A fast, versatile, remote (and local) file-copying tool
Name: rsync
Version: 3.4.3
Version: 3.4.4
%define fullversion %{version}
Release: 1
%define srcdir src
@@ -79,5 +79,5 @@ rm -rf $RPM_BUILD_ROOT
%dir /etc/rsync-ssl/certs
%changelog
* Wed May 20 2026 Rsync Project <rsync.project@gmail.com>
Released 3.4.3.
* Mon Jun 08 2026 Rsync Project <rsync.project@gmail.com>
Released 3.4.4.

View File

@@ -83,6 +83,65 @@ static int updating_basis_or_equiv;
#define MAX_UNIQUE_NUMBER 999999
#define MAX_UNIQUE_LOOP 100
/* Open a basis/output path that may legitimately be an operator-trusted
* ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for
* partial-dir work") or --backup-dir. secure_relative_open() deliberately
* rejects an absolute relpath, so feeding it the whole absolute partialptr
* (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is
* mapped, and receive_data() omits every matched block from the whole-file
* verification checksum -> a spurious "failed verification" that strands the
* (correct) data in the partial-dir forever.
*
* The operator's directory is trusted; only the leaf basename is peer-supplied.
* So when basedir is NULL and relpath is absolute, split it into its directory
* (trusted) and leaf and confine just the leaf -- exactly how secure_relative_
* open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.
* Anything else is a straight pass-through that preserves the strict contract. */
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
{
extern int am_daemon, am_chrooted;
/* The confined resolver is only needed for the sanitizing daemon
* (am_daemon && !am_chrooted, i.e. use_secure_symlinks). Local /
* remote-shell mode has no module boundary, and "use chroot = yes" makes
* the kernel root the boundary, so there an alt-dest basis like
* --link-dest=../01 must resolve against the cwd as a bare open did before
* the hardening (confining it would reject the legitimate sibling "..",
* #915). */
if (!am_daemon || am_chrooted) {
if (basedir) {
char fullpath[MAXPATHLEN];
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
errno = ENAMETOOLONG;
return -1;
}
return do_open(fullpath, flags, mode);
}
return do_open(relpath, flags, mode);
}
if (!basedir && relpath && *relpath == '/') {
const char *slash = strrchr(relpath, '/');
const char *leaf = slash + 1;
char dirbuf[MAXPATHLEN];
const char *dir;
if (slash == relpath)
dir = "/";
else {
size_t dlen = slash - relpath;
if (dlen >= sizeof dirbuf) {
errno = ENAMETOOLONG;
return -1;
}
memcpy(dirbuf, relpath, dlen);
dirbuf[dlen] = '\0';
dir = dirbuf;
}
return secure_relative_open(dir, leaf, flags, mode);
}
return secure_relative_open(basedir, relpath, flags, mode);
}
/* get_tmpname() - create a tmp filename for a given filename
*
* If a tmpdir is defined, use that as the directory to put it in. Otherwise,
@@ -364,6 +423,34 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
stats.matched_data += len;
/* A block match with no mapped basis is a protocol inconsistency
* ONLY when we are actually producing output (fd != -1): the
* generator told the sender a basis existed but the receiver could
* not open it, so honoring the match would silently omit these
* bytes from the verification checksum (a spurious failure) or
* leave a hole in the output. Fail cleanly in that case.
*
* On the DISCARD path (fd == -1, fname == NULL) there is no output
* and no verification: discard_receive_data() deliberately drains a
* delta the receiver never intends to write (basis fstat failed,
* basis is a directory, output open failed, batch skip, ...). The
* sender does not know the data is being discarded and streams an
* ordinary delta, so a match token here is NORMAL protocol, not
* malformed. Absorb it benignly (advance the offset and continue),
* as the pre-existing "if (mapbuf)" guards did before this check was
* added in 31fbb17d -- erroring would wrongly break legitimate
* transfers, and full_fname(fname) with fname==NULL would
* dereference NULL (a receiver crash on a normal transfer). */
if (!mapbuf) {
if (fd != -1) {
rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
full_fname(fname), who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
offset += len;
continue;
}
if (DEBUG_GTE(DELTASUM, 3)) {
rprintf(FINFO,
"chunk[%d] of size %ld at %s offset=%s%s\n",
@@ -793,8 +880,9 @@ int recv_files(int f_in, int f_out, char *local_name)
fnamecmp = fname;
}
/* open the file */
fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
/* open the file (secure_basis_open tolerates an operator-trusted
* absolute fnamecmp, e.g. an absolute --partial-dir basis) */
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
if (fd1 == -1 && protocol_version < 29) {
if (fnamecmp != fname) {
@@ -808,7 +896,7 @@ int recv_files(int f_in, int f_out, char *local_name)
basedir = basis_dir[0];
fnamecmp = fname;
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
fd1 = secure_relative_open(basedir, fnamecmp, O_RDONLY, 0);
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
}
}
@@ -884,7 +972,7 @@ int recv_files(int f_in, int f_out, char *local_name)
* attacker could switch a directory to a symlink between
* path validation and file open. */
if (use_secure_symlinks)
fd2 = secure_relative_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
fd2 = secure_basis_open(NULL, fnametmp, O_WRONLY|O_CREAT, 0600);
else
fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
#ifdef linux

View File

@@ -362,6 +362,7 @@ void send_files(int f_in, int f_out)
* Reconstruct the full path relative to module_dir
* from F_PATHNAME (path) and f_name (fname). */
char secure_path[MAXPATHLEN];
const char *relp;
int slen = snprintf(secure_path, sizeof secure_path, "%s%s%s", path, slash, fname);
if (slen >= (int)sizeof secure_path) {
io_error |= IOERR_GENERAL;
@@ -371,7 +372,13 @@ void send_files(int f_in, int f_out)
send_msg_int(MSG_NO_SEND, ndx);
continue;
}
fd = secure_relative_open(module_dir, secure_path, O_RDONLY, 0);
/* A module with `path = /` makes F_PATHNAME absolute, so the
* joined path starts with '/'; strip leading slashes to a
* module-relative path that secure_relative_open accepts (#897). */
relp = secure_path;
while (*relp == '/')
relp++;
fd = secure_relative_open(module_dir, relp, O_RDONLY, 0);
} else {
fd = do_open_checklinks(fname);
}

120
syscall.c
View File

@@ -33,7 +33,7 @@
#include <sys/syscall.h>
#endif
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
#include <sys/syscall.h>
#include <linux/openat2.h>
#endif
@@ -535,7 +535,9 @@ int do_mknod(const char *pathname, mode_t mode, dev_t dev)
*/
int do_mknod_at(const char *pathname, mode_t mode, dev_t dev)
{
#ifdef AT_FDCWD
/* HAVE_MKNODAT: older Darwin declares AT_FDCWD but not mknodat(), so
* the at-variant won't build there; fall back to do_mknod() (#896). */
#if defined(AT_FDCWD) && defined(HAVE_MKNODAT)
extern int am_daemon, am_chrooted;
char dirpath[MAXPATHLEN];
const char *bname;
@@ -597,7 +599,7 @@ int do_mknod_at(const char *pathname, mode_t mode, dev_t dev)
return ret;
}
#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO
#if !defined MKNOD_CREATES_FIFOS && defined HAVE_MKFIFO && defined HAVE_MKFIFOAT
if (S_ISFIFO(mode))
ret = mkfifoat(dfd, bname, mode);
else
@@ -1691,7 +1693,20 @@ static int path_has_dotdot_component(const char *path)
return 0;
}
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
/* openat2(RESOLVE_BENEATH) via the raw syscall, gated on openat2_usable() so a
* seccomp filter that traps openat2 with SIGSYS (e.g. the Android sandbox)
* makes us report ENOSYS and fall back rather than killing the process. Only
* the openat2 call is gated here; a plain openat() is always safe to attempt. */
static int openat2_beneath(int dirfd, const char *path, const struct open_how *how)
{
if (!openat2_usable()) {
errno = ENOSYS;
return -1;
}
return syscall(SYS_openat2, dirfd, path, how, sizeof *how);
}
static int secure_relative_open_linux(const char *basedir, const char *relpath, int flags, mode_t mode)
{
struct open_how how;
@@ -1720,12 +1735,12 @@ static int secure_relative_open_linux(const char *basedir, const char *relpath,
memset(&bhow, 0, sizeof bhow);
bhow.flags = O_RDONLY | O_DIRECTORY;
bhow.resolve = RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS;
dirfd = syscall(SYS_openat2, AT_FDCWD, basedir, &bhow, sizeof bhow);
dirfd = openat2_beneath(AT_FDCWD, basedir, &bhow);
if (dirfd == -1)
return -1;
}
retfd = syscall(SYS_openat2, dirfd, relpath, &how, sizeof how);
retfd = openat2_beneath(dirfd, relpath, &how);
if (dirfd != AT_FDCWD)
close(dirfd);
@@ -1766,13 +1781,68 @@ static int secure_relative_open_resolve_beneath(const char *basedir, const char
}
#endif
/* The logical current directory (maintained by change_dir() in util1.c).
* Defined here -- rather than in util1.c -- so the test helpers that link
* syscall.o but not util1.o (tls, trimslash) get the definition without a
* weak-symbol fallback, which is not portable to PE/COFF targets (Cygwin). */
char curr_dir[MAXPATHLEN];
unsigned int curr_dir_len;
int secure_relative_open(const char *basedir, const char *relpath, int flags, mode_t mode)
{
extern int am_daemon, am_chrooted;
extern char *module_dir;
extern unsigned int module_dirlen;
char modrel_buf[MAXPATHLEN];
int reanchored = 0;
if (!relpath || relpath[0] == '/') {
// must be a relative path
errno = EINVAL;
return -1;
}
/* Sanitizing daemon only (am_daemon && !am_chrooted). Here we have chdir'd
* into a sub-dir of the module (the transfer destination), so a relative
* alt-dest like "../01" may legitimately climb to a sibling that is still
* inside the module (#915). Confining beneath the cwd would reject that
* climb. Re-anchor at the module root -- the real trust boundary -- by
* prefixing the cwd's module-relative path (from rsync's logical curr_dir[],
* a guaranteed lexical prefix of module_dir, unlike getcwd()) and resolving
* beneath module_dir; RESOLVE_BENEATH then allows in-module climbs and still
* rejects escapes. Only for paths that contain "..". module_dirlen is 0 for
* a `path = /` module (clientserver.c), so we gate on module_dir, not its
* length, to cover that case too -- the prefix check below treats
* module_dirlen 0 as "module root is /". */
if (am_daemon && !am_chrooted
&& module_dir && module_dir[0] == '/'
&& (basedir == NULL || basedir[0] != '/')
&& (path_has_dotdot_component(relpath)
|| (basedir && path_has_dotdot_component(basedir)))) {
const char *p;
int n;
if (curr_dir_len >= module_dirlen
&& strncmp(curr_dir, module_dir, module_dirlen) == 0
&& (curr_dir[module_dirlen] == '\0' || curr_dir[module_dirlen] == '/')) {
for (p = curr_dir + module_dirlen; *p == '/'; p++) {}
if (basedir)
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s/%s",
p, *p ? "/" : "", basedir, relpath);
else
n = snprintf(modrel_buf, sizeof modrel_buf, "%s%s%s",
p, *p ? "/" : "", relpath);
if (n < 0 || n >= (int)sizeof modrel_buf) {
errno = ENAMETOOLONG;
return -1;
}
basedir = module_dir; /* absolute, operator-trusted anchor */
relpath = modrel_buf;
reanchored = 1;
}
/* else: cwd not under module root as expected -- fall through to the
* front-door rejection below (fail safe). */
}
/* Reject any path with a literal ".." component (bare "..",
* "../foo", "foo/..", "foo/../bar", "subdir/.."). The previous
* substring-based check caught only "../" prefix and "/../"
@@ -1781,17 +1851,22 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
* and pre-5.6 Linux. RESOLVE_BENEATH on Linux/FreeBSD/macOS
* catches some of these in-kernel with EXDEV, but the front
* door must reject them consistently with EINVAL across all
* platforms so callers can rely on the validation. */
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
* platforms so callers can rely on the validation. Skipped for a
* re-anchored path: its ".." is deliberate, stays within the module,
* and is adjudicated by RESOLVE_BENEATH below (the portable fallback
* re-rejects it -- see there). */
if (!reanchored) {
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
}
}
#ifdef __linux__
#if defined(__linux__) && defined(HAVE_OPENAT2)
{
int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
/* ENOSYS = kernel < 5.6 doesn't have the syscall even though
@@ -1805,6 +1880,21 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode);
#endif
/* Portable fallback only (no kernel RESOLVE_BENEATH): the per-component
* O_NOFOLLOW walk below can't adjudicate ".." safely, so reject it here --
* even for a re-anchored path. This re-breaks --link-dest=../01 on
* openat2/O_RESOLVE_BENEATH-less platforms (NetBSD/OpenBSD/Solaris/Cygwin/
* pre-5.6 Linux), trading function for safety; on the kernel paths above
* RESOLVE_BENEATH already allowed the in-module climb. */
if (path_has_dotdot_component(relpath)) {
errno = EINVAL;
return -1;
}
if (basedir && basedir[0] != '/' && path_has_dotdot_component(basedir)) {
errno = EINVAL;
return -1;
}
#if !defined(O_NOFOLLOW) || !defined(O_DIRECTORY) || !defined(AT_FDCWD)
// really old system, all we can do is live with the risks
if (!basedir) {

View File

@@ -36,9 +36,14 @@ int preserve_perms = 0;
int preserve_executability = 0;
int omit_link_times = 0;
int open_noatime = 0;
size_t max_alloc = 0; /* max_alloc is needed when combined with util2.o */
size_t max_alloc = (size_t)-1; /* unlimited: helpers link util2.o, where 0 makes
* every my_alloc()/my_strdup() abort with
* "exceeded --max-alloc=0" (hit on the
* secure_relative_open() fallback path). */
char *partial_dir;
char *module_dir;
/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in
* syscall.c, which every helper links -- no stub needed here. */
filter_rule_list daemon_filter_list;
void rprintf(UNUSED(enum logcode code), const char *format, ...)

31
token.c
View File

@@ -481,14 +481,29 @@ send_deflated_token(int f, int32 token, struct map_struct *buf, OFF_T offset, in
tx_strm.avail_in = n1;
if (protocol_version >= 31) /* Newer protocols avoid a data-duplicating bug */
offset += n1;
tx_strm.next_out = (Bytef *) obuf;
tx_strm.avail_out = AVAIL_OUT_SIZE(CHUNK_SIZE);
r = deflate(&tx_strm, Z_INSERT_ONLY);
if (r != Z_OK || tx_strm.avail_in != 0) {
rprintf(FERROR, "deflate on token returned %d (%d bytes left)\n",
r, tx_strm.avail_in);
exit_cleanup(RERR_STREAMIO);
}
/* With our bundled zlib's Z_INSERT_ONLY this produces no
* output and consumes the input in one call. A build
* against a system zlib lacks Z_INSERT_ONLY and falls back
* to Z_SYNC_FLUSH (see top of file), which emits a flush
* block we discard -- and for an incompressible token that
* block can exceed obuf. Loop, resetting the output buffer,
* until all the input is consumed so a large token can't
* overflow obuf and abort the transfer (#951). Drain until
* avail_out != 0 too: a full output buffer can leave pending
* bytes that would otherwise leak into the next real deflate
* send and corrupt the stream (same condition as the data loop
* above). The discarded output is not sent: the receiver
* rebuilds the matching history itself in see_deflate_token(). */
do {
tx_strm.next_out = (Bytef *) obuf;
tx_strm.avail_out = AVAIL_OUT_SIZE(CHUNK_SIZE);
r = deflate(&tx_strm, Z_INSERT_ONLY);
if (r != Z_OK) {
rprintf(FERROR, "deflate on token returned %d (%d bytes left)\n",
r, tx_strm.avail_in);
exit_cleanup(RERR_STREAMIO);
}
} while (tx_strm.avail_in != 0 || tx_strm.avail_out == 0);
} while (toklen > 0);
}
}

View File

@@ -41,8 +41,8 @@ extern filter_rule_list daemon_filter_list;
int sanitize_paths = 0;
char curr_dir[MAXPATHLEN];
unsigned int curr_dir_len;
extern char curr_dir[MAXPATHLEN]; /* defined in syscall.c */
extern unsigned int curr_dir_len;
int curr_dir_depth; /* This is only set for a sanitizing daemon. */
/* Set a fd into nonblocking mode. */
@@ -1788,8 +1788,6 @@ void *expand_item_list(item_list *lp, size_t item_size, const char *desc, int in
new_ptr == lp->items ? " not" : "");
}
memset((char *)new_ptr + lp->malloced * item_size, 0,
(expand_size - lp->malloced) * item_size);
lp->items = new_ptr;
lp->malloced = expand_size;
}

View File

@@ -79,7 +79,9 @@ void *my_alloc(void *ptr, size_t num, size_t size, const char *file, int line)
who_am_i(), do_big_num(max_alloc, 0, NULL), src_file(file), line);
exit_cleanup(RERR_MALLOC);
}
if (!ptr || ptr == do_calloc)
if (!ptr)
ptr = malloc(num * size);
else if (ptr == do_calloc)
ptr = calloc(num, size);
else
ptr = realloc(ptr, num * size);

View File

@@ -1,2 +1,2 @@
#define RSYNC_VERSION "3.4.3"
#define RSYNC_VERSION "3.4.4"
#define MAINTAINER_TZ_OFFSET 10.0