mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-15 17:40:43 -04:00
Compare commits
4 Commits
master
...
v34-stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88d9793bb2 | ||
|
|
bd4bf5dcf0 | ||
|
|
e5d718741f | ||
|
|
7afff20964 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: RsyncProject
|
||||
patreon: AndrewTridgell
|
||||
2
.github/workflows/almalinux-8-build.yml
vendored
2
.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:
|
||||
|
||||
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:
|
||||
|
||||
2
.github/workflows/cygwin-build.yml
vendored
2
.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:
|
||||
|
||||
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:
|
||||
|
||||
2
.github/workflows/macos-build.yml
vendored
2
.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:
|
||||
|
||||
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:
|
||||
|
||||
2
.github/workflows/ubuntu-22.04-build.yml
vendored
2
.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:
|
||||
|
||||
2
.github/workflows/ubuntu-build.yml
vendored
2
.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:
|
||||
|
||||
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:
|
||||
|
||||
96
.github/workflows/valgrind.yml
vendored
96
.github/workflows/valgrind.yml
vendored
@@ -1,96 +0,0 @@
|
||||
name: Valgrind memcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
memcheck:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
privilege: [ user, root ]
|
||||
transport: [ pipe, tcp ]
|
||||
name: memcheck (${{ matrix.privilege }}, ${{ matrix.transport }})
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y valgrind 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 --enable-debug
|
||||
- name: make
|
||||
run: make check-progs # builds rsync + the test helper programs runtests.py needs
|
||||
- name: info
|
||||
run: ./rsync --version
|
||||
|
||||
# Run the whole suite under valgrind. We gate on memory *errors* (uninit
|
||||
# reads, invalid read/write, bad frees, uninit syscall params), not leaks:
|
||||
# rsync deliberately leaves file-list/socket/option memory unfreed at exit
|
||||
# (short-lived process; the OS reclaims), so --leak-check=no avoids a sea of
|
||||
# by-design "definitely lost" reports. Functional pass/fail is covered by
|
||||
# the other workflows, so the suite is allowed to finish regardless of
|
||||
# per-test results; the scan step below is the gate. --error-exitcode=0
|
||||
# keeps valgrind from perturbing test exit codes; the bundled
|
||||
# testsuite/valgrind.supp silences known-benign reports.
|
||||
- name: run testsuite under valgrind
|
||||
run: |
|
||||
SUDO=
|
||||
[ "${{ matrix.privilege }}" = root ] && SUDO="sudo -E"
|
||||
TCP=
|
||||
[ "${{ matrix.transport }}" = tcp ] && TCP="--use-tcp"
|
||||
$SUDO ./runtests.py --valgrind \
|
||||
--valgrind-opts="--leak-check=no --error-exitcode=0" \
|
||||
$TCP -j8 --preserve-scratch || true
|
||||
|
||||
- name: scan for unsuppressed valgrind errors
|
||||
run: |
|
||||
sudo chown -R "$USER" testtmp 2>/dev/null || true
|
||||
mapfile -t logs < <(find testtmp -name 'valgrind.*.log' 2>/dev/null)
|
||||
if [ "${#logs[@]}" -eq 0 ]; then
|
||||
echo "::error::no valgrind logs were produced -- the suite did not run"
|
||||
exit 1
|
||||
fi
|
||||
echo "scanned ${#logs[@]} valgrind log(s)"
|
||||
bad=()
|
||||
for f in "${logs[@]}"; do
|
||||
grep -qE 'ERROR SUMMARY: [1-9][0-9]* errors' "$f" && bad+=("$f")
|
||||
done
|
||||
if [ "${#bad[@]}" -ne 0 ]; then
|
||||
echo "::error::valgrind reported unsuppressed errors in ${#bad[@]} run(s)"
|
||||
for f in "${bad[@]}"; do
|
||||
echo "===== $f ====="
|
||||
sed 's/==[0-9]*== //' "$f" | grep -A18 \
|
||||
-E 'depends on uninitialised|points to uninitialised|Invalid (read|write|free)|lost in loss record|Mismatched free' \
|
||||
| head -60
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
echo "valgrind clean: no unsuppressed errors"
|
||||
|
||||
- name: upload valgrind logs on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: valgrind-logs-${{ matrix.privilege }}-${{ matrix.transport }}
|
||||
path: testtmp/**/valgrind.*.log
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -139,7 +139,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, ModeCOPY_SRC, ModeCOPY_DST, ModeCOPY_AND, ModeOP;
|
||||
int ModeAND, ModeOR;
|
||||
char flags;
|
||||
};
|
||||
|
||||
@@ -43,20 +43,6 @@ 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. */
|
||||
@@ -64,13 +50,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, copybits = 0;
|
||||
int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0;
|
||||
struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL,
|
||||
*prev_mode = NULL;
|
||||
|
||||
while (state != STATE_ERROR) {
|
||||
if (!*modestr || *modestr == ',') {
|
||||
int bits, where_specified;
|
||||
int bits;
|
||||
|
||||
if (!op) {
|
||||
state = STATE_ERROR;
|
||||
@@ -84,10 +70,9 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
first_mode = curr_mode;
|
||||
curr_mode->next = NULL;
|
||||
|
||||
where_specified = where;
|
||||
if (where) {
|
||||
if (where)
|
||||
bits = where * what;
|
||||
} else {
|
||||
else {
|
||||
where = 0111;
|
||||
bits = (where * what) & ~orig_umask;
|
||||
}
|
||||
@@ -96,35 +81,18 @@ 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)
|
||||
- (copybits ? mode_dest_special_bits(where) : 0);
|
||||
curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 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;
|
||||
}
|
||||
|
||||
@@ -135,7 +103,7 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
|
||||
modestr++;
|
||||
|
||||
state = STATE_1ST_HALF;
|
||||
where = what = op = topoct = topbits = flags = copybits = 0;
|
||||
where = what = op = topoct = topbits = flags = 0;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
@@ -191,53 +159,26 @@ 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;
|
||||
@@ -271,20 +212,6 @@ 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. */
|
||||
@@ -292,25 +219,17 @@ 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;
|
||||
|
||||
11
generator.c
11
generator.c
@@ -718,7 +718,8 @@ static void sum_sizes_sqroot(struct sum_struct *sum, int64 len)
|
||||
else {
|
||||
int32 max_blength = protocol_version < 30 ? OLD_MAX_BLOCK_SIZE : MAX_BLOCK_SIZE;
|
||||
int32 c;
|
||||
for (c = 1, l = len; l >>= 2; c <<= 1) {}
|
||||
int cnt;
|
||||
for (c = 1, l = len, cnt = 0; l >>= 2; c <<= 1, cnt++) {}
|
||||
if (c < 0 || c >= max_blength)
|
||||
blength = max_blength;
|
||||
else {
|
||||
@@ -1229,7 +1230,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
static int need_fuzzy_dirlist = 0;
|
||||
struct file_struct *fuzzy_file = NULL;
|
||||
int fd = -1, f_copy = -1;
|
||||
stat_x sx = {0}, real_sx;
|
||||
stat_x sx, real_sx;
|
||||
STRUCT_STAT partial_st;
|
||||
struct file_struct *back_file = NULL;
|
||||
int statret, real_ret, stat_errno;
|
||||
@@ -1627,10 +1628,6 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
|| (preserve_specials && ftype == FT_SPECIAL)) {
|
||||
dev_t rdev;
|
||||
int del_for_flag;
|
||||
/* Whether the dest existed, captured before the type-mismatch
|
||||
* flip below clears statret -- so atomic_create() gets a delete
|
||||
* flag (and reads sx.st) only when sx.st was actually stat'd. */
|
||||
int dest_existed = (statret == 0);
|
||||
if (ftype == FT_DEVICE) {
|
||||
uint32 *devp = F_RDEV_P(file);
|
||||
rdev = MAKEDEV(DEV_MAJOR(devp), DEV_MINOR(devp));
|
||||
@@ -1677,7 +1674,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
fname, (int)file->mode,
|
||||
(long)major(rdev), (long)minor(rdev));
|
||||
}
|
||||
if (atomic_create(file, fname, NULL, NULL, rdev, &sx, dest_existed ? del_for_flag : 0)) {
|
||||
if (atomic_create(file, fname, NULL, NULL, rdev, &sx, del_for_flag)) {
|
||||
set_file_attrs(fname, file, NULL, NULL, 0);
|
||||
if (itemizing) {
|
||||
itemize(fnamecmp, file, ndx, statret, &sx,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -34,9 +34,7 @@
|
||||
#endif
|
||||
|
||||
.text
|
||||
/* .balign = N bytes everywhere; bare .align means 2^N on Mach-O (would ask
|
||||
* for 64KB alignment and trip a macOS linker warning). */
|
||||
.balign 16
|
||||
.align 16
|
||||
|
||||
.globl md5_process_asm
|
||||
md5_process_asm:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
log.c
8
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;
|
||||
@@ -298,12 +297,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
|
||||
in_block = 1;
|
||||
if (!log_initialised)
|
||||
log_init(0);
|
||||
/* buf holds exactly len bytes and is not necessarily NUL-terminated
|
||||
* (e.g. a forwarded MSG_* payload from read_a_msg), so copy by length
|
||||
* rather than strlcpy(), which would strlen() past the end of buf. */
|
||||
int mlen = MIN((int)sizeof msg - 1, len);
|
||||
memcpy(msg, buf, mlen);
|
||||
msg[mlen] = '\0';
|
||||
strlcpy(msg, buf, MIN((int)sizeof msg, len + 1));
|
||||
logit(priority, msg);
|
||||
in_block = 0;
|
||||
|
||||
|
||||
@@ -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
|
||||
""")
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
10
rsync.1.md
10
rsync.1.md
@@ -1523,16 +1523,6 @@ 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.
|
||||
|
||||
|
||||
19
runtests.py
19
runtests.py
@@ -266,19 +266,8 @@ def build_rsync_cmd(rsync_bin, args, scratchbase):
|
||||
"""Build the RSYNC command string for tests."""
|
||||
parts = []
|
||||
if args.valgrind:
|
||||
# Logs go in a world-writable+sticky subdir so that rsync children
|
||||
# which drop privileges (the setpriv cap-drop in partial_nowrite, a
|
||||
# daemon dropping to the module's uid) can still create their log file
|
||||
# even when scratchbase itself is root-owned.
|
||||
vgdir = os.path.join(scratchbase, 'valgrind-logs')
|
||||
os.makedirs(vgdir, exist_ok=True)
|
||||
os.chmod(vgdir, 0o1777)
|
||||
vlog = os.path.join(vgdir, 'valgrind.%p.log')
|
||||
vlog = os.path.join(scratchbase, 'valgrind.%p.log')
|
||||
vopts = f'--log-file={vlog}'
|
||||
supp = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'testsuite', 'valgrind.supp')
|
||||
if os.path.exists(supp):
|
||||
vopts += f' --suppressions={supp}'
|
||||
if args.valgrind_opts:
|
||||
vopts += ' ' + args.valgrind_opts
|
||||
parts.append(f'valgrind {vopts}')
|
||||
@@ -461,7 +450,7 @@ def main():
|
||||
print(f' os={subprocess.check_output(["uname", "-a"], text=True).strip()}')
|
||||
print(f' preserve_scratch={"yes" if args.preserve_scratch else "no"}')
|
||||
if args.valgrind:
|
||||
print(f' valgrind=enabled (logs in valgrind-logs/valgrind.*.log)')
|
||||
print(f' valgrind=enabled (logs in valgrind.*.log)')
|
||||
if args.parallel > 1:
|
||||
print(f' parallel={args.parallel}')
|
||||
print(f' daemon_transport={"tcp (loopback)" if args.use_tcp else "pipe (secure default)"}')
|
||||
@@ -627,7 +616,7 @@ def main():
|
||||
# Check valgrind logs for errors
|
||||
vg_errors = 0
|
||||
if args.valgrind:
|
||||
for vlog in sorted(glob.glob(os.path.join(scratchbase, 'valgrind-logs', 'valgrind.*.log'))):
|
||||
for vlog in sorted(glob.glob(os.path.join(scratchbase, 'valgrind.*.log'))):
|
||||
try:
|
||||
with open(vlog) as f:
|
||||
content = f.read()
|
||||
@@ -651,7 +640,7 @@ def main():
|
||||
if skipped > 0:
|
||||
print(f' {skipped} skipped')
|
||||
if vg_errors > 0:
|
||||
print(f' {vg_errors} valgrind error(s) found (see logs in {os.path.join(scratchbase, "valgrind-logs")})')
|
||||
print(f' {vg_errors} valgrind error(s) found (see logs in {scratchbase})')
|
||||
|
||||
if expect is not None:
|
||||
# Version-mixing mode: the run is judged purely on whether each test's
|
||||
|
||||
@@ -317,21 +317,6 @@ __attribute__ ((target("sse2"))) MVSTATIC int32 get_checksum1_sse2_32(schar* buf
|
||||
|
||||
extern "C" __attribute__ ((target("avx2"))) int32 get_checksum1_avx2_asm(schar* buf, int32 len, int32 i, uint32* ps1, uint32* ps2);
|
||||
|
||||
/* The asm routine is AVX2-only and, unlike the multi-versioned intrinsic
|
||||
* paths, has no compiler-generated fallback, so it must not be called on a
|
||||
* CPU without AVX2 (it would fault with SIGILL). Gate it on a cached runtime
|
||||
* check; when AVX2 is absent we skip it and the SSSE3/SSE2/scalar steps,
|
||||
* which are safe everywhere, do all the work. */
|
||||
static int roll_asm_have_avx2(void)
|
||||
{
|
||||
static int have = -1;
|
||||
if (have < 0) {
|
||||
__builtin_cpu_init();
|
||||
have = __builtin_cpu_supports("avx2") ? 1 : 0;
|
||||
}
|
||||
return have;
|
||||
}
|
||||
|
||||
#else /* } { */
|
||||
|
||||
/*
|
||||
@@ -476,8 +461,7 @@ static inline uint32 get_checksum1_cpp(char *buf1, int32 len)
|
||||
|
||||
// multiples of 64 bytes using AVX2 (if available)
|
||||
#ifdef USE_ROLL_ASM
|
||||
if (roll_asm_have_avx2())
|
||||
i = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2);
|
||||
i = get_checksum1_avx2_asm((schar*)buf1, len, i, &s1, &s2);
|
||||
#else
|
||||
i = get_checksum1_avx2_64((schar*)buf1, len, i, &s1, &s2);
|
||||
#endif
|
||||
@@ -595,10 +579,7 @@ static uint32 checksum_via_avx2(char *buf, int32 len)
|
||||
int32 i;
|
||||
uint32 s1 = 0, s2 = 0;
|
||||
#ifdef USE_ROLL_ASM
|
||||
if (roll_asm_have_avx2())
|
||||
i = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2);
|
||||
else
|
||||
i = 0;
|
||||
i = get_checksum1_avx2_asm((schar*)buf, len, 0, &s1, &s2);
|
||||
#else
|
||||
i = get_checksum1_avx2_64((schar*)buf, len, 0, &s1, &s2);
|
||||
#endif
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
# abdiff helper: a "remote shell" that emulates an sshd forced-command of
|
||||
# `rrsync DIR`. rsync invokes a remote shell as:
|
||||
# <shell-words...> [ssh-opts] <host> <rsync --server ...>
|
||||
# so when used as -e "sh rrsh.sh <RRSYNC> <DIR>" rsync calls us as:
|
||||
# sh rrsh.sh <RRSYNC> <DIR> [opts] lh rsync --server ...
|
||||
# We hand the server command to rrsync via SSH_ORIGINAL_COMMAND (exactly as
|
||||
# sshd would) and exec the restricted wrapper, so abdiff can A/B the rrsync
|
||||
# path itself. Only the pretend hosts "lh"/"localhost" are accepted.
|
||||
RRSYNC="$1"; DIR="$2"; shift 2
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-l) shift 2 ;;
|
||||
lh|localhost) shift; break ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
SSH_ORIGINAL_COMMAND="$*"
|
||||
export SSH_ORIGINAL_COMMAND
|
||||
exec "$RRSYNC" "$DIR"
|
||||
@@ -184,53 +184,3 @@ Each target must be provisioned with the build toolchain its workflow installs
|
||||
(autoconf, automake, a C compiler, perl, a python3 markdown module such as
|
||||
cmarkgfm or commonmark unless the flags pass `--disable-md2man`, and the dev
|
||||
libraries its configure flags enable). A missing piece shows up as `BUILD-FAIL`.
|
||||
|
||||
## Differential regression hunting (abdiff.py)
|
||||
|
||||
`testsuite/abdiff.py` is a developer tool — **not** a `*_test.py`, so `runtests.py`
|
||||
ignores it. It hunts *regressions* by running the **same benign transfer** with
|
||||
two rsync binaries (`A` = the build under test, `B` = a baseline) and comparing
|
||||
the OUTCOME. The oracle is: for a benign input, a correctness/behaviour change
|
||||
between the builds must be **invisible**, so A and B must produce an identical
|
||||
result. Any divergence is a regression candidate to investigate and, if real,
|
||||
minimize into a `*_test.py`.
|
||||
|
||||
It compares exit code, stderr (error markers + normalised text), `--stats`
|
||||
"Literal data", the destination tree (content + full metadata: mode/uid/gid/
|
||||
mtime/size/symlink target/xattrs/ACLs/hardlink grouping), the `--itemize` list,
|
||||
and — with `--cost` — peak process-group RSS (a resource-regression oracle that
|
||||
functional comparison misses). A **stability gate** runs each binary several
|
||||
times and escalates on a candidate diff; nondeterministic scenarios are
|
||||
quarantined `FLAKY`, never reported as regressions.
|
||||
|
||||
Run it from the build directory (so `./rsync` and `old_versions/` resolve):
|
||||
|
||||
```sh
|
||||
testsuite/abdiff.py # default: ./rsync vs old_versions/rsync_3.4.1
|
||||
testsuite/abdiff.py --sweep all -j5 # broad single pass, 5-way parallel
|
||||
testsuite/abdiff.py --loop --timelimit 3600 --cost # hunt for an hour, resource oracle on
|
||||
testsuite/abdiff.py --list --sweep all # list scenarios without running
|
||||
```
|
||||
|
||||
Each finding is classed `DIFF` (regression candidate), `ALLOW` (an intentional,
|
||||
documented behaviour change listed in the tool's allowlist), `BETTER` (A succeeds
|
||||
where B fails), `FLAKY`, or `TIMEOUT`. Findings are printed and appended to a
|
||||
per-run `abdiff-log_<TIME>.txt` (and the curated `--findings` log).
|
||||
|
||||
Key options: `-j N` parallelism; `--sweep NAME|all`; `--loop` (endless
|
||||
random + systematic-combo stream) bounded by `--timelimit SECS`; `--cost`
|
||||
(+`--scale N` for the large-tree fixtures); `--repeat N` (stability samples);
|
||||
`--rsync-a`/`--rsync-b` the two binaries. Run **as root** to fold in the
|
||||
owner/device/specials/fake-super and chroot-daemon sweeps automatically.
|
||||
|
||||
Transport lanes (a feature broken only over the wire is invisible to a local
|
||||
copy): local, an ssh split (`support/lsh.sh`), a stdio-pipe daemon, a **real TCP
|
||||
daemon** (bound port + greeting/handshake, and an auth challenge-response
|
||||
variant), and the restricted **rrsync** wrapper (`support/rrsh.sh`). rrsync's
|
||||
behaviour ships in the *script*, so pair each binary with its own version's
|
||||
rrsync via `--rrsync-a`/`--rrsync-b` (give B's rrsync, e.g. one extracted from
|
||||
that release's `support/rrsync`).
|
||||
|
||||
Cross-version baselines are the static binaries already in `old_versions/`;
|
||||
`old_versions/build_static.sh` builds more from a git tag (and you can grab a
|
||||
matching `support/rrsync` from the same tag for the rrsync lane).
|
||||
|
||||
2824
testsuite/abdiff.py
2824
testsuite/abdiff.py
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@ import shutil
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, SCRATCHDIR, TODIR,
|
||||
build_rsyncd_conf, check_perms, checkit, makepath, rmtree,
|
||||
run_rsync, start_test_daemon, test_fail,
|
||||
build_rsyncd_conf, checkit, makepath, rmtree,
|
||||
run_rsync, start_test_daemon,
|
||||
)
|
||||
|
||||
|
||||
@@ -62,37 +62,6 @@ 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)
|
||||
|
||||
@@ -16,8 +16,37 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from rsyncfns import SCRATCHDIR, TOOLDIR, rmtree, test_fail
|
||||
from rsyncfns import SCRATCHDIR, TOOLDIR, rmtree, test_fail, test_xfail
|
||||
|
||||
|
||||
def kernel_has_resolve_beneath():
|
||||
"""Whether the running kernel honours a 'beneath' confinement primitive,
|
||||
matching t_chmod_secure's kernel_resolve_beneath_supported(). On Linux probe
|
||||
openat2(RESOLVE_BENEATH); elsewhere we can't probe O_RESOLVE_BENEATH from
|
||||
Python, but the FreeBSD/macOS versions that have it pass t_chmod_secure
|
||||
outright, so this is never consulted on a failure there. NB: this is a
|
||||
different question from rsyncfns.resolve_beneath_supported(), which probes
|
||||
in-tree dir-symlink following -- the per-component fallback handles that, so
|
||||
it stays True without any kernel primitive."""
|
||||
if not sys.platform.startswith('linux'):
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
libc = ctypes.CDLL(None, use_errno=True)
|
||||
libc.syscall.restype = ctypes.c_long
|
||||
SYS_openat2, AT_FDCWD, RESOLVE_BENEATH = 437, -100, 0x08
|
||||
# struct open_how { __u64 flags; __u64 mode; __u64 resolve; }
|
||||
how = (ctypes.c_uint64 * 3)(os.O_RDONLY | os.O_DIRECTORY, 0, RESOLVE_BENEATH)
|
||||
fd = libc.syscall(SYS_openat2, AT_FDCWD, ctypes.c_char_p(b'.'),
|
||||
how, ctypes.c_size_t(24))
|
||||
if fd >= 0:
|
||||
os.close(fd)
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
mod = SCRATCHDIR / 'module'
|
||||
@@ -39,13 +68,28 @@ os.symlink('../trap', mod / 'escape_link')
|
||||
os.chmod(mod / 'topfile', 0o600)
|
||||
|
||||
proc = subprocess.run([str(TOOLDIR / 't_chmod_secure'), str(mod)])
|
||||
if proc.returncode != 0:
|
||||
test_fail("t_chmod_secure reported failures (see stderr above)")
|
||||
|
||||
# Second-look sanity check from Python.
|
||||
sentinel_mode = (trap_outside / 'sentinel').stat().st_mode & 0o777
|
||||
if sentinel_mode != 0o600:
|
||||
test_fail(
|
||||
f"outside sentinel mode changed from 600 to {oct(sentinel_mode)[2:]} "
|
||||
"-- chmod escaped the module"
|
||||
)
|
||||
escaped = sentinel_mode != 0o600
|
||||
|
||||
if not kernel_has_resolve_beneath():
|
||||
# No kernel RESOLVE_BENEATH primitive, so do_chmod_at() falls back to the
|
||||
# per-component O_NOFOLLOW resolver, which cannot fully confine every chmod
|
||||
# scenario against a TOCTOU symlink swap. master's t_chmod_secure adjusts
|
||||
# its expectations for this fallback; 3.4's older helper does not and counts
|
||||
# it as a failure. The do_chmod_at() code is identical to master's, so this
|
||||
# is an inherent platform limitation (no kernel beneath primitive), not a
|
||||
# 3.4 regression -- mark it XFAIL.
|
||||
if escaped or proc.returncode != 0:
|
||||
test_xfail(
|
||||
"no kernel RESOLVE_BENEATH primitive: the per-component fallback "
|
||||
"cannot fully confine chmod and 3.4's t_chmod_secure lacks master's "
|
||||
"fallback expectation adjustment (same do_chmod_at as master)")
|
||||
else:
|
||||
# RESOLVE_BENEATH is active: confinement is guaranteed, so any escape or
|
||||
# helper-reported failure is a real bug.
|
||||
if escaped:
|
||||
test_fail(
|
||||
f"outside sentinel mode changed from 600 to {oct(sentinel_mode)[2:]} "
|
||||
"-- chmod escaped the module")
|
||||
if proc.returncode != 0:
|
||||
test_fail("t_chmod_secure reported failures (see stderr above)")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
# from source to destination (other permission changes ignored), while a
|
||||
# normal copy without -E should leave the destination permissions alone.
|
||||
|
||||
import errno
|
||||
import os
|
||||
|
||||
from rsyncfns import FROMDIR, TODIR, check_perms, run_rsync, test_skipped
|
||||
@@ -16,14 +15,11 @@ FROMDIR.mkdir(parents=True, exist_ok=True)
|
||||
(FROMDIR / '2').write_text("#!/bin/sh\necho 'Program Two!'\n")
|
||||
|
||||
# Setuid-and-rwx for owner, nothing else. Some platforms reject 1700 for
|
||||
# non-root callers (no permission to set sticky); FreeBSD rejects it with
|
||||
# EFTYPE rather than EPERM. Only skip on those; re-raise anything unexpected.
|
||||
_STICKY_SKIP_ERRNOS = {errno.EPERM, errno.EACCES, getattr(errno, 'EFTYPE', None)}
|
||||
# non-root callers (no permission to set sticky); the shell test treats
|
||||
# that case as a skip.
|
||||
try:
|
||||
os.chmod(FROMDIR / '1', 0o1700)
|
||||
except OSError as e:
|
||||
if e.errno not in _STICKY_SKIP_ERRNOS:
|
||||
raise
|
||||
except PermissionError:
|
||||
test_skipped("Can't chmod")
|
||||
os.chmod(FROMDIR / '2', 0o600)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -774,94 +771,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,10 +813,7 @@ 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 "
|
||||
|
||||
@@ -10,7 +10,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from rsyncfns import make_data_file, cp_p, makepath, checkit, RSYNC, TMPDIR, get_testuid, get_rootuid
|
||||
from rsyncfns import make_data_file, cp_p, makepath, checkit, run_rsync, test_xfail, RSYNC, TMPDIR, get_testuid, get_rootuid
|
||||
|
||||
BASEDIR = TMPDIR
|
||||
|
||||
@@ -67,4 +67,15 @@ if (is_root and sys.platform == 'linux' and hasattr(os, 'unshare')
|
||||
pass # mount namespace denied (unprivileged container) -- run as root
|
||||
|
||||
|
||||
# 3.4 stable lacks #957 (receiver chmod-the-target-when-denied): when the dest
|
||||
# temp is read-only to the (cap-dropped / non-root) receiver, rsync can't write
|
||||
# it and doesn't retry with a chmod, so the transfer errors. As root the write
|
||||
# is never denied and it succeeds normally, so this only fires off-root.
|
||||
probe = run_rsync('-avv', '--partial', '--delay-updates',
|
||||
f'{FROMDIR}/', f'{TODIR}/', check=False, capture_output=True)
|
||||
if probe.returncode != 0:
|
||||
test_xfail(
|
||||
"#957 (receiver chmod-the-target-when-denied) not in 3.4 stable: rsync "
|
||||
"cannot write the read-only dest temp and does not retry with chmod "
|
||||
f"(rsync exited {probe.returncode})")
|
||||
checkit(['-avv', '--partial', '--delay-updates', f'{FROMDIR}/', f'{TODIR}/'], FROMDIR, TODIR)
|
||||
|
||||
@@ -1,506 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the transfer performance of two rsync binaries (local <-> local).
|
||||
|
||||
This is a standalone dev tool (run it directly, not via runtests.py) for
|
||||
spotting performance regressions between rsync releases. Given two rsync
|
||||
binaries it builds one test tree, then runs the two binaries ALTERNATELY for a
|
||||
number of loops, timing each transfer, and reports the mean and standard
|
||||
deviation of the transfer time for each binary.
|
||||
|
||||
Two transfers are timed each loop (see --mode):
|
||||
* full -- a fresh copy into an emptied destination (end-to-end read+write).
|
||||
* noop -- a re-run against an already-synced destination (rsync's own
|
||||
scan / file-list / stat overhead, where many regressions hide).
|
||||
|
||||
The first measured run of each binary is dropped (see --warmup) because it
|
||||
cold-loads the source into the page cache and is an outlier.
|
||||
|
||||
The test tree's shape (heavy-tailed file sizes, a directory spine, symlinks,
|
||||
hard links and a spread of permission modes) follows the gentestdata.py
|
||||
generator; it is deterministic for a given --seed.
|
||||
|
||||
Examples:
|
||||
# Quick smoke run, same binary twice (means should match, no regression).
|
||||
./perftest.py --files 200 --total-size 5M -n 3 ./rsync ./rsync
|
||||
|
||||
# Compare a released binary against a fresh build over 8 loops.
|
||||
./perftest.py -n 8 ../old_versions/rsync_3.4.0 ./rsync
|
||||
|
||||
# Heavier tree, no-op (scan-overhead) timing only.
|
||||
./perftest.py --files 50000 --total-size 2G --mode noop OLD/rsync NEW/rsync
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import shutil
|
||||
import statistics
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test-tree generation (ported from gentestdata.py, kept self-contained).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Marker file at the tree root; safe_rmtree only deletes a tree carrying it.
|
||||
MARKER = ".perftest"
|
||||
|
||||
# Permission modes drawn at random for regular files (execs + read-only).
|
||||
FILE_MODES = [0o644, 0o644, 0o600, 0o640, 0o664, 0o444, 0o755, 0o750, 0o700]
|
||||
# Directory modes; owner always keeps r-x so the tree stays traversable.
|
||||
DIR_MODES = [0o755, 0o755, 0o775, 0o750, 0o700, 0o555]
|
||||
|
||||
SIZE_SIGMA = 1.8 # sigma of the underlying lognormal size distribution
|
||||
BASE_BUF_SIZE = 1 << 20 # 1 MiB shared random buffer for file content
|
||||
|
||||
|
||||
def parse_size(s):
|
||||
"""Parse a human size like 500M, 1.5GiB, 200KB, or a bare byte count."""
|
||||
s = s.strip()
|
||||
units = {
|
||||
"": 1, "B": 1,
|
||||
"K": 1024, "KIB": 1024, "KB": 1000,
|
||||
"M": 1024**2, "MIB": 1024**2, "MB": 1000**2,
|
||||
"G": 1024**3, "GIB": 1024**3, "GB": 1000**3,
|
||||
"T": 1024**4, "TIB": 1024**4, "TB": 1000**4,
|
||||
}
|
||||
num, suffix = s, ""
|
||||
while num and not (num[-1].isdigit() or num[-1] == "."):
|
||||
suffix = num[-1] + suffix
|
||||
num = num[:-1]
|
||||
suffix = suffix.upper()
|
||||
if suffix not in units:
|
||||
raise argparse.ArgumentTypeError(f"unknown size suffix in {s!r}")
|
||||
try:
|
||||
value = float(num)
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError(f"invalid size {s!r}")
|
||||
return int(value * units[suffix])
|
||||
|
||||
|
||||
def human(n):
|
||||
"""Format a byte count for the summary output."""
|
||||
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||
if abs(n) < 1024 or unit == "TiB":
|
||||
return f"{n:.1f}{unit}" if unit != "B" else f"{n}B"
|
||||
n /= 1024
|
||||
|
||||
|
||||
def gen_sizes(n, total, rng):
|
||||
"""Return n heavy-tailed file sizes (bytes) summing to exactly `total`."""
|
||||
if n == 0:
|
||||
return []
|
||||
weights = [math.exp(rng.gauss(0.0, SIZE_SIGMA)) for _ in range(n)]
|
||||
wsum = sum(weights)
|
||||
sizes = [int(w / wsum * total) for w in weights]
|
||||
drift = total - sum(sizes)
|
||||
if drift and sizes:
|
||||
i = max(range(n), key=lambda k: sizes[k])
|
||||
sizes[i] += drift
|
||||
return sizes
|
||||
|
||||
|
||||
def build_dirs(root, num_dirs, max_depth, rng):
|
||||
"""Create `num_dirs` dirs under root, up to `max_depth` deep; return them."""
|
||||
os.makedirs(root)
|
||||
dirs = [root]
|
||||
depth_of = {root: 0}
|
||||
candidates = [root] if max_depth > 0 else []
|
||||
counter = 0
|
||||
|
||||
cur = root
|
||||
for d in range(1, max_depth + 1):
|
||||
cur = os.path.join(cur, f"d{d}")
|
||||
os.mkdir(cur)
|
||||
dirs.append(cur)
|
||||
depth_of[cur] = d
|
||||
if d < max_depth:
|
||||
candidates.append(cur)
|
||||
|
||||
while len(dirs) < num_dirs and candidates:
|
||||
parent = rng.choice(candidates)
|
||||
counter += 1
|
||||
child = os.path.join(parent, f"dir{counter}")
|
||||
os.mkdir(child)
|
||||
d = depth_of[parent] + 1
|
||||
dirs.append(child)
|
||||
depth_of[child] = d
|
||||
if d < max_depth:
|
||||
candidates.append(child)
|
||||
|
||||
return dirs
|
||||
|
||||
|
||||
def write_file(path, size, index, base):
|
||||
"""Write a regular file of exactly `size` bytes (index/size in first 16)."""
|
||||
with open(path, "wb") as f:
|
||||
remaining = size
|
||||
if remaining >= 16:
|
||||
f.write(struct.pack("<QQ", index, size))
|
||||
remaining -= 16
|
||||
blen = len(base)
|
||||
while remaining > 0:
|
||||
chunk = base if remaining >= blen else base[:remaining]
|
||||
f.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
|
||||
|
||||
def rel_symlink(target, link_path):
|
||||
"""Create a relative symlink at link_path pointing at target."""
|
||||
rel = os.path.relpath(target, os.path.dirname(link_path))
|
||||
os.symlink(rel, link_path)
|
||||
|
||||
|
||||
def safe_rmtree(path):
|
||||
"""Remove a tree, even one containing read-only directories."""
|
||||
for dirpath, _dirnames, _filenames in os.walk(path):
|
||||
try:
|
||||
os.chmod(dirpath, 0o700)
|
||||
except OSError:
|
||||
pass
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
def generate_tree(root, args):
|
||||
"""Build the deterministic source tree at `root`; return a summary string."""
|
||||
n = args.files
|
||||
num_dirs = args.dirs if args.dirs is not None else max(args.depth, n // 20, 1)
|
||||
n_sym = args.symlinks if args.symlinks is not None else (max(1, n // 20) if n else 0)
|
||||
n_hard = args.hardlinks if args.hardlinks is not None else (max(1, n // 20) if n else 0)
|
||||
|
||||
rng = random.Random(args.seed)
|
||||
base = rng.randbytes(BASE_BUF_SIZE)
|
||||
|
||||
dirs = build_dirs(root, num_dirs, args.depth, rng)
|
||||
with open(os.path.join(root, MARKER), "w") as f:
|
||||
f.write(f"generated by perftest.py seed={args.seed} files={n} "
|
||||
f"total={args.total_size}\n")
|
||||
|
||||
sizes = gen_sizes(n, args.total_size, rng)
|
||||
files = []
|
||||
for i in range(n):
|
||||
path = os.path.join(rng.choice(dirs), f"file{i}.dat")
|
||||
write_file(path, sizes[i], i, base)
|
||||
files.append(path)
|
||||
|
||||
hard_made = 0
|
||||
if files:
|
||||
for i in range(n_hard):
|
||||
tgt = rng.choice(files)
|
||||
link = os.path.join(rng.choice(dirs), f"hlink{i}_{os.path.basename(tgt)}")
|
||||
try:
|
||||
os.link(tgt, link)
|
||||
hard_made += 1
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
sym_made = 0
|
||||
for i in range(n_sym):
|
||||
link = os.path.join(rng.choice(dirs), f"sym{i}")
|
||||
roll = rng.random()
|
||||
try:
|
||||
if roll < 0.15 or not files:
|
||||
os.symlink(f"../broken-target-{i}", link)
|
||||
elif roll < 0.30:
|
||||
rel_symlink(rng.choice(dirs), link)
|
||||
else:
|
||||
rel_symlink(rng.choice(files), link)
|
||||
sym_made += 1
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
for path in files:
|
||||
os.chmod(path, rng.choice(FILE_MODES))
|
||||
for path in sorted((d for d in dirs if d != root),
|
||||
key=lambda p: p.count(os.sep), reverse=True):
|
||||
os.chmod(path, rng.choice(DIR_MODES))
|
||||
|
||||
return (f"files={n} dirs={len(dirs)} symlinks={sym_made} hardlinks={hard_made} "
|
||||
f"total={human(sum(sizes))} biggest={human(max(sizes) if sizes else 0)} "
|
||||
f"seed={args.seed}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Benchmark.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Binary:
|
||||
label: str # "A" / "B"
|
||||
path: str # absolute path to the rsync binary
|
||||
version: str # first line of `rsync --version`
|
||||
|
||||
|
||||
def rsync_version(path):
|
||||
"""Return the first line of `<rsync> --version`, or a placeholder."""
|
||||
try:
|
||||
r = subprocess.run([path, "--version"], capture_output=True, text=True, timeout=15)
|
||||
line = (r.stdout or r.stderr or "").splitlines()
|
||||
return line[0].strip() if line else "(no --version output)"
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
return f"(version unavailable: {e})"
|
||||
|
||||
|
||||
def drop_caches():
|
||||
"""Best-effort: flush dirty pages and drop the page/dentry/inode caches.
|
||||
|
||||
Needs root to write /proc/sys/vm/drop_caches; returns True on success.
|
||||
"""
|
||||
subprocess.run(["sync"], check=False)
|
||||
try:
|
||||
with open("/proc/sys/vm/drop_caches", "w") as f:
|
||||
f.write("3\n")
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def time_transfer(binary, rsync_args, src, dest, timeout):
|
||||
"""Run one `rsync <args> src/ dest/` and return its wall-clock seconds.
|
||||
|
||||
Raises RuntimeError if rsync exits non-zero (a failed transfer can't be
|
||||
timed meaningfully).
|
||||
"""
|
||||
argv = [binary.path, *rsync_args, src + "/", dest + "/"]
|
||||
t0 = time.monotonic()
|
||||
r = subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
|
||||
elapsed = time.monotonic() - t0
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"{binary.label} ({binary.path}) rsync exited {r.returncode}:\n"
|
||||
f" cmd: {shlex.join(argv)}\n"
|
||||
f" {(r.stderr or r.stdout).strip()}")
|
||||
return elapsed
|
||||
|
||||
|
||||
def run_benchmark(binaries, args, src, dest_full, dest_noop):
|
||||
"""Run the alternating loops; return {label: {mode: [all samples]}}."""
|
||||
do_full = args.mode in ("both", "full")
|
||||
do_noop = args.mode in ("both", "noop")
|
||||
|
||||
# Pre-populate the shared no-op destination so every timed no-op run finds
|
||||
# nothing to do. Use binary A; its content is identical for B.
|
||||
if do_noop:
|
||||
time_transfer(binaries[0], args.rsync_args, src, dest_noop, args.timeout)
|
||||
|
||||
samples = {b.label: {m: [] for m in ("full", "noop")} for b in binaries}
|
||||
total_loops = args.warmup + args.runs
|
||||
|
||||
for loop in range(total_loops):
|
||||
tag = "warmup" if loop < args.warmup else f"run {loop - args.warmup + 1}/{args.runs}"
|
||||
# Alternate which binary goes first to cancel first-mover/thermal drift.
|
||||
order = binaries if loop % 2 == 0 else list(reversed(binaries))
|
||||
for b in order:
|
||||
if do_full:
|
||||
safe_rmtree(dest_full) if os.path.exists(dest_full) else None
|
||||
os.mkdir(dest_full)
|
||||
if args.drop_caches:
|
||||
drop_caches()
|
||||
t = time_transfer(b, args.rsync_args, src, dest_full, args.timeout)
|
||||
samples[b.label]["full"].append(t)
|
||||
_progress(b, "full", tag, t)
|
||||
if do_noop:
|
||||
if args.drop_caches:
|
||||
drop_caches()
|
||||
t = time_transfer(b, args.rsync_args, src, dest_noop, args.timeout)
|
||||
samples[b.label]["noop"].append(t)
|
||||
_progress(b, "noop", tag, t)
|
||||
return samples
|
||||
|
||||
|
||||
def _progress(binary, mode, tag, t):
|
||||
excl = " (warmup, excluded)" if tag == "warmup" else ""
|
||||
print(f" [{tag:>10}] {binary.label} {mode:<4} {t:8.3f}s{excl}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reporting.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _stats(times):
|
||||
"""(n, mean, stddev, min, median) over the timing samples."""
|
||||
n = len(times)
|
||||
if n == 0:
|
||||
return (0, 0.0, 0.0, 0.0, 0.0)
|
||||
return (n, statistics.mean(times),
|
||||
statistics.stdev(times) if n > 1 else 0.0,
|
||||
min(times), statistics.median(times))
|
||||
|
||||
|
||||
def report(binaries, samples, args):
|
||||
"""Print the per-binary tables and the A-vs-B comparison; return exit code."""
|
||||
print("\n" + "=" * 72)
|
||||
for b in binaries:
|
||||
print(f"{b.label}: {b.path}\n {b.version}")
|
||||
print(f"rsync args: {' '.join(args.rsync_args)} "
|
||||
f"(note: a full copy is not fsync'd unless you add --fsync)")
|
||||
print("=" * 72)
|
||||
|
||||
modes = [m for m in ("full", "noop") if any(samples[b.label][m] for b in binaries)]
|
||||
hdr = f"{'binary':<7}{'mode':<6}{'runs':>5}{'mean':>11}{'stddev':>11}{'min':>11}{'median':>11}"
|
||||
|
||||
for mode in modes:
|
||||
print(f"\n{hdr}\n{'-' * len(hdr)}")
|
||||
st = {}
|
||||
for b in binaries:
|
||||
# Drop the leading warm-up samples before computing statistics.
|
||||
kept = samples[b.label][mode][args.warmup:]
|
||||
st[b.label] = _stats(kept)
|
||||
n, mean, sd, mn, md = st[b.label]
|
||||
print(f"{b.label:<7}{mode:<6}{n:>5}{mean:>10.3f}s{sd:>10.3f}s"
|
||||
f"{mn:>10.3f}s{md:>10.3f}s")
|
||||
|
||||
a, c = binaries[0].label, binaries[1].label
|
||||
(na, ma, sda, *_), (nc, mc, sdc, *_) = st[a], st[c]
|
||||
if na and nc and ma > 0:
|
||||
delta = mc - ma
|
||||
pct = delta / ma * 100.0
|
||||
noise = max(sda, sdc)
|
||||
# Flag only when B is slower beyond the run-to-run noise and a small
|
||||
# relative threshold, so jitter doesn't cry "regression".
|
||||
if delta > noise and pct > args.threshold:
|
||||
verdict = f"REGRESSION (slower): {c} is {pct:+.1f}% vs {a}"
|
||||
elif delta < -noise and -pct > args.threshold:
|
||||
verdict = f"faster: {c} is {pct:+.1f}% vs {a}"
|
||||
else:
|
||||
verdict = f"no significant change: {pct:+.1f}% (within noise)"
|
||||
print(f" {mode}: {a} {ma:.3f}s vs {c} {mc:.3f}s -> {verdict}")
|
||||
|
||||
if args.csv:
|
||||
_write_csv(args.csv, binaries, samples)
|
||||
print(f"\nraw per-run timings written to {args.csv}")
|
||||
return 0
|
||||
|
||||
|
||||
def _write_csv(path, binaries, samples):
|
||||
with open(path, "w") as f:
|
||||
f.write("binary,path,mode,run,warmup,seconds\n")
|
||||
for b in binaries:
|
||||
for mode in ("full", "noop"):
|
||||
for i, t in enumerate(samples[b.label][mode]):
|
||||
f.write(f"{b.label},{b.path},{mode},{i},{int(i == 0)},{t:.6f}\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("rsync_a", help="path to the first rsync binary (labelled A)")
|
||||
ap.add_argument("rsync_b", help="path to the second rsync binary (labelled B)")
|
||||
ap.add_argument("-n", "--runs", type=int, default=10,
|
||||
help="measured loops per binary (default: 10)")
|
||||
ap.add_argument("--warmup", type=int, default=1,
|
||||
help="leading runs per binary dropped from the stats to "
|
||||
"reduce cache impact (default: 1)")
|
||||
ap.add_argument("--mode", choices=("both", "full", "noop"), default="both",
|
||||
help="full=clean-dest copy, noop=re-sync scan overhead, "
|
||||
"both (default)")
|
||||
ap.add_argument("--rsync-args", default="-aH",
|
||||
help="rsync flags for the timed transfer (default: -aH)")
|
||||
ap.add_argument("--threshold", type=float, default=2.0,
|
||||
help="percent slowdown above run-to-run noise before a "
|
||||
"regression is flagged (default: 2.0)")
|
||||
# Tree-generation knobs (mirror gentestdata.py).
|
||||
ap.add_argument("--src", default=None,
|
||||
help="benchmark this existing tree instead of generating one")
|
||||
ap.add_argument("-f", "--files", type=int, default=10000,
|
||||
help="number of regular files to generate (default: 10000)")
|
||||
ap.add_argument("-s", "--total-size", type=parse_size, default="500M",
|
||||
help="total size of all regular files (default: 500M)")
|
||||
ap.add_argument("-d", "--depth", type=int, default=10,
|
||||
help="maximum directory tree depth (default: 10)")
|
||||
ap.add_argument("--dirs", type=int, default=None,
|
||||
help="number of directories (default: max(depth, files/20))")
|
||||
ap.add_argument("--symlinks", type=int, default=None,
|
||||
help="number of symlinks (default: files/20)")
|
||||
ap.add_argument("--hardlinks", type=int, default=None,
|
||||
help="number of hard links (default: files/20)")
|
||||
ap.add_argument("--seed", type=int, default=1,
|
||||
help="PRNG seed for a reproducible tree (default: 1)")
|
||||
ap.add_argument("--workdir", default=None,
|
||||
help="scratch root for src/dest dirs (default: a tempdir)")
|
||||
ap.add_argument("--drop-caches", action="store_true",
|
||||
help="sync + drop page/dentry/inode caches before each timed "
|
||||
"run (needs root; cold-cache measurement)")
|
||||
ap.add_argument("--timeout", type=float, default=3600.0,
|
||||
help="seconds before a single rsync run is abandoned "
|
||||
"(default: 3600)")
|
||||
ap.add_argument("--keep", action="store_true",
|
||||
help="keep the scratch tree on exit (default: remove it)")
|
||||
ap.add_argument("--csv", default=None,
|
||||
help="write raw per-run timings to this CSV file")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.runs < 2:
|
||||
ap.error("--runs must be >= 2 (need >=2 samples for a stddev)")
|
||||
args.rsync_args = shlex.split(args.rsync_args)
|
||||
|
||||
binaries = []
|
||||
for label, p in (("A", args.rsync_a), ("B", args.rsync_b)):
|
||||
path = os.path.abspath(p)
|
||||
if not (os.path.isfile(path) and os.access(path, os.X_OK)):
|
||||
ap.error(f"rsync {label} is not an executable file: {p}")
|
||||
binaries.append(Binary(label, path, rsync_version(path)))
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="rsync-perftest-",
|
||||
dir=args.workdir) if not args.keep or not args.workdir \
|
||||
else os.path.join(args.workdir, "rsync-perftest")
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
dest_full = os.path.join(workdir, "dest_full")
|
||||
dest_noop = os.path.join(workdir, "dest_noop")
|
||||
os.makedirs(dest_noop, exist_ok=True)
|
||||
|
||||
generated = None
|
||||
if args.src:
|
||||
src = os.path.abspath(args.src)
|
||||
if not os.path.isdir(src):
|
||||
ap.error(f"--src is not a directory: {args.src}")
|
||||
print(f"using existing source tree {src}")
|
||||
else:
|
||||
src = os.path.join(workdir, "src")
|
||||
print(f"generating source tree in {src} ...")
|
||||
t0 = time.monotonic()
|
||||
summary = generate_tree(src, args)
|
||||
generated = src
|
||||
print(f" {summary} ({time.monotonic() - t0:.1f}s)")
|
||||
|
||||
print(f"\nbenchmarking: warmup={args.warmup} runs={args.runs} mode={args.mode} "
|
||||
f"drop_caches={args.drop_caches}\n")
|
||||
rc = 1
|
||||
try:
|
||||
samples = run_benchmark(binaries, args, src, dest_full, dest_noop)
|
||||
rc = report(binaries, samples, args)
|
||||
except RuntimeError as e:
|
||||
print(f"\nbenchmark aborted: {e}", file=sys.stderr)
|
||||
rc = 2
|
||||
except KeyboardInterrupt:
|
||||
print("\ninterrupted", file=sys.stderr)
|
||||
rc = 130
|
||||
finally:
|
||||
if args.keep:
|
||||
print(f"\nkept scratch tree: {workdir}")
|
||||
else:
|
||||
for d in (dest_full, dest_noop, generated):
|
||||
if d and os.path.exists(d):
|
||||
safe_rmtree(d)
|
||||
# Remove the workdir itself if it is now empty (i.e. we made it).
|
||||
try:
|
||||
os.rmdir(workdir)
|
||||
except OSError:
|
||||
pass
|
||||
sys.exit(rc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# vim: sw=4 et ft=python
|
||||
@@ -15,7 +15,7 @@ import os
|
||||
from rsyncfns import (
|
||||
FROMDIR, TODIR,
|
||||
assert_same, make_data_file, makepath, rmtree, run_rsync, test_fail,
|
||||
test_skipped,
|
||||
test_skipped, test_xfail,
|
||||
)
|
||||
|
||||
src = FROMDIR
|
||||
@@ -103,10 +103,12 @@ seed_holey()
|
||||
run_rsync('-a', '--preallocate', '--sparse', f'{src}/', f'{TODIR}/')
|
||||
assert_same(TODIR / deep, src / deep, label='--preallocate --sparse content')
|
||||
if can_punch and allocated(TODIR / deep) >= os.path.getsize(TODIR / deep):
|
||||
test_fail(f"--preallocate --sparse left the file fully allocated "
|
||||
f"(allocated {allocated(TODIR / deep)} for a "
|
||||
f"{os.path.getsize(TODIR / deep)}-byte file); the preallocated "
|
||||
"extent's zero run was not punched into a hole")
|
||||
test_xfail(
|
||||
"3.4 stable lacks the --preallocate --sparse fix (4f5a5857): "
|
||||
"do_fallocate() does not report the preallocated length, so write_sparse() "
|
||||
"cannot punch the zero run and the file is left fully allocated "
|
||||
f"(allocated {allocated(TODIR / deep)} for a "
|
||||
f"{os.path.getsize(TODIR / deep)}-byte file)")
|
||||
|
||||
# --- --inplace --sparse update that introduces a zero run: do_punch_hole ----
|
||||
# (sparse_end's updating_basis_or_equiv branch punches the hole in place.)
|
||||
|
||||
@@ -57,11 +57,8 @@ def peer_client(args, label):
|
||||
"""Run the OLD client (RSYNC_PEER) and return (sent, received) wire bytes
|
||||
parsed from rsync's summary line. Fails the test on non-zero exit."""
|
||||
argv = shlex.split(RSYNC_PEER) + args
|
||||
# Force C locale: rsync groups the "sent/received N bytes" numbers per the
|
||||
# locale (e.g. de_DE uses '.' for thousands), which would break parsing.
|
||||
proc = subprocess.run(argv, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True,
|
||||
env={**os.environ, 'LC_ALL': 'C'})
|
||||
stderr=subprocess.STDOUT, text=True)
|
||||
print(proc.stdout, end='')
|
||||
if proc.returncode != 0:
|
||||
test_fail(f"{label}: old client exited {proc.returncode}")
|
||||
|
||||
@@ -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 ---------------------------------------------
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Valgrind suppressions for the rsync test suite.
|
||||
#
|
||||
# Every stanza here is a known-benign report, not a defect in rsync's own
|
||||
# logic. They are suppressed so that a --valgrind run fails only on a *real*
|
||||
# error. Pass to valgrind with --suppressions=testsuite/valgrind.supp
|
||||
# (runtests.py --valgrind does this automatically).
|
||||
#
|
||||
# The valgrind CI gate runs with --leak-check=no and so checks memory *errors*
|
||||
# only: rsync intentionally leaves file-list/socket/option memory unfreed at
|
||||
# exit, which makes a full leak check inherently noisy. The two Memcheck:Leak
|
||||
# stanzas below therefore only matter for a manual leak audit (--leak-check=
|
||||
# full); they cover the two leaks that are not rsync's own at-exit slack.
|
||||
|
||||
# popt alias strings are strdup'd while parsing arguments and only reclaimed
|
||||
# at process exit. One-time, bounded, unreachable-at-exit; bundled popt.
|
||||
{
|
||||
rsync-popt-alias-leak
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: definite
|
||||
fun:malloc
|
||||
fun:my_alloc
|
||||
fun:my_strdup
|
||||
fun:popt_unalias
|
||||
fun:parse_arguments
|
||||
fun:main
|
||||
}
|
||||
|
||||
# libxxhash returns an internally-aligned XXH3 state whose pointer is offset
|
||||
# from the malloc base, so valgrind sees only an interior pointer and reports
|
||||
# the one-time checksum state as "possibly lost". Alignment artifact.
|
||||
{
|
||||
xxhash-createstate-aligned
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
fun:malloc
|
||||
fun:XXH3_createState
|
||||
}
|
||||
|
||||
# libfakeroot's own SysV message padding in send_fakem(); the uninitialised
|
||||
# bytes are inside libfakeroot's mtext buffer, not rsync memory.
|
||||
{
|
||||
fakeroot-msgsnd-padding
|
||||
Memcheck:Param
|
||||
msgsnd(msgp->mtext)
|
||||
fun:msgsnd
|
||||
...
|
||||
obj:*libfakeroot*
|
||||
}
|
||||
12
token.c
12
token.c
@@ -292,10 +292,14 @@ static int32 simple_recv_token(int f, char **data)
|
||||
int32 i = read_int(f);
|
||||
if (i <= 0)
|
||||
return i;
|
||||
/* A literal run may exceed CHUNK_SIZE: some peers (e.g. the
|
||||
* acrosync library) use a 64k block size. The loop below reads
|
||||
* the run CHUNK_SIZE bytes at a time, so read_buf never writes
|
||||
* past the static CHUNK_SIZE buffer regardless of i. */
|
||||
/* simple_send_token caps each literal chunk at CHUNK_SIZE;
|
||||
* reject anything larger so a hostile peer cannot drive the
|
||||
* read_buf below past our static CHUNK_SIZE buffer. */
|
||||
if (i > CHUNK_SIZE) {
|
||||
rprintf(FERROR, "invalid uncompressed token length %ld [%s]\n",
|
||||
(long)i, who_am_i());
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
residue = i;
|
||||
}
|
||||
|
||||
|
||||
12
wildtest.c
12
wildtest.c
@@ -163,17 +163,14 @@ main(int argc, char **argv)
|
||||
flag[i] = 0;
|
||||
else
|
||||
flag[i] = -1;
|
||||
if (!*s || (*++s != ' ' && *s != '\t'))
|
||||
if (*++s != ' ' && *s != '\t')
|
||||
flag[i] = -1;
|
||||
if (flag[i] < 0) {
|
||||
fprintf(stderr, "Invalid flag syntax on line %d of %s:\n%s",
|
||||
line, *argv, buf);
|
||||
exit(1);
|
||||
}
|
||||
if (*s)
|
||||
s++;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
while (*++s == ' ' || *s == '\t') {}
|
||||
}
|
||||
for (i = 0; i <= 1; i++) {
|
||||
if (*s == '\'' || *s == '"' || *s == '`') {
|
||||
@@ -197,10 +194,7 @@ main(int argc, char **argv)
|
||||
while (*++s && *s != ' ' && *s != '\t' && *s != '\n') {}
|
||||
end[i] = s;
|
||||
}
|
||||
if (*s)
|
||||
s++;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
while (*++s == ' ' || *s == '\t') {}
|
||||
}
|
||||
*end[0] = *end[1] = '\0';
|
||||
run_test(line, flag[0],
|
||||
|
||||
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