mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-08 06:05:57 -04:00
Compare commits
23 Commits
v34-stable
...
v3.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f26f747b80 | ||
|
|
ed2950f867 | ||
|
|
37d0080e92 | ||
|
|
5073e6a575 | ||
|
|
bb8d1c14c5 | ||
|
|
517c35e2db | ||
|
|
ee4f668f29 | ||
|
|
c14e2258b5 | ||
|
|
499ed5e1ab | ||
|
|
c7ca5217a7 | ||
|
|
20cc824592 | ||
|
|
f86309f230 | ||
|
|
ee7c8a5783 | ||
|
|
b29c149529 | ||
|
|
7811f2b1b9 | ||
|
|
f3757a470a | ||
|
|
6c8295fd62 | ||
|
|
a8f80f5a12 | ||
|
|
d8847ff7a8 | ||
|
|
51c5f05771 | ||
|
|
f68facd22f | ||
|
|
9e2e9f3362 | ||
|
|
3786926703 |
4
.github/workflows/almalinux-8-build.yml
vendored
4
.github/workflows/almalinux-8-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/cygwin-build.yml
vendored
4
.github/workflows/cygwin-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/freebsd-build.yml
vendored
4
.github/workflows/freebsd-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/macos-build.yml
vendored
4
.github/workflows/macos-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/netbsd-build.yml
vendored
4
.github/workflows/netbsd-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/openbsd-build.yml
vendored
4
.github/workflows/openbsd-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/solaris-build.yml
vendored
4
.github/workflows/solaris-build.yml
vendored
@@ -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
76
.github/workflows/stable-testsuite.yml
vendored
Normal 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
|
||||
4
.github/workflows/ubuntu-22.04-build.yml
vendored
4
.github/workflows/ubuntu-22.04-build.yml
vendored
@@ -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'
|
||||
|
||||
4
.github/workflows/ubuntu-build.yml
vendored
4
.github/workflows/ubuntu-build.yml
vendored
@@ -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'
|
||||
|
||||
33
Makefile.in
33
Makefile.in
@@ -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
93
NEWS.md
@@ -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
82
android.c
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
24
configure.ac
24
configure.ac
@@ -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
54
flist.c
@@ -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;
|
||||
|
||||
17
generator.c
17
generator.c
@@ -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
20
io.c
@@ -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
13
main.c
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
96
receiver.c
96
receiver.c
@@ -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
|
||||
|
||||
9
sender.c
9
sender.c
@@ -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
120
syscall.c
@@ -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) {
|
||||
|
||||
7
t_stub.c
7
t_stub.c
@@ -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
31
token.c
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
6
util1.c
6
util1.c
@@ -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;
|
||||
}
|
||||
|
||||
4
util2.c
4
util2.c
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user