mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-08 22:26:01 -04:00
Compare commits
3 Commits
master
...
fix/chmod-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b68225e5 | ||
|
|
b2fc33868f | ||
|
|
7371c898e4 |
4
.github/workflows/almalinux-8-build.yml
vendored
4
.github/workflows/almalinux-8-build.yml
vendored
@@ -18,7 +18,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/almalinux-8-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
# crtimes-not-supported skip matches the other Linux jobs;
|
||||
# daemon-chroot-acl and proxy-response-line-too-long skip because
|
||||
# the default (secure) transport opens no listening socket.
|
||||
run: RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check
|
||||
run: RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run exercising the real loopback-TCP daemon path.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
|
||||
|
||||
2
.github/workflows/android-static-build.yml
vendored
2
.github/workflows/android-static-build.yml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/android-static-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
|
||||
72
.github/workflows/asan-build.yml
vendored
72
.github/workflows/asan-build.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: rsync ASan+UBSan (clang)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
schedule:
|
||||
# Weekly (Mon 09:42 UTC): catch breakage from a moving ubuntu-latest/clang
|
||||
# toolchain (a new clang can add a UBSan check, or change ASan behaviour)
|
||||
# that no code push would otherwise trigger. Push/PR already gate every
|
||||
# code change, so daily would just re-run an unchanged tree.
|
||||
- cron: '42 9 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
asan:
|
||||
runs-on: ubuntu-latest
|
||||
name: rsync ASan+UBSan (clang)
|
||||
env:
|
||||
# rsync intentionally leaks small allocations at process exit, so leak
|
||||
# detection would be all noise; chase only memory-safety errors.
|
||||
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
|
||||
# UBSan is a gate: -fno-sanitize-recover=undefined (below) aborts on the
|
||||
# first finding and halt_on_error=1 makes that fatal, so any undefined
|
||||
# behaviour fails the run. This needs the tree to be UBSan-clean: the
|
||||
# remaining findings are fixed in code (hashtable/mdfour shifts, xattrs,
|
||||
# and log.c's file_struct, kept aligned via rounding.h); only byteorder.h's
|
||||
# intentional unaligned accessors are suppressed, with no_sanitize.
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
# -DNDEBUG builds as a shipped release does (assert() compiled out), so
|
||||
# AddressSanitizer catches the over-reads/over-writes that an "assert()
|
||||
# instead of a real bounds check" bug would cause in a production build.
|
||||
# UBSan rides along on the same build; -fno-sanitize-recover=undefined
|
||||
# makes any undefined behaviour abort (and thus fail the run) instead of
|
||||
# merely printing it.
|
||||
run: |
|
||||
CC=clang \
|
||||
CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g -O1 -DNDEBUG" \
|
||||
LDFLAGS="-fsanitize=address,undefined" \
|
||||
./configure --with-rrsync --disable-md2man
|
||||
- name: make
|
||||
# check-progs builds rsync plus the test helper programs (tls, trimslash,
|
||||
# t_unsafe, ...) that runtests.py requires; plain "make" builds only rsync
|
||||
# and runtests aborts on the missing helpers.
|
||||
run: make check-progs
|
||||
- name: info
|
||||
run: ./rsync --version
|
||||
- name: check (stdio-pipe transport)
|
||||
# ASan+UBSan-instrumented coverage of the transfer, daemon, sender,
|
||||
# receiver and metadata paths over the default stdio-pipe transport.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" -j8
|
||||
- name: check (TCP daemon transport)
|
||||
# --use-tcp also exercises the loopback rsyncd listener and the client's
|
||||
# TCP connection path.
|
||||
run: ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j8
|
||||
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/coverage.yml'
|
||||
schedule:
|
||||
- cron: '42 9 * * 1'
|
||||
- cron: '42 9 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
4
.github/workflows/cygwin-build.yml
vendored
4
.github/workflows/cygwin-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/cygwin-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
# RESOLVE_BENEATH symlink-race tests. symlink-dirlink-basis also now
|
||||
# RUNS (the #915 non-daemon basis open uses a plain do_open, restoring
|
||||
# following an in-tree dir-symlink basis without RESOLVE_BENEATH).
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,acls,bare-do-open-symlink-race,chdir-symlink-race,chown,daemon-access-ip,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,proxy-response-line-too-long,recv-discard-nullderef,sender-flist-symlink-leak,simd-checksum make check'
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,acls,bare-do-open-symlink-race,chdir-symlink-race,chown,daemon-access-ip,daemon-chroot-acl,devices,dir-sgid,open-noatime,protected-regular,proxy-response-line-too-long,sender-flist-symlink-leak,simd-checksum make check'
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
|
||||
2
.github/workflows/freebsd-build.yml
vendored
2
.github/workflows/freebsd-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/freebsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
4
.github/workflows/macos-build.yml
vendored
4
.github/workflows/macos-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/macos-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on macOS
|
||||
# (rsyncfns.py drives xattrs via the `xattr` command), verified on a
|
||||
# real macOS host, so they're no longer in the skip set.
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,chmod-temp-dir,daemon-access-ip,daemon-chroot-acl,dir-sgid,open-noatime,preallocate,protected-regular,proxy-response-line-too-long,recv-discard-nullderef,simd-checksum,sparse make check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,acls-depth,chmod-temp-dir,daemon-access-ip,daemon-chroot-acl,dir-sgid,open-noatime,preallocate,protected-regular,proxy-response-line-too-long,simd-checksum,sparse make check
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
|
||||
2
.github/workflows/netbsd-build.yml
vendored
2
.github/workflows/netbsd-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/netbsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
2
.github/workflows/openbsd-build.yml
vendored
2
.github/workflows/openbsd-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/openbsd-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
51
.github/workflows/scan-build.yml
vendored
51
.github/workflows/scan-build.yml
vendored
@@ -1,51 +0,0 @@
|
||||
name: rsync scan-build (clang analyzer)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
scan-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: rsync scan-build (clang analyzer)
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang clang-tools acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev openssl
|
||||
- name: configure (under scan-build)
|
||||
# Run configure under scan-build so its analyzer compiler-wrapper is baked
|
||||
# into the Makefile's $(CC); --disable-md2man avoids the doc toolchain.
|
||||
run: scan-build ./configure --with-rrsync --disable-md2man
|
||||
- name: scan-build (informational)
|
||||
# Static analysis only -- INFORMATIONAL, not a gate. rsync currently has
|
||||
# a fair number of reports that are overwhelmingly known false positives
|
||||
# (e.g. unix.Chroot "no chdir after chroot", core.NonNullParamChecker
|
||||
# against functions that can't actually receive NULL). We publish the
|
||||
# HTML report as an artifact and print the bug count to the run summary,
|
||||
# but do NOT pass --status-bugs, so this surfaces new analyzer findings
|
||||
# without going red on arrival. check-progs builds rsync + the test
|
||||
# helpers without needing the man-page toolchain.
|
||||
run: |
|
||||
scan-build -o "$PWD/scan-report" make check-progs -j"$(nproc)" 2>&1 | tee scan-build.out
|
||||
echo '## scan-build summary' >>"$GITHUB_STEP_SUMMARY"
|
||||
grep -E 'scan-build: .* bugs? found|scan-build: No bugs found' scan-build.out >>"$GITHUB_STEP_SUMMARY" || true
|
||||
- name: upload report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: scan-build-report
|
||||
path: scan-report
|
||||
if-no-files-found: ignore
|
||||
2
.github/workflows/solaris-build.yml
vendored
2
.github/workflows/solaris-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/solaris-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
8
.github/workflows/ubuntu-22.04-build.yml
vendored
8
.github/workflows/ubuntu-22.04-build.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-22.04-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -39,11 +39,11 @@ jobs:
|
||||
- name: info
|
||||
run: rsync --version
|
||||
- name: check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check
|
||||
- name: check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check30
|
||||
- name: check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check29
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd; the default
|
||||
# 'make check' above uses the secure stdio-pipe transport.
|
||||
|
||||
36
.github/workflows/ubuntu-build.yml
vendored
36
.github/workflows/ubuntu-build.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-build.yml'
|
||||
schedule:
|
||||
- cron: '42 8 * * 1'
|
||||
- cron: '42 8 * * *'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -30,44 +30,16 @@ jobs:
|
||||
run: ./configure --with-rrsync
|
||||
- name: make
|
||||
run: make
|
||||
- name: install/uninstall DESTDIR smoke test
|
||||
run: |
|
||||
set -e
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
make install-all DESTDIR="$tmp"
|
||||
|
||||
for path in \
|
||||
/usr/local/bin/rsync \
|
||||
/usr/local/bin/rsync-ssl \
|
||||
/usr/local/bin/rrsync \
|
||||
/usr/local/share/man/man1/rsync.1 \
|
||||
/usr/local/share/man/man1/rsync-ssl.1 \
|
||||
/usr/local/share/man/man1/rrsync.1 \
|
||||
/usr/local/share/man/man5/rsyncd.conf.5 \
|
||||
/etc/stunnel/rsyncd.conf
|
||||
do
|
||||
test -e "$tmp$path"
|
||||
done
|
||||
|
||||
make uninstall-all DESTDIR="$tmp"
|
||||
|
||||
leftover="$(find "$tmp" -type f -print)"
|
||||
if [ -n "$leftover" ]; then
|
||||
printf '%s\n' "$leftover"
|
||||
exit 1
|
||||
fi
|
||||
- name: install
|
||||
run: sudo make install
|
||||
- name: info
|
||||
run: rsync --version
|
||||
- name: check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check
|
||||
- name: check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check30
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check30
|
||||
- name: check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long,recv-discard-nullderef make check29
|
||||
run: sudo RSYNC_EXPECT_SKIPPED=crtimes,daemon-access-ip,daemon-chroot-acl,proxy-response-line-too-long make check29
|
||||
- name: check (TCP daemon transport)
|
||||
# Second run with daemon tests over a real loopback rsyncd. The default
|
||||
# 'make check' above uses the secure stdio-pipe transport (no listening
|
||||
|
||||
2
.github/workflows/ubuntu-version-mix.yml
vendored
2
.github/workflows/ubuntu-version-mix.yml
vendored
@@ -33,7 +33,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-version-mix.yml'
|
||||
schedule:
|
||||
- cron: '52 8 * * 1'
|
||||
- cron: '52 8 * * *'
|
||||
|
||||
jobs:
|
||||
version-mix:
|
||||
|
||||
16
Makefile.in
16
Makefile.in
@@ -111,21 +111,6 @@ install-all: install install-ssl-daemon
|
||||
install-strip:
|
||||
$(MAKE) INSTALL_STRIP='-s' install
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(bindir)/rsync$(EXEEXT) $(DESTDIR)$(bindir)/rsync-ssl
|
||||
rm -f $(DESTDIR)$(bindir)/rrsync
|
||||
rm -f $(DESTDIR)$(mandir)/man1/rsync.1 $(DESTDIR)$(mandir)/man1/rsync-ssl.1
|
||||
rm -f $(DESTDIR)$(mandir)/man1/rrsync.1
|
||||
rm -f $(DESTDIR)$(mandir)/man5/rsyncd.conf.5
|
||||
|
||||
.PHONY: uninstall-ssl-daemon
|
||||
uninstall-ssl-daemon:
|
||||
rm -f $(DESTDIR)/etc/stunnel/rsyncd.conf
|
||||
|
||||
.PHONY: uninstall-all
|
||||
uninstall-all: uninstall uninstall-ssl-daemon
|
||||
|
||||
rsync$(EXEEXT): $(OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS)
|
||||
|
||||
@@ -139,7 +124,6 @@ usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h defaul
|
||||
loadparm.o: default-dont-compress.h daemon-parm.h
|
||||
|
||||
flist.o: rounding.h
|
||||
log.o: rounding.h
|
||||
|
||||
default-cvsignore.h default-dont-compress.h: rsync.1.md define-from-md.awk
|
||||
$(AWK) -f $(srcdir)/define-from-md.awk -v hfile=$@ $(srcdir)/rsync.1.md
|
||||
|
||||
31
byteorder.h
31
byteorder.h
@@ -68,26 +68,10 @@ SIVAL64(char *buf, int pos, int64 val)
|
||||
|
||||
#else /* !CAREFUL_ALIGNMENT */
|
||||
|
||||
/* We don't want false positives about alignment from UBSAN, see:
|
||||
https://github.com/WayneD/rsync/issues/427#issuecomment-1375132291
|
||||
*/
|
||||
|
||||
/* From https://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html */
|
||||
#ifndef GCC_VERSION
|
||||
#define GCC_VERSION (__GNUC__ * 10000 \
|
||||
+ __GNUC_MINOR__ * 100 \
|
||||
+ __GNUC_PATCHLEVEL__)
|
||||
#endif
|
||||
|
||||
/* This handles things for architectures like the 386 that can handle alignment errors.
|
||||
* WARNING: This section is dependent on the length of an int32 (and thus a uint32)
|
||||
* being correct (4 bytes)! Set CAREFUL_ALIGNMENT if it is not. */
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline uint32
|
||||
IVALu(const uchar *buf, int pos)
|
||||
{
|
||||
@@ -99,11 +83,6 @@ IVALu(const uchar *buf, int pos)
|
||||
return *u.num;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline void
|
||||
SIVALu(uchar *buf, int pos, uint32 val)
|
||||
{
|
||||
@@ -115,11 +94,6 @@ SIVALu(uchar *buf, int pos, uint32 val)
|
||||
*u.num = val;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline int64
|
||||
IVAL64(const char *buf, int pos)
|
||||
{
|
||||
@@ -131,11 +105,6 @@ IVAL64(const char *buf, int pos)
|
||||
return *u.num;
|
||||
}
|
||||
|
||||
#ifdef __clang__
|
||||
__attribute__((no_sanitize("undefined")))
|
||||
#elif GCC_VERSION >= 409
|
||||
__attribute__((no_sanitize_undefined))
|
||||
#endif
|
||||
static inline void
|
||||
SIVAL64(char *buf, int pos, int64 val)
|
||||
{
|
||||
|
||||
95
chmod.c
95
chmod.c
@@ -29,7 +29,7 @@ extern mode_t orig_umask;
|
||||
|
||||
struct chmod_mode_struct {
|
||||
struct chmod_mode_struct *next;
|
||||
int ModeAND, ModeOR;
|
||||
int ModeAND, ModeOR, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP;
|
||||
char flags;
|
||||
};
|
||||
|
||||
@@ -43,6 +43,20 @@ struct chmod_mode_struct {
|
||||
#define STATE_2ND_HALF 2
|
||||
#define STATE_OCTAL_NUM 3
|
||||
|
||||
static int mode_dest_special_bits(int where)
|
||||
{
|
||||
int bits = 0;
|
||||
|
||||
if (where & 0100)
|
||||
bits |= S_ISUID;
|
||||
if (where & 0010)
|
||||
bits |= S_ISGID;
|
||||
if (where & 0001)
|
||||
bits |= S_ISVTX;
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/* Parse a chmod-style argument, and break it down into one or more AND/OR
|
||||
* pairs in a linked list. We return a pointer to new items on success
|
||||
* (appending the items to the specified list), or NULL on error. */
|
||||
@@ -50,13 +64,13 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
struct chmod_mode_struct **root_mode_ptr)
|
||||
{
|
||||
int state = STATE_1ST_HALF;
|
||||
int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0;
|
||||
int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0, copybits = 0;
|
||||
struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL,
|
||||
*prev_mode = NULL;
|
||||
|
||||
while (state != STATE_ERROR) {
|
||||
if (!*modestr || *modestr == ',') {
|
||||
int bits;
|
||||
int bits, where_specified;
|
||||
|
||||
if (!op) {
|
||||
state = STATE_ERROR;
|
||||
@@ -70,9 +84,10 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
first_mode = curr_mode;
|
||||
curr_mode->next = NULL;
|
||||
|
||||
if (where)
|
||||
where_specified = where;
|
||||
if (where) {
|
||||
bits = where * what;
|
||||
else {
|
||||
} else {
|
||||
where = 0111;
|
||||
bits = (where * what) & ~orig_umask;
|
||||
}
|
||||
@@ -81,18 +96,35 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
case CHMOD_ADD:
|
||||
curr_mode->ModeAND = CHMOD_BITS;
|
||||
curr_mode->ModeOR = bits + topoct;
|
||||
curr_mode->ModeCOPY_SRC = copybits;
|
||||
curr_mode->ModeCOPY_DST = where;
|
||||
curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
|
||||
curr_mode->ModeOP = op;
|
||||
break;
|
||||
case CHMOD_SUB:
|
||||
curr_mode->ModeAND = CHMOD_BITS - bits - topoct;
|
||||
curr_mode->ModeOR = 0;
|
||||
curr_mode->ModeCOPY_SRC = copybits;
|
||||
curr_mode->ModeCOPY_DST = where;
|
||||
curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
|
||||
curr_mode->ModeOP = op;
|
||||
break;
|
||||
case CHMOD_EQ:
|
||||
curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0);
|
||||
curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0)
|
||||
- (copybits ? mode_dest_special_bits(where) : 0);
|
||||
curr_mode->ModeOR = bits + topoct;
|
||||
curr_mode->ModeCOPY_SRC = copybits;
|
||||
curr_mode->ModeCOPY_DST = where;
|
||||
curr_mode->ModeCOPY_AND = where_specified ? CHMOD_BITS : ~orig_umask;
|
||||
curr_mode->ModeOP = op;
|
||||
break;
|
||||
case CHMOD_SET:
|
||||
curr_mode->ModeAND = 0;
|
||||
curr_mode->ModeOR = bits;
|
||||
curr_mode->ModeCOPY_SRC = 0;
|
||||
curr_mode->ModeCOPY_DST = 0;
|
||||
curr_mode->ModeCOPY_AND = CHMOD_BITS;
|
||||
curr_mode->ModeOP = op;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,7 +135,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
modestr++;
|
||||
|
||||
state = STATE_1ST_HALF;
|
||||
where = what = op = topoct = topbits = flags = 0;
|
||||
where = what = op = topoct = topbits = flags = copybits = 0;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
@@ -159,26 +191,53 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
case STATE_2ND_HALF:
|
||||
switch (*modestr) {
|
||||
case 'r':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
what |= 4;
|
||||
break;
|
||||
case 'w':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
what |= 2;
|
||||
break;
|
||||
case 'X':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
flags |= FLAG_X_KEEP;
|
||||
/* FALL THROUGH */
|
||||
case 'x':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
what |= 1;
|
||||
break;
|
||||
case 's':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
if (topbits)
|
||||
topoct |= topbits;
|
||||
else
|
||||
topoct = 04000;
|
||||
break;
|
||||
case 't':
|
||||
if (copybits)
|
||||
state = STATE_ERROR;
|
||||
topoct |= 01000;
|
||||
break;
|
||||
case 'u':
|
||||
if (what || topoct || copybits)
|
||||
state = STATE_ERROR;
|
||||
copybits = 0100;
|
||||
break;
|
||||
case 'g':
|
||||
if (what || topoct || copybits)
|
||||
state = STATE_ERROR;
|
||||
copybits = 0010;
|
||||
break;
|
||||
case 'o':
|
||||
if (what || topoct || copybits)
|
||||
state = STATE_ERROR;
|
||||
copybits = 0001;
|
||||
break;
|
||||
default:
|
||||
state = STATE_ERROR;
|
||||
break;
|
||||
@@ -212,6 +271,20 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
return first_mode;
|
||||
}
|
||||
|
||||
static int mode_copy_bits(int mode, int copy_src, int copy_dst, int copy_and)
|
||||
{
|
||||
int copy_bits = 0;
|
||||
|
||||
if (copy_src & 0100)
|
||||
copy_bits |= (mode >> 6) & 7;
|
||||
if (copy_src & 0010)
|
||||
copy_bits |= (mode >> 3) & 7;
|
||||
if (copy_src & 0001)
|
||||
copy_bits |= mode & 7;
|
||||
|
||||
return (copy_dst * copy_bits) & copy_and;
|
||||
}
|
||||
|
||||
|
||||
/* Takes an existing file permission and a list of AND/OR changes, and
|
||||
* create a new permissions. */
|
||||
@@ -219,17 +292,25 @@ int tweak_mode(int mode, struct chmod_mode_struct *chmod_modes)
|
||||
{
|
||||
int IsX = mode & 0111;
|
||||
int NonPerm = mode & ~CHMOD_BITS;
|
||||
int copy_bits;
|
||||
|
||||
for ( ; chmod_modes; chmod_modes = chmod_modes->next) {
|
||||
if ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm))
|
||||
continue;
|
||||
if ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm))
|
||||
continue;
|
||||
copy_bits = mode_copy_bits(mode, chmod_modes->ModeCOPY_SRC,
|
||||
chmod_modes->ModeCOPY_DST,
|
||||
chmod_modes->ModeCOPY_AND);
|
||||
mode &= chmod_modes->ModeAND;
|
||||
if ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm))
|
||||
mode |= chmod_modes->ModeOR & ~0111;
|
||||
else
|
||||
mode |= chmod_modes->ModeOR;
|
||||
if (chmod_modes->ModeOP == CHMOD_SUB)
|
||||
mode &= CHMOD_BITS - copy_bits;
|
||||
else
|
||||
mode |= copy_bits;
|
||||
}
|
||||
|
||||
return mode | NonPerm;
|
||||
|
||||
@@ -351,7 +351,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
|
||||
*/
|
||||
|
||||
#define NON_ZERO_32(x) ((x) ? (x) : (uint32_t)1)
|
||||
#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (uint64_t)(x) << 32 | (y) : (int64)1)
|
||||
#define NON_ZERO_64(x, y) ((x) || (y) ? (y) | (int64)(x) << 32 | (y) : (int64)1)
|
||||
|
||||
uint32_t hashlittle(const void *key, size_t length)
|
||||
{
|
||||
|
||||
@@ -89,8 +89,8 @@ static void copy64(uint32 *M, const uchar *in)
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MD4_DIGEST_LEN; i++) {
|
||||
M[i] = ((uint32)in[i*4+3] << 24) | ((uint32)in[i*4+2] << 16)
|
||||
| ((uint32)in[i*4+1] << 8) | ((uint32)in[i*4+0] << 0);
|
||||
M[i] = (in[i*4+3] << 24) | (in[i*4+2] << 16)
|
||||
| (in[i*4+1] << 8) | (in[i*4+0] << 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
log.c
1
log.c
@@ -22,7 +22,6 @@
|
||||
#include "rsync.h"
|
||||
#include "itypes.h"
|
||||
#include "inums.h"
|
||||
#include "rounding.h" /* EXTRA_ROUNDING, so log_delete() aligns its file_struct */
|
||||
|
||||
extern int dry_run;
|
||||
extern int am_daemon;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
TARGETS := all install install-ssl-daemon install-all install-strip uninstall uninstall-ssl-daemon uninstall-all conf gen reconfigure restatus \
|
||||
TARGETS := all install install-ssl-daemon install-all install-strip conf gen reconfigure restatus \
|
||||
proto man clean cleantests distclean test check check29 check30 installcheck splint \
|
||||
doxygen doxygen-upload finddead rrsync
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ def require_samba_host():
|
||||
def require_top_of_checkout():
|
||||
if not os.path.isfile('packaging/release.py'):
|
||||
die("Run this script from the top of your rsync checkout.")
|
||||
if not os.path.exists('.git'):
|
||||
die("There is no .git in the current directory (run from the top of a git checkout or worktree).")
|
||||
if not os.path.isdir('.git'):
|
||||
die("There is no .git dir in the current directory.")
|
||||
|
||||
|
||||
def replace_or_die(regex, repl, txt, die_msg):
|
||||
@@ -636,8 +636,6 @@ If you have a 'samba' remote configured (git.samba.org:/data/git/rsync.git):
|
||||
|
||||
Then upload the tarball + .asc to the GitHub release for {v_ver},
|
||||
and announce on rsync-announce@, rsync@, and Discord.
|
||||
|
||||
NOTE! Also update the PPAs if needed
|
||||
""")
|
||||
|
||||
|
||||
|
||||
34
receiver.c
34
receiver.c
@@ -423,32 +423,16 @@ 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). */
|
||||
/* A block match can only be honored if we actually mapped the
|
||||
* basis. If we didn't (basis open failed), the sender should
|
||||
* never have been told a basis existed -- treat it as a protocol
|
||||
* inconsistency rather than silently omitting these bytes from
|
||||
* the verification checksum (which yields a spurious failure) or
|
||||
* leaving a hole in the output. */
|
||||
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;
|
||||
rprintf(FERROR, "got a block match with no basis file for %s [%s]\n",
|
||||
full_fname(fname), who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
|
||||
if (DEBUG_GTE(DELTASUM, 3)) {
|
||||
|
||||
@@ -26,58 +26,6 @@ License</A> and is currently being maintained by
|
||||
<img src="badge.svg">
|
||||
</a></div>
|
||||
|
||||
<h3>Rsync version 3.4.4 released</h3>
|
||||
<i class=date>June 8th, 2026</i>
|
||||
|
||||
<p>Rsync version 3.4.4 has been released. This is a regression fix
|
||||
release for the issues that have been reported with the 3.4.3
|
||||
security release. Many thanks to everyone who reported the issues
|
||||
(see <a href="https://download.samba.org/pub/rsync/NEWS#3.4.4">NEWS.md</a>
|
||||
for credits).<p>
|
||||
The 3.4.3 release had so many issues for two main reasons:
|
||||
<ul>
|
||||
<li>the 3.4 testsuite did not have broad enough coverage to
|
||||
catch the regressions notices by users
|
||||
<li>the nature of a security release prevents wide beta testing,
|
||||
resulting in not enough manual testing in disparate environments
|
||||
</ul>
|
||||
To fix this for future releases we have greatly expanded the test
|
||||
suite for 3.5 (currently in master) and grown the development
|
||||
team, especially with more people with security expertise.
|
||||
Thanks for your patience!
|
||||
|
||||
|
||||
<p>See the <a href="https://download.samba.org/pub/rsync/NEWS#3.4.4">3.4.4 NEWS</a> for a detailed changelog.
|
||||
The latest manpages are also available for:<ul>
|
||||
<li><a href="https://download.samba.org/pub/rsync/rsync.1"><b>rsync</b>(1)</a>
|
||||
<li><a href="https://download.samba.org/pub/rsync/rsync-ssl.1"><b>rsync-ssl</b>(1)</a>
|
||||
<li><a href="https://download.samba.org/pub/rsync/rsyncd.conf.5"><b>rsyncd.conf</b>(5)</a>
|
||||
<li><a href="https://download.samba.org/pub/rsync/rrsync.1"><b>rrsync</b>(1)</a>
|
||||
</ul>
|
||||
|
||||
<p>The source tar is available here:
|
||||
<b><a href="https://download.samba.org/pub/rsync/src/rsync-3.4.4.tar.gz">rsync-3.4.4.tar.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src/rsync-3.4.4.tar.gz.asc">signature</a>)</b>,
|
||||
and the diffs from version 3.4.3 are available here:
|
||||
<b><a href="https://download.samba.org/pub/rsync/src-diffs/rsync-3.4.3-3.4.4.diffs.gz">rsync-3.4.3-3.4.4.diffs.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src-diffs/rsync-3.4.3-3.4.4.diffs.gz.asc">signature</a>)</b>.
|
||||
|
||||
<p>Patch sets are also available for the older stable series, for
|
||||
distributors not yet able to move to 3.4.4. Each is GPG signed. The
|
||||
<i>full</i> set applies to a pristine release tarball; the <i>update</i>
|
||||
set has only the patches added since the previous security release:<ul>
|
||||
<li>rsync 3.2.7:
|
||||
<b><a href="https://download.samba.org/pub/rsync/src/rsync-3.2.7-full.tar.gz">rsync-3.2.7-full.tar.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src/rsync-3.2.7-full.tar.gz.asc">signature</a>)</b>,
|
||||
<b><a href="https://download.samba.org/pub/rsync/src/rsync-3.2.7-update.tar.gz">rsync-3.2.7-update.tar.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src/rsync-3.2.7-update.tar.gz.asc">signature</a>)</b>
|
||||
<li>rsync 3.4.1:
|
||||
<b><a href="https://download.samba.org/pub/rsync/src/rsync-3.4.1-full.tar.gz">rsync-3.4.1-full.tar.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src/rsync-3.4.1-full.tar.gz.asc">signature</a>)</b>,
|
||||
<b><a href="https://download.samba.org/pub/rsync/src/rsync-3.4.1-update.tar.gz">rsync-3.4.1-update.tar.gz</a>
|
||||
(<a href="https://download.samba.org/pub/rsync/src/rsync-3.4.1-update.tar.gz.asc">signature</a>)</b>
|
||||
</ul>
|
||||
|
||||
<h3>Rsync version 3.4.3 released</h3>
|
||||
<i class=date>May 20th, 2026</i>
|
||||
|
||||
|
||||
30
rsync.1.md
30
rsync.1.md
@@ -425,9 +425,6 @@ has its own detailed description later in this manpage.
|
||||
--archive, -a archive mode is -rlptgoD (no -A,-X,-U,-N,-H)
|
||||
--no-OPTION turn off an implied OPTION (e.g. --no-D)
|
||||
--recursive, -r recurse into directories
|
||||
--inc-recursive, --i-r enable incremental recursion
|
||||
--no-inc-recursive disable incremental recursion
|
||||
--no-i-r same as --no-inc-recursive
|
||||
--relative, -R use relative path names
|
||||
--no-implied-dirs don't send implied dirs with --relative
|
||||
--backup, -b make backups (see --suffix & --backup-dir)
|
||||
@@ -438,8 +435,7 @@ has its own detailed description later in this manpage.
|
||||
--append append data onto shorter files
|
||||
--append-verify --append w/old data in file checksum
|
||||
--dirs, -d transfer directories without recursing
|
||||
--old-dirs works like --dirs when talking to old rsync
|
||||
--old-d same as --old-dirs
|
||||
--old-dirs, --old-d works like --dirs when talking to old rsync
|
||||
--mkpath create destination's missing path components
|
||||
--links, -l copy symlinks as symlinks
|
||||
--copy-links, -L transform symlink into referent file/dir
|
||||
@@ -473,14 +469,12 @@ has its own detailed description later in this manpage.
|
||||
--preallocate allocate dest files before writing them
|
||||
--dry-run, -n perform a trial run with no changes made
|
||||
--whole-file, -W copy files whole (w/o delta-xfer algorithm)
|
||||
--no-whole-file, --no-W use the delta-xfer algorithm
|
||||
--checksum-choice=STR choose the checksum algorithm (aka --cc)
|
||||
--one-file-system, -x don't cross filesystem boundaries
|
||||
--block-size=SIZE, -B force a fixed checksum block-size
|
||||
--rsh=COMMAND, -e specify the remote shell to use
|
||||
--rsync-path=PROGRAM specify the rsync to run on remote machine
|
||||
--existing skip creating new files on receiver
|
||||
--ignore-non-existing skip creating new files on receiver
|
||||
--ignore-existing skip updating files that exist on receiver
|
||||
--remove-source-files sender removes synchronized files (non-dir)
|
||||
--del an alias for --delete-during
|
||||
@@ -874,7 +868,7 @@ expand it.
|
||||
|
||||
0. `--inc-recursive`, `--i-r`
|
||||
|
||||
This option explicitly enables incremental recursion when scanning for
|
||||
This option explicitly enables on incremental recursion when scanning for
|
||||
files, which is enabled by default when using the [`--recursive`](#opt)
|
||||
option and both sides of the transfer are running rsync 3.0.0 or newer.
|
||||
|
||||
@@ -1150,13 +1144,9 @@ expand it.
|
||||
seen in the listing). Specify `--no-dirs` (or `--no-d`) if you want to
|
||||
turn this off.
|
||||
|
||||
See also the backward-compatibility helper option [`--old-dirs`](#opt).
|
||||
|
||||
0. `--old-dirs`, `--old-d`
|
||||
|
||||
This backward-compatibility helper tells rsync to use a hack of
|
||||
`-r --exclude='/*/*'` to get an older rsync to list a single directory
|
||||
without recursing.
|
||||
There is also a backward-compatibility helper option, `--old-dirs`
|
||||
(`--old-d`) that tells rsync to use a hack of `-r --exclude='/*/*'` to get
|
||||
an older rsync to list a single directory without recursing.
|
||||
|
||||
0. `--mkpath`
|
||||
|
||||
@@ -1523,6 +1513,16 @@ expand it.
|
||||
|
||||
> --chmod=D2775,F664
|
||||
|
||||
Symbolic permission-copy modes are also allowed, such as `g=u`, `o=g` or
|
||||
`g-o`. A permission-copy item may copy from one class only (`u`, `g` or
|
||||
`o`) and cannot be combined with `rwxXst` permission letters in the same
|
||||
item. Use comma-separated items when you need both behaviours, such as
|
||||
`g=o,o=`.
|
||||
|
||||
A permission-copy `=` item also clears the special bit for each destination
|
||||
class it updates (`u` clears setuid, `g` clears setgid, and `o` clears
|
||||
sticky), matching GNU **chmod** behaviour.
|
||||
|
||||
It is also legal to specify multiple `--chmod` options, as each additional
|
||||
option is just appended to the list of changes to make.
|
||||
|
||||
|
||||
14
runtests.py
14
runtests.py
@@ -191,31 +191,35 @@ _PY_TEST_SUFFIX = '_test.py'
|
||||
|
||||
|
||||
def _is_test_path(path):
|
||||
return os.path.basename(path).endswith(_PY_TEST_SUFFIX)
|
||||
base = os.path.basename(path)
|
||||
return base.endswith('.test') or base.endswith(_PY_TEST_SUFFIX)
|
||||
|
||||
|
||||
def _testbase(path):
|
||||
"""Strip the test extension to get the canonical test name."""
|
||||
base = os.path.basename(path)
|
||||
if base.endswith('.test'):
|
||||
return base[:-len('.test')]
|
||||
if base.endswith(_PY_TEST_SUFFIX):
|
||||
return base[:-len(_PY_TEST_SUFFIX)]
|
||||
return base
|
||||
|
||||
|
||||
def collect_tests(suitedir, patterns):
|
||||
"""Collect test scripts (_test.py) matching the given patterns."""
|
||||
"""Collect test scripts (.test or _test.py) matching the given patterns."""
|
||||
if not patterns:
|
||||
candidates = glob.glob(os.path.join(suitedir, '*' + _PY_TEST_SUFFIX))
|
||||
candidates = (glob.glob(os.path.join(suitedir, '*.test'))
|
||||
+ glob.glob(os.path.join(suitedir, '*' + _PY_TEST_SUFFIX)))
|
||||
tests = sorted(p for p in candidates if _is_test_path(p))
|
||||
else:
|
||||
seen = set()
|
||||
tests = []
|
||||
for pat in patterns:
|
||||
# Accept either bare name ("mkpath"), explicit extension, or glob.
|
||||
if pat.endswith('.py'):
|
||||
if pat.endswith('.test') or pat.endswith('.py'):
|
||||
pats = [pat]
|
||||
else:
|
||||
pats = [pat + _PY_TEST_SUFFIX]
|
||||
pats = [pat + '.test', pat + _PY_TEST_SUFFIX]
|
||||
for p in pats:
|
||||
for m in sorted(glob.glob(os.path.join(suitedir, p))):
|
||||
if _is_test_path(m) and m not in seen:
|
||||
|
||||
@@ -11,8 +11,8 @@ import shutil
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, SCRATCHDIR, TODIR,
|
||||
build_rsyncd_conf, checkit, makepath, rmtree,
|
||||
run_rsync, start_test_daemon,
|
||||
build_rsyncd_conf, check_perms, checkit, makepath, rmtree,
|
||||
run_rsync, start_test_daemon, test_fail,
|
||||
)
|
||||
|
||||
|
||||
@@ -62,6 +62,37 @@ for d in (checkdir, checkdir / 'dir1', checkdir / 'dir2'):
|
||||
checkit(['-avv', '--chmod', 'ug-s,a+rX,D+w', f'{FROMDIR}/', f'{TODIR}/'],
|
||||
checkdir, TODIR)
|
||||
|
||||
def check_permcopy(chmod_arg, start_mode, expected, is_dir=False):
|
||||
rmtree(FROMDIR)
|
||||
rmtree(TODIR)
|
||||
makepath(FROMDIR)
|
||||
permcopy = FROMDIR / 'permcopy'
|
||||
if is_dir:
|
||||
permcopy.mkdir()
|
||||
else:
|
||||
permcopy.write_text('permcopy\n')
|
||||
os.chmod(permcopy, start_mode)
|
||||
run_rsync('-avv', f'--chmod={chmod_arg}', f'{FROMDIR}/', f'{TODIR}/')
|
||||
check_perms(TODIR / 'permcopy', expected)
|
||||
|
||||
|
||||
# Exercise chmod(1)-style permission copies.
|
||||
check_permcopy('g=o,o=', 0o647, 'rw-rwx---')
|
||||
check_permcopy('g=u', 0o741, 'rwxrwx--x')
|
||||
check_permcopy('g-o', 0o775, 'rwx-w-r-x')
|
||||
check_permcopy('u=g', 0o4755, 'r-xr-xr-x')
|
||||
check_permcopy('g=u', 0o2755, 'rwxrwxr-x')
|
||||
check_permcopy('o=u', 0o1750, 'rwxr-xrwx', is_dir=True)
|
||||
|
||||
rmtree(FROMDIR)
|
||||
rmtree(TODIR)
|
||||
makepath(FROMDIR)
|
||||
(FROMDIR / 'permcopy').write_text('permcopy\n')
|
||||
proc = run_rsync('-avv', '--chmod=g=ur', f'{FROMDIR}/', f'{TODIR}/',
|
||||
check=False, capture_output=True)
|
||||
if proc.returncode == 0:
|
||||
test_fail('--chmod=g=ur was not rejected')
|
||||
|
||||
# Now exercise the F-only chmod path.
|
||||
rmtree(FROMDIR)
|
||||
rmtree(checkdir)
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
"this target mirrors), configure_flags. Optional (with defaults): make (\"make\"),",
|
||||
"python (\"python3\"), rsync_bin (\"rsync\"; \"rsync.exe\" on Cygwin), privilege",
|
||||
"(\"root\" | \"sudo\" | \"user\"), pipe_jobs/tcp_jobs (8), builddir (\"rsync-citest\",",
|
||||
"relative to the remote $HOME), env_prefix, configure_pre, nonroot, protocols,",
|
||||
"max_retry.",
|
||||
"relative to the remote $HOME), env_prefix, configure_pre, nonroot, protocols.",
|
||||
"",
|
||||
"nonroot: true reruns -- as the non-root ssh user, after the sudo runs -- the",
|
||||
"tests that declare `fleet_nonroot = True` at module level (so the set is",
|
||||
@@ -20,14 +19,8 @@
|
||||
"",
|
||||
"protocols: [30, 29] adds one extra stdio-pipe test pass per listed version,",
|
||||
"each run with runtests --protocol=N (the fleet analogue of a workflow's",
|
||||
"check30/check29 steps) and shown as a protoNN column.",
|
||||
"",
|
||||
"max_retry: N (default 0) re-runs each failed test on its own up to N more",
|
||||
"times and drops any that then pass (listed under RECOVERED, not hidden). Use",
|
||||
"on a slow/loaded box where concurrency-sensitive tests occasionally flake,",
|
||||
"instead of dropping the whole target to a lower pipe_jobs/tcp_jobs.",
|
||||
"",
|
||||
"Keys starting with \"_\" are comments. See testsuite/README.md."
|
||||
"check30/check29 steps) and shown as a protoNN column. Keys starting with",
|
||||
"\"_\" are comments. See testsuite/README.md."
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
@@ -47,13 +40,12 @@
|
||||
"--disable-xxhash", "--disable-lz4"]
|
||||
},
|
||||
{
|
||||
"_comment": "Nested-VM OpenBSD occasionally flakes a daemon/tcp test under load; max_retry re-runs just the failed test rather than throttling the whole box (tcp_jobs/pipe_jobs are still available if you prefer that).",
|
||||
"name": "openbsd",
|
||||
"ssh_host": "root@openbsd",
|
||||
"workflow": "openbsd-build.yml",
|
||||
"make": "gmake",
|
||||
"configure_pre": "export AUTOCONF_VERSION=2.71 AUTOMAKE_VERSION=1.16;",
|
||||
"max_retry": 2,
|
||||
"tcp_jobs": 2,
|
||||
"configure_flags": ["--with-rrsync", "--disable-zstd", "--disable-md2man",
|
||||
"--disable-xxhash", "--disable-lz4"]
|
||||
},
|
||||
|
||||
@@ -31,11 +31,8 @@ without interfering: each pushes, builds and tests in isolation. The run dir is
|
||||
removed when the run ends -- on success or failure, and best-effort on
|
||||
Ctrl-C/kill (pass --keep to retain it for inspection). A run that is hard-killed
|
||||
(SIGKILL), or signalled mid-push, or whose ssh dies during cleanup can leave a
|
||||
stray <builddir>-<id> behind -- plus an orphaned path-flipper or test rsyncd on
|
||||
platforms without a parent-death backstop; sweep all of those (root-owned files
|
||||
included, via sudo -n) with `fleettest.py --cleanup` (optionally scoped with
|
||||
--targets). Run --cleanup between runs, not during one: its process kills are
|
||||
host-global and would also catch a concurrent run's flipper/daemon. Because each
|
||||
stray <builddir>-<id> behind; sweep those with `fleettest.py --cleanup`
|
||||
(optionally scoped with --targets). Because each
|
||||
run starts from a fresh dir, every build is a full configure + build.
|
||||
|
||||
PROVISIONING: each target must have the build toolchain its workflow's prepare
|
||||
@@ -86,11 +83,7 @@ from pathlib import Path
|
||||
# source tree these point at, so it must be run from inside an rsync checkout
|
||||
# or given --repo PATH.
|
||||
REPO = Path.cwd()
|
||||
# Source tree providing the test suite (runtests.py + testsuite/). Defaults to
|
||||
# REPO; --testsuite-repo decouples it so one tree is built and another's suite is
|
||||
# run against the result.
|
||||
TESTSUITE_REPO = REPO
|
||||
WORKFLOWS = TESTSUITE_REPO / ".github" / "workflows"
|
||||
WORKFLOWS = REPO / ".github" / "workflows"
|
||||
|
||||
# Fleet config (overridable with --fleet): ~/.fleettest.json is tried first, then
|
||||
# fleettest.json next to this script. The example template sits next to the
|
||||
@@ -144,12 +137,6 @@ class Target:
|
||||
# stdio-pipe pass with runtests --protocol=N (the fleet analogue of a
|
||||
# workflow's check30/check29 steps). e.g. [30, 29]. Empty => proto pass off.
|
||||
protocols: list[int] = dataclasses.field(default_factory=list)
|
||||
# Per-target retry budget for FLAKY tests: after a run, each failed test is
|
||||
# re-run on its own up to max_retry more times, and any that then pass are
|
||||
# dropped from the failure list (and reported as "recovered", never hidden).
|
||||
# Use on a slow/loaded box where concurrency-sensitive tests occasionally
|
||||
# flake, instead of dropping the whole target to a lower -j. 0 => no retry.
|
||||
max_retry: int = 0
|
||||
|
||||
|
||||
def load_fleet(path: Path) -> list[Target]:
|
||||
@@ -296,7 +283,7 @@ def build_script(t: Target) -> str:
|
||||
|
||||
|
||||
def test_script(t: Target, transport: str, skip_csv: str | None, jobs: int,
|
||||
protocol: int | None = None, only: list[str] | None = None) -> str:
|
||||
protocol: int | None = None) -> str:
|
||||
rb = f'--rsync-bin="$PWD/{t.rsync_bin}"'
|
||||
tcp = " --use-tcp" if transport == "tcp" else ""
|
||||
# protocol forces an older wire version (mirrors `make check30`/`check29`).
|
||||
@@ -304,14 +291,9 @@ def test_script(t: Target, transport: str, skip_csv: str | None, jobs: int,
|
||||
# PYTHONDONTWRITEBYTECODE: don't drop root-owned __pycache__/*.pyc into the
|
||||
# tree (a sudo run would, breaking the next non-root push --delete).
|
||||
env = "PYTHONDONTWRITEBYTECODE=1 "
|
||||
# Named tests (a max_retry re-run) make runtests full_run False, so the
|
||||
# expected-skip list does not apply -- only the named tests' pass/fail matter.
|
||||
names = ""
|
||||
if only:
|
||||
names = " " + " ".join(only)
|
||||
elif skip_csv:
|
||||
if skip_csv:
|
||||
env += f"RSYNC_EXPECT_SKIPPED={skip_csv} "
|
||||
runtests = f'{t.python} runtests.py {rb}{tcp}{proto} -j {jobs}{names}'
|
||||
runtests = f'{t.python} runtests.py {rb}{tcp}{proto} -j {jobs}'
|
||||
# env_prefix (e.g. a brew PATH) must reach the test too: some tests build a
|
||||
# helper binary on the fly (a test may invoke `make`, which needs gawk etc.),
|
||||
# so the build tools must be on PATH at test time.
|
||||
@@ -367,10 +349,6 @@ class TransportResult:
|
||||
skip_expected: set[str]
|
||||
skip_got: set[str]
|
||||
raw: str
|
||||
# Tests that failed the initial run but passed on a max_retry re-run, so they
|
||||
# were dropped from `failed`. Surfaced in the report (a recovered flake is
|
||||
# noted, never silently hidden).
|
||||
recovered: list[str] = dataclasses.field(default_factory=list)
|
||||
|
||||
@property
|
||||
def skip_mismatch(self) -> bool:
|
||||
@@ -398,35 +376,6 @@ def parse_transport(transport: str, r: CmdResult, skip_checked: bool) -> Transpo
|
||||
skip_checked, exp, got, r.out)
|
||||
|
||||
|
||||
def retry_failed(t: Target, label: str, tr: TransportResult, rerun) -> None:
|
||||
"""Honour the target's max_retry budget: re-run each failed test on its own
|
||||
(serially) up to max_retry more times; drop any that pass and record them in
|
||||
tr.recovered. `rerun(names)` runs the given tests and returns a CmdResult.
|
||||
A no-op when max_retry is 0 or there were no failures."""
|
||||
if not t.max_retry or not tr.failed:
|
||||
return
|
||||
remaining = list(tr.failed)
|
||||
for attempt in range(1, t.max_retry + 1):
|
||||
r = rerun(remaining)
|
||||
still = [m.group(2) for m in RE_RESULT.finditer(r.out)
|
||||
if m.group(1) in ("FAIL", "ERROR")]
|
||||
recovered = [n for n in remaining if n not in still]
|
||||
if recovered:
|
||||
tr.recovered.extend(recovered)
|
||||
log(f"[{t.name}] {label} retry {attempt}/{t.max_retry}: "
|
||||
f"recovered {','.join(recovered)}"
|
||||
+ (f"; still failing {','.join(still)}" if still else ""))
|
||||
remaining = [n for n in remaining if n in still]
|
||||
if not remaining:
|
||||
break
|
||||
tr.failed = remaining
|
||||
# The initial run's non-zero exit was the now-recovered failures; once they
|
||||
# all pass on retry the cell is OK, so clear the stale exit code (only the
|
||||
# failed tests can make runtests exit non-zero on a no-skip-list re-run).
|
||||
if not remaining and tr.recovered and tr.exit_code != 0:
|
||||
tr.exit_code = 0
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TargetResult:
|
||||
target: str
|
||||
@@ -495,12 +444,9 @@ def run_target(t: Target, args, staging: str) -> TargetResult:
|
||||
t0 = time.monotonic()
|
||||
r = run_on(t, cmd, timeout=2400)
|
||||
res.timings[transport] = time.monotonic() - t0
|
||||
tr = parse_transport(transport, r, skip_csv is not None)
|
||||
retry_failed(t, transport, tr, lambda names, tp=transport: run_on(
|
||||
t, test_script(t, tp, None, 1, only=names), timeout=1200))
|
||||
res.transports[transport] = tr
|
||||
res.transports[transport] = parse_transport(transport, r, skip_csv is not None)
|
||||
log(f"[{t.name}] {transport} done "
|
||||
f"({'ok' if tr.ok else 'ISSUE'})")
|
||||
f"({'ok' if res.transports[transport].ok else 'ISSUE'})")
|
||||
|
||||
# Extra older-protocol passes (mirroring the workflow's check30/check29
|
||||
# steps): same stdio-pipe transport and skip list as `make check`, but with
|
||||
@@ -515,13 +461,9 @@ def run_target(t: Target, args, staging: str) -> TargetResult:
|
||||
t0 = time.monotonic()
|
||||
r = run_on(t, cmd, timeout=2400)
|
||||
res.timings[label] = time.monotonic() - t0
|
||||
tr = parse_transport(label, r, skip_csv is not None)
|
||||
retry_failed(t, label, tr, lambda names, pr=proto: run_on(
|
||||
t, test_script(t, "pipe", None, 1, protocol=pr, only=names),
|
||||
timeout=1200))
|
||||
res.transports[label] = tr
|
||||
res.transports[label] = parse_transport(label, r, skip_csv is not None)
|
||||
log(f"[{t.name}] {label} done "
|
||||
f"({'ok' if tr.ok else 'ISSUE'})")
|
||||
f"({'ok' if res.transports[label].ok else 'ISSUE'})")
|
||||
|
||||
# Extra non-root pass (after the sudo runs) for targets that opt in, running
|
||||
# the tests that declare `fleet_nonroot = True` (discovered in main()).
|
||||
@@ -529,12 +471,9 @@ def run_target(t: Target, args, staging: str) -> TargetResult:
|
||||
t0 = time.monotonic()
|
||||
r = run_on(t, nonroot_test_script(t, args.nonroot_tests), timeout=2400)
|
||||
res.timings["nonroot"] = time.monotonic() - t0
|
||||
tr = parse_transport("nonroot", r, skip_checked=False)
|
||||
retry_failed(t, "nonroot", tr, lambda names: run_on(
|
||||
t, nonroot_test_script(t, names), timeout=1200))
|
||||
res.transports["nonroot"] = tr
|
||||
res.transports["nonroot"] = parse_transport("nonroot", r, skip_checked=False)
|
||||
log(f"[{t.name}] nonroot done "
|
||||
f"({'ok' if tr.ok else 'ISSUE'})")
|
||||
f"({'ok' if res.transports['nonroot'].ok else 'ISSUE'})")
|
||||
res.timings["total"] = time.monotonic() - started
|
||||
return res
|
||||
|
||||
@@ -659,17 +598,6 @@ def print_report(results: list[TargetResult], args, fleet: list[Target]) -> bool
|
||||
for d in details:
|
||||
print(d)
|
||||
print("=" * 64)
|
||||
# Recovered flakes: tests that failed but passed within the target's
|
||||
# max_retry budget. The cell counts as OK, but list them so a flaky test is
|
||||
# never silently swallowed.
|
||||
recovered = [f"{res.target} / {transport}: {','.join(tr.recovered)}"
|
||||
for res in results for transport in transports
|
||||
if (tr := res.transports.get(transport)) and tr.recovered]
|
||||
if recovered:
|
||||
print("==== RECOVERED (flaky -- failed, then passed on retry) ====")
|
||||
for r in recovered:
|
||||
print(f" {r}")
|
||||
print("=" * 64)
|
||||
print(f"{len(results)} targets x {len(transports)} transports = {cells} cells: "
|
||||
f"{ok_cells} OK, {cells - ok_cells} not OK")
|
||||
return all_ok
|
||||
@@ -774,94 +702,33 @@ def _on_signal(signum, frame):
|
||||
os._exit(130 if signum == signal.SIGINT else 143)
|
||||
|
||||
|
||||
# sweep() counts a pattern, kills it (best effort; sudo -n retry for processes a
|
||||
# root-running test left), then RE-counts after a settle so we report what
|
||||
# actually died (KILLED = before-after) and flag any survivor (SURVIVED, sets
|
||||
# fail) instead of claiming success when pkill couldn't reach it. The patterns
|
||||
# use the pgrep self-exclusion trick -- 'r[e]name'/'det[a]ch' match a real
|
||||
# process's "rename"/"detach" but not the bracketed literal in this script's own
|
||||
# argv (run_on passes the whole script as the remote argv), so we never signal
|
||||
# ourselves. @BASE@ is substituted with the target's run-dir prefix.
|
||||
_CLEANUP_SCRIPT = r'''fail=0
|
||||
sweep() {
|
||||
command -v pgrep >/dev/null 2>&1 || return 0
|
||||
before=$(pgrep -f "$2" 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "$before" -gt 0 ] 2>/dev/null || return 0
|
||||
pkill -f "$2" 2>/dev/null
|
||||
sudo -n pkill -f "$2" 2>/dev/null
|
||||
sleep 1
|
||||
after=$(pgrep -f "$2" 2>/dev/null | wc -l | tr -d ' ')
|
||||
killed=$((before - after))
|
||||
[ "$killed" -gt 0 ] 2>/dev/null && echo "KILLED $killed stray $1(s)"
|
||||
if [ "$after" -gt 0 ] 2>/dev/null; then
|
||||
echo "SURVIVED $after stray $1(s)"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
sweep flipper 'r[e]name.*r[e]name.*r[e]name'
|
||||
sweep daemon 'det[a]ch --address=127.0.0.1'
|
||||
for d in @BASE@-*; do
|
||||
[ -e "$d" ] || continue
|
||||
if rm -rf -- "$d" 2>/dev/null || sudo -n rm -rf -- "$d" 2>/dev/null; then
|
||||
echo "REMOVED $d"
|
||||
else
|
||||
echo "FAILED $d"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
'''
|
||||
|
||||
|
||||
def cleanup_remnants(targets: list[Target]) -> int:
|
||||
"""--cleanup mode: on each target, kill the stray processes a killed run can
|
||||
leave behind, then remove every <base>-* run dir, reporting what went.
|
||||
Returns a process exit code. Only suffixed run dirs are swept -- a bare
|
||||
<base> is left alone.
|
||||
|
||||
A run that is SIGKILLed (or whose ssh drops) can strand two kinds of process
|
||||
on platforms without a parent-death backstop: the TOCTOU path-flipper (a
|
||||
busy `python -c` rename loop that pins a CPU) and an orphaned test rsyncd
|
||||
(`--no-detach --address=127.0.0.1`, which then squats its fixed port -- the
|
||||
very wedge claim_ports()' bind-probe now reports). Both are killed best
|
||||
effort (sudo -n retry for root-owned ones); a kill is verified by re-counting
|
||||
afterwards, and a process that survives is reported and fails the run.
|
||||
|
||||
CAVEAT: the kill patterns are host-global, not scoped to a particular run, so
|
||||
--cleanup assumes no *other* fleettest run is active on the target -- it
|
||||
would also kill a concurrent run's flipper/daemon (and any manual `rsync
|
||||
--daemon --no-detach --address=127.0.0.1`). Run it between runs, not during
|
||||
one. Run dirs whose contents a root test owns are removed via a `sudo -n rm`
|
||||
fallback; only a dir that survives even that is a failure."""
|
||||
"""--cleanup mode: remove every <base>-* run dir on each target, reporting
|
||||
what each removed. Returns a process exit code. Only suffixed run dirs are
|
||||
swept -- a bare <base> is left alone."""
|
||||
rc = 0
|
||||
for t in targets:
|
||||
base = t.builddir
|
||||
if _unsafe_builddir(base):
|
||||
log(f"[{t.name}] skipped (unsafe builddir {base!r})")
|
||||
continue
|
||||
# Structured markers (KILLED/SURVIVED/REMOVED/FAILED) keep the report
|
||||
# clean even though run_on() folds stderr into stdout.
|
||||
r = run_on(t, _CLEANUP_SCRIPT.replace("@BASE@", base), timeout=120)
|
||||
lines = r.out.splitlines()
|
||||
removed = [ln.split(" ", 1)[1] for ln in lines if ln.startswith("REMOVED ")]
|
||||
failed = [ln.split(" ", 1)[1] for ln in lines if ln.startswith("FAILED ")]
|
||||
killed = [ln.replace("KILLED ", "killed ", 1)
|
||||
for ln in lines if ln.startswith("KILLED ")]
|
||||
survived = [ln.replace("SURVIVED ", "still alive: ", 1)
|
||||
for ln in lines if ln.startswith("SURVIVED ")]
|
||||
msgs = killed[:]
|
||||
if removed:
|
||||
msgs.append("removed: " + " ".join(removed))
|
||||
if survived:
|
||||
# Echo each match before removing it so the harness can report what
|
||||
# went; an unmatched glob stays literal and is skipped by the -e test.
|
||||
script = (f'set -e\n'
|
||||
f'for d in {base}-*; do\n'
|
||||
f' [ -e "$d" ] || continue\n'
|
||||
f' echo "$d"\n'
|
||||
f' rm -rf -- "$d"\n'
|
||||
f'done\n')
|
||||
r = run_on(t, script, timeout=120)
|
||||
removed = [ln for ln in r.out.splitlines() if ln.strip()]
|
||||
if r.rc != 0:
|
||||
rc = 1
|
||||
msgs += survived
|
||||
if failed:
|
||||
rc = 1
|
||||
msgs.append("could not remove (even with sudo): " + " ".join(failed))
|
||||
if r.rc not in (0, 1):
|
||||
rc = 1
|
||||
msgs.append(f"cleanup error rc={r.rc}: {r.out.strip()[:160]}")
|
||||
log(f"[{t.name}] " + ("; ".join(msgs) if msgs else "nothing to remove"))
|
||||
log(f"[{t.name}] cleanup error (rc={r.rc}): {r.out.strip()[:200]}")
|
||||
elif removed:
|
||||
log(f"[{t.name}] removed: {' '.join(removed)}")
|
||||
else:
|
||||
log(f"[{t.name}] nothing to remove")
|
||||
return rc
|
||||
|
||||
|
||||
@@ -877,44 +744,24 @@ def main() -> int:
|
||||
ap.add_argument("--keep", action="store_true",
|
||||
help="keep each run's build dir (default: remove it at exit)")
|
||||
ap.add_argument("--cleanup", action="store_true",
|
||||
help="kill stray flippers/test daemons and remove stray "
|
||||
"<builddir>-* run dirs (root-owned via sudo -n) on the "
|
||||
"targets, then exit; run between runs, not during one "
|
||||
"(kills are host-global)")
|
||||
help="remove stray <builddir>-* run dirs on the targets, then exit")
|
||||
ap.add_argument("--jobs", type=int, help="override -j for both transports")
|
||||
ap.add_argument("--timing", action="store_true",
|
||||
help="report per-target wall-clock (push/build/test) to find "
|
||||
"the slowest target")
|
||||
ap.add_argument("--repo", help="rsync source tree to build (default: cwd)")
|
||||
ap.add_argument("--testsuite-repo",
|
||||
help="rsync tree to take runtests.py + testsuite/ from "
|
||||
"(default: --repo). Build one tree and run another's test "
|
||||
"suite against it, e.g. --repo ../rsync-v3.4 --testsuite-repo .")
|
||||
ap.add_argument("--fleet", help="fleet config JSON (default: ~/.fleettest.json, "
|
||||
"else fleettest.json next to this script)")
|
||||
ap.add_argument("--list", action="store_true", help="list targets and exit")
|
||||
args = ap.parse_args()
|
||||
|
||||
global REPO, WORKFLOWS, TESTSUITE_REPO
|
||||
global REPO, WORKFLOWS
|
||||
REPO = Path(args.repo).resolve() if args.repo else Path.cwd()
|
||||
TESTSUITE_REPO = Path(args.testsuite_repo).resolve() if args.testsuite_repo else REPO
|
||||
# The expected-skip lists travel with the suite, so read workflows from the
|
||||
# tree that provides the tests.
|
||||
WORKFLOWS = TESTSUITE_REPO / ".github" / "workflows"
|
||||
if not args.cleanup:
|
||||
# The Python test suite (runtests.py + testsuite/) comes from
|
||||
# TESTSUITE_REPO, so that is where runtests.py must live. The build tree
|
||||
# (REPO) only has to be a buildable rsync source -- it may be an older
|
||||
# release whose runtests.py predates the Python suite, or lacks it.
|
||||
if not (TESTSUITE_REPO / "runtests.py").is_file():
|
||||
print(f"{TESTSUITE_REPO} has no runtests.py; run from inside a "
|
||||
f"checkout or pass --testsuite-repo a tree with the Python "
|
||||
f"test suite", file=sys.stderr)
|
||||
return 2
|
||||
if not (REPO / "rsync.h").is_file():
|
||||
print(f"{REPO} is not an rsync source tree (no rsync.h); "
|
||||
f"run from inside a checkout or pass --repo", file=sys.stderr)
|
||||
return 2
|
||||
WORKFLOWS = REPO / ".github" / "workflows"
|
||||
if not args.cleanup and not (REPO / "runtests.py").is_file():
|
||||
print(f"{REPO} is not an rsync source tree (no runtests.py); "
|
||||
f"run from inside a checkout or pass --repo", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.fleet:
|
||||
config_path = Path(args.fleet).resolve()
|
||||
@@ -993,19 +840,6 @@ def main() -> int:
|
||||
print(f"git archive failed: {ar.stderr}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# --testsuite-repo: overlay another tree's runtests.py + testsuite/ onto
|
||||
# the built source (merge, no delete). Build REPO's rsync, but run
|
||||
# TESTSUITE_REPO's suite against it. The leftover .test files from REPO
|
||||
# are ignored by a Python runtests.py (it globs *_test.py).
|
||||
if TESTSUITE_REPO != REPO:
|
||||
ov = subprocess.run(
|
||||
f"git -C {TESTSUITE_REPO} archive HEAD -- runtests.py testsuite "
|
||||
f"| tar -x -C {staging}",
|
||||
shell=True, capture_output=True, text=True)
|
||||
if ov.returncode != 0:
|
||||
print(f"testsuite overlay archive failed: {ov.stderr}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Tests that opt into the non-root pass (same for every target).
|
||||
args.nonroot_tests = discover_nonroot_tests(Path(staging) / "testsuite")
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Regression test for a receiver NULL-deref on the delta DISCARD path.
|
||||
#
|
||||
# In receiver.c receive_data(), a block-MATCH token that arrives while the
|
||||
# receiver is DISCARDING a file (discard_receive_data() -> receive_data() with
|
||||
# fname==NULL, fd==-1, hence mapbuf==NULL) reached
|
||||
# rprintf(FERROR, "...%s...", full_fname(fname), ...)
|
||||
# with fname==NULL. full_fname() dereferences its argument unconditionally
|
||||
# (util1.c: `if (*fn == '/')`), so the receiver SIGSEGVs. The faulty error
|
||||
# branch was added in 31fbb17d ("receiver: fix absolute --partial-dir delta
|
||||
# resume"); the fix discriminates on fd (not mapbuf) and, on the discard path
|
||||
# (fd==-1), absorbs the matched bytes benignly instead of erroring.
|
||||
#
|
||||
# This is a NORMAL-operation crash, not adversarial: a stock cooperating sender
|
||||
# triggers it. The generator sends real block sums (basis readable, delta mode);
|
||||
# the receiver then has to discard because its output mkstemp() fails -- here
|
||||
# because the destination directory is not writable. A block MATCH against the
|
||||
# shared leading block reaches the discard path and crashes the pre-fix binary.
|
||||
#
|
||||
# We drive a real sender<->receiver pair (client sender -> daemon receiver) so
|
||||
# the receiver actually takes the recv_files discard path; a local `rsync a b`
|
||||
# does not. In the default (pipe) daemon transport both ends are the binary
|
||||
# under test.
|
||||
#
|
||||
# Skipped (exit 77) when running as root (root bypasses DAC), or when the
|
||||
# directory mode is not enforced (e.g. a non-root process holding
|
||||
# CAP_DAC_OVERRIDE in an unprivileged container): in both cases the receiver's
|
||||
# mkstemp() would succeed despite chmod 0555, the discard path would not be
|
||||
# taken, and the test would silently pass against a buggy binary. The
|
||||
# post-chmod writability probe converts that silent false-pass into an honest
|
||||
# skip and subsumes the root check.
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, RSYNC, TMPDIR,
|
||||
get_testuid, get_rootuid, makepath, start_test_daemon, write_daemon_conf,
|
||||
test_fail, test_skipped,
|
||||
)
|
||||
|
||||
DAEMON_PORT = 12895
|
||||
|
||||
if get_testuid() == get_rootuid():
|
||||
test_skipped("root bypasses DAC: the unwritable dest dir wouldn't make "
|
||||
"the receiver's mkstemp fail, so the discard path (and the "
|
||||
"bug) is never reached")
|
||||
|
||||
os.chdir(TMPDIR)
|
||||
|
||||
MODDIR = SCRATCHDIR / 'recvdiscard-mod' # daemon module root (writable)
|
||||
BASISDIR = MODDIR / 'd' # made read-only -> mkstemp fails
|
||||
SRCDIR_ = SCRATCHDIR / 'recvdiscard-src' # client source tree
|
||||
makepath(MODDIR, BASISDIR, SRCDIR_)
|
||||
|
||||
# Basis and source share a leading block (2000 'A's) so the generator emits
|
||||
# real sums and the receiver gets a block MATCH; the tails differ and the
|
||||
# source is larger so a delta (not a no-op) is sent.
|
||||
basis = BASISDIR / 'f'
|
||||
basis.write_bytes(b'A' * 2000 + b'C' * 1000)
|
||||
src = SRCDIR_ / 'f'
|
||||
src.write_bytes(b'A' * 2000 + b'B' * 3000)
|
||||
|
||||
# A read/write daemon module rooted at MODDIR.
|
||||
conf = write_daemon_conf([('recvdiscard', {'path': str(MODDIR),
|
||||
'read only': 'no'})])
|
||||
url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)
|
||||
|
||||
# Make the destination directory unwritable so the receiver's output mkstemp()
|
||||
# fails and it falls back to discarding the delta stream. Restore in finally so
|
||||
# the per-test scratch tree can be cleaned up.
|
||||
os.chmod(BASISDIR, 0o555)
|
||||
|
||||
# Probe that the chmod actually denies writes for *this* process. A non-root
|
||||
# user holding CAP_DAC_OVERRIDE bypasses the directory write bit, so mkstemp
|
||||
# would succeed in the daemon receiver too, the discard path would never be
|
||||
# taken, and the test would silently pass on a buggy binary. Better to skip
|
||||
# explicitly. (Root takes this path too: its probe succeeds → skip, which
|
||||
# subsumes the uid==0 check.)
|
||||
try:
|
||||
_fd, _probe = tempfile.mkstemp(dir=BASISDIR)
|
||||
os.close(_fd)
|
||||
os.unlink(_probe)
|
||||
os.chmod(BASISDIR, 0o755)
|
||||
test_skipped("destination dir is writable despite chmod 0555 "
|
||||
"(CAP_DAC_OVERRIDE?); cannot force the receiver discard path")
|
||||
except OSError:
|
||||
pass # EACCES -- good, the precondition is enforced
|
||||
|
||||
try:
|
||||
argv = shlex.split(RSYNC) + [
|
||||
'--no-whole-file', '-a',
|
||||
str(src), f'{url}recvdiscard/d/f',
|
||||
]
|
||||
print('Running:', ' '.join(argv))
|
||||
proc = subprocess.run(argv, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True)
|
||||
print(proc.stdout, end='')
|
||||
finally:
|
||||
os.chmod(BASISDIR, 0o755)
|
||||
|
||||
rc = proc.returncode
|
||||
|
||||
# A receiver SIGSEGV manifests to the client as a protocol error (the daemon's
|
||||
# receiver child crashes mid-stream and the connection drops): exit code 12.
|
||||
# With the fix the receiver drains the delta and, because the forced-unwritable
|
||||
# destination leaves the file untransferred, the run reports the benign "some
|
||||
# files were not transferred" -- exit code 23.
|
||||
#
|
||||
# 23 is the ONLY non-crash outcome here: the writability probe above guarantees
|
||||
# the receiver's mkstemp() fails, so the file is always discarded. An exit 0
|
||||
# would mean the file actually transferred -- the discard path was NOT exercised
|
||||
# and the run proves nothing -- so require exactly 23 (and call out 12 as the
|
||||
# pre-fix crash).
|
||||
if rc == 12:
|
||||
test_fail(f"receiver crashed on the discard path (rsync exited {rc}: "
|
||||
"error in rsync protocol data stream -- the receiver child "
|
||||
"SIGSEGV'd in full_fname(NULL))")
|
||||
if rc != 23:
|
||||
test_fail(f"expected rsync exit 23 (the forced discard leaves the file "
|
||||
f"untransferred); got {rc} -- the discard path was not exercised, "
|
||||
"so this run validates nothing (12 would be the pre-fix crash)")
|
||||
|
||||
print(f"OK: receiver discarded the delta without crashing (rsync exit {rc})")
|
||||
@@ -175,42 +175,6 @@ def _open_lock_file() -> int:
|
||||
return fd
|
||||
|
||||
|
||||
def _probe_bindable(port: int) -> 'None':
|
||||
"""Confirm `port` is actually free once we hold its claim_ports() lock.
|
||||
|
||||
The byte-range lock only coordinates *live* test drivers, and the kernel
|
||||
releases it the instant the holding process dies -- even if that driver left
|
||||
an orphaned daemon still bound to the port. That happens when a run is
|
||||
SIGKILLed (or its ssh drops) on a platform with no parent-death backstop:
|
||||
rsyncfns only arms PR_SET_PDEATHSIG, which is Linux-only, so on the
|
||||
BSDs/Solaris/macOS a killed fleettest run can strand its rsyncd, which then
|
||||
squats the fixed test port forever. A later run wins the (now-free) lock but
|
||||
the socket is still taken, and the daemon dies with a cryptic "bind() failed:
|
||||
Address already in use" / the client "did not see server greeting".
|
||||
|
||||
So actually try to bind it. SO_REUSEADDR is used so a port merely in
|
||||
TIME_WAIT (recently and cleanly closed) is NOT a false positive; only a
|
||||
live bound/listening socket -- a real squatter -- makes the bind fail, and
|
||||
then we stop here with an actionable message instead of failing obscurely
|
||||
later. The probe socket is closed immediately, freeing the port for the
|
||||
daemon that is about to bind it.
|
||||
"""
|
||||
s = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
||||
s.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
s.bind(('127.0.0.1', port))
|
||||
except OSError as e:
|
||||
test_fail(
|
||||
f"port {port} was claimed for this run but something is still bound "
|
||||
f"to 127.0.0.1:{port} ({e.strerror}). The claim_ports() lock only "
|
||||
"serializes live test runs, so a still-bound port almost always "
|
||||
"means an orphaned 'rsync --daemon' from a previously killed run "
|
||||
f"(find it with `fstat | grep {port}` / `netstat -an | grep {port}` "
|
||||
"and kill it, or run `fleettest.py --cleanup`), then retry.")
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
|
||||
def claim_ports(*ports: int) -> 'None':
|
||||
"""Reserve the given TCP port numbers for the rest of this process.
|
||||
|
||||
@@ -246,9 +210,6 @@ def claim_ports(*ports: int) -> 'None':
|
||||
# F_SETLKW via fcntl.lockf(LOCK_EX, length, start): exclusive
|
||||
# byte-range lock on byte `port`, blocking until acquired.
|
||||
fcntl.lockf(_port_lock_fd, fcntl.LOCK_EX, 1, port)
|
||||
# The lock only proves no other live test run owns the port; an orphaned
|
||||
# daemon from a killed run can still squat it (see _probe_bindable).
|
||||
_probe_bindable(port)
|
||||
|
||||
|
||||
# --- standalone rsyncd helpers ---------------------------------------------
|
||||
|
||||
24
xattrs.c
24
xattrs.c
@@ -295,12 +295,8 @@ static int rsync_xal_get(const char *fname, item_list *xalp)
|
||||
rxa = xalp->items;
|
||||
if (count > 1)
|
||||
qsort(rxa, count, sizeof (rsync_xa), rsync_xal_compare_names);
|
||||
/* Guard count==0: rxa is then xalp->items (possibly NULL) and the
|
||||
* "rxa += count-1" init would form NULL-1 (undefined). */
|
||||
if (count) {
|
||||
for (rxa += count-1; count; count--, rxa--)
|
||||
rxa->num = count;
|
||||
}
|
||||
for (rxa += count-1; count; count--, rxa--)
|
||||
rxa->num = count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -385,19 +381,17 @@ static int64 xattr_lookup_hash(const item_list *xalp)
|
||||
{
|
||||
const rsync_xa *rxas = xalp->items;
|
||||
size_t i;
|
||||
/* Accumulate unsigned: the summed hash values routinely overflow a
|
||||
* signed int64 (UB), and we only care about the resulting bit pattern. */
|
||||
uint64_t key = (uint64_t)hashlittle2(&xalp->count, sizeof xalp->count);
|
||||
int64 key = hashlittle2(&xalp->count, sizeof xalp->count);
|
||||
|
||||
for (i = 0; i < xalp->count; i++) {
|
||||
key += (uint64_t)hashlittle2(rxas[i].name, rxas[i].name_len);
|
||||
key += hashlittle2(rxas[i].name, rxas[i].name_len);
|
||||
if (rxas[i].datum_len > MAX_FULL_DATUM)
|
||||
key += (uint64_t)hashlittle2(rxas[i].datum, xattr_sum_len);
|
||||
key += hashlittle2(rxas[i].datum, xattr_sum_len);
|
||||
else
|
||||
key += (uint64_t)hashlittle2(rxas[i].datum, rxas[i].datum_len);
|
||||
key += hashlittle2(rxas[i].datum, rxas[i].datum_len);
|
||||
}
|
||||
|
||||
return (int64)key;
|
||||
return key;
|
||||
}
|
||||
|
||||
static int find_matching_xattr(const item_list *xalp)
|
||||
@@ -466,9 +460,7 @@ static int rsync_xal_store(item_list *xalp)
|
||||
* entire initial-count, not just enough space for one new item. */
|
||||
*new_list = empty_xa_list;
|
||||
(void)EXPAND_ITEM_LIST(&new_list->xa_items, rsync_xa, xalp->count);
|
||||
/* xalp->items is NULL for an empty list; memcpy(dst, NULL, 0) is UB. */
|
||||
if (xalp->count)
|
||||
memcpy(new_list->xa_items.items, xalp->items, xalp->count * sizeof (rsync_xa));
|
||||
memcpy(new_list->xa_items.items, xalp->items, xalp->count * sizeof (rsync_xa));
|
||||
new_list->xa_items.count = xalp->count;
|
||||
xalp->count = 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user