Compare commits

..
Author SHA1 Message Date
Andrew Tridgell f7ebb57c5a update maintainer address
use rsync.project@gmail.com
2025-01-15 08:20:12 +11:00
664 changed files with 8808 additions and 69869 deletions

No files matched your search

-16
View File
@@ -1,17 +1 @@
* text=auto eol=lf
# The rsync-web/ subdirectory holds the project website source content
# (mirrors what gets pushed to https://rsync.samba.org). Exclude it from
# `git archive` output so the release source tarball produced by
# packaging/release.py step_7_tarball does not bloat with HTML the
# tarball doesn't need.
/rsync-web/ export-ignore
# old_versions/ holds static binaries of historical rsync releases, used by the
# version-mixing test suite (.github/workflows/ubuntu-version-mix.yml) to run
# the current code against a real old peer over the daemon / remote-shell.
# Mark the binaries as binary so the `text=auto eol=lf` rule above can't try to
# normalise line endings and corrupt them; export-ignore keeps them out of the
# release source tarball.
/old_versions/rsync_* binary
/old_versions/rsync_* export-ignore
-4
View File
@@ -1,4 +0,0 @@
# These are supported funding model platforms
github: RsyncProject
patreon: AndrewTridgell
-46
View File
@@ -1,46 +0,0 @@
name: Lint GitHub Actions workflows
# Static-check the workflow YAML with rhysd/actionlint. Catches missing
# secrets, bad expressions, expression-type errors, unsupported runner
# images, and (via embedded shellcheck) common pitfalls in `run:` scripts.
# Trigger only on changes under .github/workflows/ so the rest of the
# matrix isn't billed when nothing here moves.
on:
push:
branches: [ master ]
paths:
- '.github/workflows/*.yml'
- '.github/actionlint.yaml'
- '.github/actionlint.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths:
- '.github/workflows/*.yml'
- '.github/actionlint.yaml'
- '.github/actionlint.yml'
permissions:
contents: read
jobs:
actionlint:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: actionlint
steps:
- uses: actions/checkout@v4
- name: install actionlint
# Pin a version so this job is reproducible; bump deliberately.
# The download script verifies a SHA256 of the release tarball.
run: |
bash <(curl --proto '=https' --tlsv1.2 -fsSL \
https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) \
1.7.12
echo "$PWD" >>"$GITHUB_PATH"
- name: actionlint --version
run: actionlint -version
- name: actionlint .github/workflows/*.yml
run: actionlint -color
-86
View File
@@ -1,86 +0,0 @@
name: Test rsync on AlmaLinux 8
# Older-LTS coverage on the Fedora/RHEL family to help with backporting
# security fixes. AlmaLinux 8 is the RHEL 8 rebuild and is the oldest
# active LTS in this family (RHEL 8 full support runs to 2029).
# GitHub Actions has no native runner for this family, so the job runs
# inside an almalinux:8 container hosted on ubuntu-latest.
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/almalinux-8-build.yml'
schedule:
- cron: '42 8 * * *'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
container:
image: almalinux:8
name: Test rsync on AlmaLinux 8
steps:
- name: install git
# actions/checkout needs git in the container before the checkout step.
run: dnf -y install git
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
# PowerTools is needed for libzstd-devel etc; xxhash and lz4 dev
# headers live in EPEL on RHEL 8. The default python3 on RHEL 8
# is 3.6, which is too old for runtests.py (uses capture_output=
# / text= introduced in 3.7), so install python39 and point
# /usr/bin/python3 at it.
run: |
dnf -y install epel-release
dnf config-manager --set-enabled powertools
dnf -y install gcc gcc-c++ make autoconf automake m4 \
python39 python39-pip diffutils \
openssl openssl-devel \
attr libattr-devel acl libacl-devel \
zstd libzstd-devel \
lz4 lz4-devel \
xxhash xxhash-devel
alternatives --set python3 /usr/bin/python3.9
pip3 install commonmark
- name: configure
run: ./configure --with-rrsync
- name: make
run: make
- name: info
run: ./rsync --version
- name: check
# In the container we already run as root, so no sudo. The
# 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=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt 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
- name: ssl file list
run: ./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: almalinux-8-bin
path: |
rsync
rsync-ssl
rsync.1
rsync-ssl.1
rsyncd.conf.5
rrsync.1
rrsync
-124
View File
@@ -1,124 +0,0 @@
name: Build static rsync for Android
# Cross-compiles statically-linked rsync binaries with the Android NDK,
# suitable for dropping onto a phone (adb push / Termux) with no shared
# libraries. arm64-v8a covers all modern phones; armeabi-v7a covers older
# 32-bit devices. The binaries are uploaded as workflow artifacts.
#
# These are cross-compiled, so the test suite can't run here; we sanity
# check that each binary is the right architecture, is static, and that
# it executes (`--version`) under qemu-user.
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/android-static-build.yml'
schedule:
- cron: '42 8 * * 1'
workflow_dispatch:
env:
# Minimum supported API level. 24 (Android 7.0) runs on every modern
# phone while keeping broad reach; bump if you need newer Bionic APIs.
ANDROID_API: 24
jobs:
build:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: ${{ matrix.abi }}
strategy:
fail-fast: false
matrix:
include:
- abi: arm64-v8a # modern phones
triple: aarch64-linux-android
qemu: qemu-aarch64-static
- abi: armeabi-v7a # older 32-bit phones
triple: armv7a-linux-androideabi
qemu: qemu-arm-static
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install build prerequisites
run: sudo apt-get update && sudo apt-get install -y autoconf automake gawk qemu-user-static
- name: Configure and build (${{ matrix.abi }})
shell: bash
run: |
set -euo pipefail
NDK="${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_ROOT}"
TC="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
export CC="$TC/${{ matrix.triple }}${ANDROID_API}-clang"
export AR="$TC/llvm-ar" RANLIB="$TC/llvm-ranlib" STRIP="$TC/llvm-strip"
export CFLAGS="-O2" LDFLAGS="-static"
# Bionic doesn't declare lchmod()/lutimes() until API 36, but the
# symbols link, so configure mis-detects them -- force them off so
# rsync uses its fallbacks. The other cache vars restore values
# that configure can't probe when cross-compiling (Android runs a
# normal Linux kernel, so these match the native Linux result).
export ac_cv_func_lchmod=no ac_cv_func_lutimes=no \
rsync_cv_HAVE_SOCKETPAIR=yes \
rsync_cv_MKNOD_CREATES_FIFOS=yes \
rsync_cv_MKNOD_CREATES_SOCKETS=yes
# Self-contained build: drop optional external libraries so the
# static binary needs nothing at runtime. rsync keeps md5/md4
# checksums and its bundled zlib.
./configure --host=${{ matrix.triple }} --build=x86_64-pc-linux-gnu \
--enable-ipv6 \
--disable-zstd --disable-lz4 --disable-xxhash --disable-openssl \
--disable-iconv --disable-iconv-open \
--disable-acl-support --disable-xattr-support \
--disable-md2man --disable-roll-simd \
--with-included-popt --with-included-zlib
# Generate the awk-built headers serially first so the parallel
# build can't race on proto.h <- daemon-parm.h.
make proto.h
make -j"$(nproc)" rsync
"$STRIP" rsync
- name: Verify binary
shell: bash
run: |
set -euo pipefail
file rsync
# Gate: must be a statically-linked executable (no interpreter).
file rsync | grep -q "statically linked"
if file rsync | grep -q "dynamically linked"; then
echo "ERROR: binary is not static" >&2; exit 1
fi
# Best-effort: confirm it actually runs under qemu-user.
${{ matrix.qemu }} ./rsync --version | head -3 || \
echo "WARNING: qemu smoke test did not run cleanly (check on a real device)"
- name: Package
shell: bash
run: |
set -euo pipefail
VER=$(sed -n 's/.*RSYNC_VERSION "\([^"]*\)".*/\1/p' version.h)
out="rsync-${VER}-android-${{ matrix.abi }}"
mkdir -p dist
cp rsync "dist/$out"
( cd dist && sha256sum "$out" > "$out.sha256" )
echo "ARTIFACT_NAME=rsync-android-${{ matrix.abi }}" >>"$GITHUB_ENV"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: ${{ env.ARTIFACT_NAME }}
path: dist/
-75
View File
@@ -1,75 +0,0 @@
name: rsync ASan+UBSan (clang)
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/asan-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
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:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
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 --enable-strict-confinement
- 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
-75
View File
@@ -1,75 +0,0 @@
name: Coverage (Ubuntu)
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/coverage.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/coverage.yml'
schedule:
- cron: '42 9 * * 1'
workflow_dispatch:
jobs:
coverage:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: gcov coverage
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl gcovr
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --enable-coverage --with-rrsync
- name: make
run: make
- name: info
run: rsync --version
# Two coverage runs: the default pipe transport, then a second pass over a
# real loopback rsyncd (--use-tcp) which also exercises the require_tcp-only
# tests. gcovr's --print-summary line/branch/decision totals go to the step
# log (and the job summary below), so the numbers are visible in CI.
# `make coverage` exits with the suite's status, so a regression fails CI.
- name: coverage (pipe transport)
run: |
set -o pipefail
sudo make coverage 2>&1 | tee cov-pipe.log
- name: coverage (TCP transport)
run: |
set -o pipefail
sudo make coverage-tcp 2>&1 | tee cov-tcp.log
- name: coverage summary
if: always()
run: |
{
echo "## gcov coverage"
echo "### Pipe transport (\`make coverage\`)"
echo '```'
grep -E '^(lines|functions|branches|decisions):' cov-pipe.log || echo '(no summary -- see step log)'
echo '```'
echo "### TCP transport (\`make coverage-tcp\`)"
echo '```'
grep -E '^(lines|functions|branches|decisions):' cov-tcp.log || echo '(no summary -- see step log)'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: upload HTML reports
if: always()
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: coverage-html
path: |
coverage
coverage-tcp
+2 -15
View File
@@ -7,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/cygwin-build.yml'
@@ -16,9 +16,6 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: windows-2022
name: Test rsync on Cygwin
steps:
@@ -42,22 +39,12 @@ jobs:
- name: info
run: bash -c '/usr/local/bin/rsync --version'
- name: check
# chown-fake / devices-fake / xattrs / xattrs-hlink now RUN on Cygwin
# (rsyncfns.py drives xattrs via getfattr/setfattr from the `attr`
# package installed above), verified on a real Cygwin host. The real
# chown/devices tests still skip (need root/mknod), as do the
# RESOLVE_BENEATH symlink-race tests.
run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt 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.
run: bash -c './runtests.py --rsync-bin=`pwd`/rsync.exe --use-tcp -j 8'
run: bash -c 'RSYNC_EXPECT_SKIPPED=acls-default,acls,chown,devices,dir-sgid,protected-regular make check'
- name: ssl file list
run: bash -c 'PATH="/usr/local/bin:$PATH" rsync-ssl --no-motd download.samba.org::rsyncftp/ || true'
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: cygwin-bin
path: |
rsync.exe
-73
View File
@@ -1,73 +0,0 @@
name: Test fleettest harness
# Bitrot check for testsuite/fleettest.py (the developer fleet CI harness).
# fleettest is meant to be run by developers on a modern Ubuntu box, so this
# job runs only on ubuntu-latest: it stands up a one-host "fleet" of two
# targets that both ssh to localhost and runs a real fleettest pass against it.
# It does not run on the BSD/Solaris/macOS/Cygwin matrix.
on:
push:
branches: [ master ]
paths:
- 'testsuite/fleettest.py'
- '.github/workflows/fleettest.yml'
- 'runtests.py'
- 'testsuite/skiplist/**'
- 'testsuite/skiplist-spec_test.py'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths:
- 'testsuite/fleettest.py'
- '.github/workflows/fleettest.yml'
- 'runtests.py'
- 'testsuite/skiplist/**'
- 'testsuite/skiplist-spec_test.py'
workflow_dispatch:
schedule:
- cron: '17 7 * * 1'
jobs:
fleettest:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: fleettest against localhost
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y gcc g++ gawk autoconf automake \
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev \
python3-cmarkgfm openssl rsync openssh-server
- name: set up ssh to localhost
run: |
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
sudo systemctl start ssh || sudo service ssh start
# fleettest connects with `ssh -o BatchMode=yes localhost`, which won't
# answer a host-key prompt -- so pre-trust localhost in known_hosts.
ssh-keyscan -H localhost 127.0.0.1 >> ~/.ssh/known_hosts 2>/dev/null
ssh -o BatchMode=yes -o ConnectTimeout=15 localhost 'echo ssh-to-localhost-ok'
- name: write localhost fleet config
run: |
cat > fleettest-ci.json <<'EOF'
{ "targets": [
{ "name": "local-a", "ssh_host": "localhost", "workflow": "none.yml",
"configure_flags": [], "builddir": "rsync-citest-a", "privilege": "sudo" },
{ "name": "local-b", "ssh_host": "localhost", "workflow": "none.yml",
"configure_flags": [], "builddir": "rsync-citest-b", "privilege": "sudo" }
] }
EOF
- name: fleettest --list (config sanity)
run: python3 testsuite/fleettest.py --fleet fleettest-ci.json --list
- name: run fleettest against localhost
# Two targets both on localhost exercise the parallel multi-target path
# and the per-run dir / port isolation; exit 0 iff every cell is OK.
run: python3 testsuite/fleettest.py --fleet fleettest-ci.json --timing
+2 -8
View File
@@ -7,18 +7,15 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/freebsd-build.yml'
schedule:
- cron: '42 8 * * 1'
- cron: '42 8 * * *'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on FreeBSD
steps:
@@ -37,13 +34,10 @@ jobs:
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: freebsd-bin
path: |
rsync
+6 -22
View File
@@ -7,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/macos-build.yml'
@@ -16,9 +16,6 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: macos-latest
name: Test rsync on macOS
steps:
@@ -28,15 +25,10 @@ jobs:
- name: prep
run: |
brew install automake openssl xxhash zstd lz4
pip3 install --user --break-system-packages commonmark
echo "$(brew --prefix)/bin" >>"$GITHUB_PATH"
sudo pip3 install commonmark
echo "/usr/local/bin" >>$GITHUB_PATH
- name: configure
run: |
BREW_PREFIX=$(brew --prefix)
OPENSSL_PREFIX=$(brew --prefix openssl)
CPPFLAGS="-I${BREW_PREFIX}/include -I${OPENSSL_PREFIX}/include" \
LDFLAGS="-L${BREW_PREFIX}/lib -L${OPENSSL_PREFIX}/lib" \
./configure --with-rrsync
run: CPPFLAGS=-I/usr/local/opt/openssl/include/ LDFLAGS=-L/usr/local/opt/openssl/lib/ ./configure --with-rrsync
- name: make
run: make
- name: install
@@ -44,20 +36,12 @@ jobs:
- name: info
run: rsync --version
- name: check
# 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=@testsuite/skiplist/common.txt,@testsuite/skiplist/macos.txt 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.
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
run: sudo RSYNC_EXPECT_SKIPPED=acls-default,chmod-temp-dir,chown-fake,devices-fake,dir-sgid,protected-regular,xattrs-hlink,xattrs make check
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
retention-days: 45
name: macos-bin
path: |
rsync
-56
View File
@@ -1,56 +0,0 @@
name: Test rsync on NetBSD
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/netbsd-build.yml'
schedule:
- cron: '42 8 * * 1'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on NetBSD
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Test in NetBSD VM
id: test
uses: vmactions/netbsd-vm@v1
with:
usesh: true
prepare: |
PATH=/usr/sbin:$PATH pkg_add autoconf automake python312
ln -sf /usr/pkg/bin/python3.12 /usr/pkg/bin/python3
run: |
uname -a
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: netbsd-bin
path: |
rsync
rsync-ssl
rsync.1
rsync-ssl.1
rsyncd.conf.5
rrsync.1
rrsync
-72
View File
@@ -1,72 +0,0 @@
name: Test rsync on OpenBSD
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/openbsd-build.yml'
schedule:
- cron: '42 8 * * 1'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on OpenBSD
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Test in OpenBSD VM
id: test
uses: vmactions/openbsd-vm@v1
with:
usesh: true
prepare: |
pkg_add -I bash autoconf%2.71 automake%1.16
run: |
uname -a
export AUTOCONF_VERSION=2.71
export AUTOMAKE_VERSION=1.16
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
# The flipper (symlink-race) tests are excluded on OpenBSD, as on the
# fleet's OpenBSD box: this kernel has a connect()-under-rename-load
# lost-wakeup and an FFS rename-storm corruption that hang them to
# the 300s timeout for non-rsync reasons (see
# dev-notes/openbsd-connect-lost-wakeup-report.txt); the protections
# they exercise are verified on the Linux/BSD boxes.
export RSYNC_EXCLUDE=acl-symlink-race,sender-readlink-atfd,sender-remove-source-secure
make check
# The --use-tcp daemon tests run at -j2 here (vs -j8 elsewhere): this
# job runs inside a nested VM, and at -j8 the many concurrent loopback
# daemons occasionally lose a connection-handshake timing race under
# that resource pressure, hanging one test to the 300s timeout. It is
# an environment artifact, not an rsync bug (the handshake is
# deadlock-free and unreproducible elsewhere, even pinned to 1 CPU at
# -j8); -j2 keeps the VM from over-subscribing. The pipe `make check`
# above stays at the default parallelism.
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 2
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: openbsd-bin
path: |
rsync
rsync-ssl
rsync.1
rsync-ssl.1
rsyncd.conf.5
rrsync.1
rrsync
-94
View File
@@ -1,94 +0,0 @@
name: rsync scan-build (clang analyzer)
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/scan-build.yml'
workflow_dispatch:
jobs:
# GATING run: pinned clang-18 on a pinned runner so the checker set -- and
# thus the expected zero -- is deterministic. The tree is kept clean for
# clang-18, so --status-bugs (non-zero exit on any report) fails the build
# when a new finding appears. Pin both the analyzer (clang-18/clang-tools-18)
# and the runner (ubuntu-24.04, whose apt repos carry those packages).
gate-clang18:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-24.04
name: scan-build gate (clang-18, pinned)
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
run: |
sudo apt-get update
sudo apt-get install -y clang-18 clang-tools-18 acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libpopt-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-18 ./configure --with-rrsync --disable-md2man
- name: scan-build (gating)
# --status-bugs makes scan-build exit non-zero if it finds ANY report.
# pipefail + 'exit $status' propagate that through the tee so the job goes
# red while still printing the summary; the report uploads for triage.
run: |
set -o pipefail
status=0
scan-build-18 --status-bugs -o "$PWD/scan-report" make check-progs -j"$(nproc)" 2>&1 | tee scan-build.out || status=$?
echo '## scan-build gate (clang-18)' >>"$GITHUB_STEP_SUMMARY"
grep -E 'scan-build: .* bugs? found|scan-build: No bugs found' scan-build.out >>"$GITHUB_STEP_SUMMARY" || true
exit $status
- name: upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: scan-build-report-clang18
path: scan-report
if-no-files-found: ignore
# INFORMATIONAL run: whatever clang ubuntu-latest currently ships. Newer
# clang releases enable extra, FP-heavy checkers (e.g. unix.Chroot
# "no chdir after chroot", alpha.unix.Stream) that the gate deliberately
# avoids, so this is NOT a gate (no --status-bugs). It surfaces what the
# newest analyzer sees -- useful for spotting genuine new findings before a
# gate bump -- without blocking merges. continue-on-error keeps a noisy or
# broken run from affecting the workflow's required status.
informational-latest:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: scan-build (latest clang, informational)
continue-on-error: true
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 libpopt-dev openssl
- name: configure (under scan-build)
run: scan-build ./configure --with-rrsync --disable-md2man
- name: scan-build (informational)
run: |
scan-build -o "$PWD/scan-report" make check-progs -j"$(nproc)" 2>&1 | tee scan-build.out
echo '## scan-build informational (latest clang)' >>"$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-latest
path: scan-report
if-no-files-found: ignore
+2 -8
View File
@@ -7,18 +7,15 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/solaris-build.yml'
schedule:
- cron: '42 8 * * 1'
- cron: '42 8 * * *'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: Test rsync on Solaris
steps:
@@ -37,13 +34,10 @@ jobs:
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
make
./rsync --version
make check
./runtests.py --rsync-bin=`pwd`/rsync --use-tcp -j 8
./rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: solaris-bin
path: |
rsync
-68
View File
@@ -1,68 +0,0 @@
name: Test rsync on Ubuntu 22.04
# Older-LTS coverage to help with backporting security fixes. ubuntu-22.04
# is currently the oldest GitHub Actions runner image (20.04 was retired
# in April 2025).
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-22.04-build.yml'
schedule:
- cron: '42 8 * * *'
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-22.04
name: Test rsync on Ubuntu 22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
run: |
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync
- name: make
run: make
- name: install
run: sudo make install
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
- name: check30
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check30
- name: check29
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt 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.
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: ubuntu-22.04-bin
path: |
rsync
rsync-ssl
rsync.1
rsync-ssl.1
rsyncd.conf.5
rrsync.1
rrsync
+6 -32
View File
@@ -7,7 +7,7 @@ on:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-build.yml'
@@ -16,10 +16,7 @@ on:
jobs:
test:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Test rsync on Ubuntu
steps:
- uses: actions/checkout@v4
@@ -28,7 +25,7 @@ jobs:
- name: prep
run: |
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
echo "/usr/local/bin" >>$GITHUB_PATH
- name: configure
run: ./configure --with-rrsync
- name: make
@@ -38,39 +35,16 @@ jobs:
- name: info
run: rsync --version
- name: check
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check
- name: check30
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check30
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check30
- name: check29
run: sudo RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/proto29.txt 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
# sockets); this run exercises the real TCP accept/auth path. Skip-set
# is env-dependent here (chroot-acl), so leave RSYNC_EXPECT_SKIPPED unset.
run: sudo ./runtests.py --rsync-bin="$PWD/rsync" --use-tcp -j 8
- name: check (non-root, targeted)
# Every run above is root (sudo), so privilege-sensitive tests never hit
# their non-root path. Run those here as the unprivileged 'runner' user
# (NO sudo). Explicit test names make runtests.py full_run False, so
# RSYNC_EXPECT_SKIPPED is bypassed -- no per-platform skip list needed.
# daemon-namecvt-empty-response -- REQUIRES non-root (skips as root by
# design); the only test with no other CI coverage (Benjamin #2).
# ownership-depth -- non-root takes the group-only remap path.
# daemon -- non-root takes the default-config path.
# CONVENTION: a new test that requires/meaningfully exercises a non-root
# path must be added to the list below (kept in sync with the fleet
# harness's nonroot_tests).
run: |
sudo rm -rf testtmp # prior root steps left it root-owned
./runtests.py --rsync-bin="$PWD/rsync" \
daemon-namecvt-empty-response ownership-depth daemon
run: sudo RSYNC_EXPECT_SKIPPED=crtimes make check29
- name: ssl file list
run: rsync-ssl --no-motd download.samba.org::rsyncftp/ || true
- name: save artifact
uses: actions/upload-artifact@v4
with:
retention-days: 45
name: ubuntu-bin
path: |
rsync
-80
View File
@@ -1,80 +0,0 @@
name: Test rsync version mixing on Ubuntu
# Runs the CURRENT test suite with two different rsync binaries: the freshly
# built ./rsync as the client/driver, and a committed OLD static binary
# (old_versions/rsync_<ver>) as the daemon / remote-shell peer. This exercises
# real version mixing over the wire -- more convincing than --protocol forcing,
# which only makes the current binary speak an old protocol.
#
# Direction is fixed: the current binary always drives (only it understands the
# new test scripts); the old binary is only ever the server/daemon side. The
# reverse (old client driving new scripts) is not possible -- but one test,
# reverse-daemon-delta, swaps the roles internally (current build as the daemon,
# old binary as the client) to cover the backward-compat direction: a current
# daemon serving the installed base of old clients.
#
# The per-version manifest testsuite/expect/rsync_<ver>.expect lists exactly
# which tests run and each one's expected outcome (pass/skip/fail/xfail), so an
# old peer's known feature gaps are recorded rather than treated as breakage.
#
# All peers run in a SINGLE job (looped, not a matrix) so the PR shows one check
# line rather than one per version. Each peer/transport is a foldable ::group::
# in the log, and a failure annotates which peer/transport broke.
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/ubuntu-version-mix.yml'
schedule:
- cron: '52 8 * * 1'
jobs:
version-mix:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
runs-on: ubuntu-latest
name: rsync version-mix
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: prep
run: |
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev python3-cmarkgfm openssl
echo "/usr/local/bin" >>"$GITHUB_PATH"
- name: configure
run: ./configure --with-rrsync
- name: make
# check-progs builds rsync AND the test helper programs (tls, trimslash,
# t_unsafe, ...) that runtests.py requires; plain `make` does not.
run: make check-progs
- name: info
run: ./rsync --version | head -1
- name: version mixing (all peers, pipe + TCP transports)
run: |
rc=0
for peer in old_versions/rsync_*; do
chmod +x "$peer"
name=$(basename "$peer")
expect="testsuite/expect/$name.expect"
for transport in pipe tcp; do
tcp=()
[ "$transport" = tcp ] && tcp=(--use-tcp)
echo "::group::$name ($transport): $("$peer" --version | head -1)"
if ! ./runtests.py --rsync-bin="$PWD/rsync" --rsync-bin2="$PWD/$peer" \
--expect-result "$expect" "${tcp[@]}" -j 8; then
echo "::error::version-mix failed: $name ($transport)"
rc=1
fi
echo "::endgroup::"
done
done
exit $rc
-99
View File
@@ -1,99 +0,0 @@
name: Valgrind memcheck
on:
push:
branches: [ master ]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
pull_request:
types: [opened, synchronize, reopened, labeled]
paths-ignore:
- '.github/workflows/*.yml'
- '!.github/workflows/valgrind.yml'
schedule:
- cron: '17 4 * * *'
workflow_dispatch:
jobs:
memcheck:
# temporary gate: PR CI runs only for PRs labeled 'run-ci', to save CI
# minutes; labels need triage access, so fork PRs can't self-enable.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-ci')
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
-14
View File
@@ -43,19 +43,8 @@ aclocal.m4
/testrun
/trimslash
/t_unsafe
/t_acl
/t_chmod_secure
/t_rename_secure
/t_secure_relpath
/t_symlink_secure
/t_hashtable_overflow
/t_iwildmatch
/t_clean_fname
/t_safe_arg
/simdtest
/wildtest
/getfsdev
/t_safe_arg_main
/rounding.h
/doc/rsync.pdf
/doc/rsync.ps
@@ -63,12 +52,9 @@ aclocal.m4
/testsuite/chown-fake.test
/testsuite/devices-fake.test
/testsuite/xattrs-hlink.test
/testsuite/fleettest.json
/fleettest-logs
/patches
/patches.gen
/build
/auto-build-save
.deps
/*.exe
*.dSYM/
-28
View File
@@ -7,34 +7,6 @@ option to use if you want to just skip that feature. What follows are various
support libraries that you may want to install to build rsync with the maximum
features (the impatient can skip down to the package summary):
## Ubuntu users: skip the build, use the PPA
If you are on a currently supported Ubuntu series (jammy 22.04 LTS, noble
24.04 LTS, questing 25.10, resolute 26.04 LTS) and just want the latest
upstream rsync, the rsync project maintains a Launchpad PPA that tracks
stable releases:
> sudo add-apt-repository ppa:rsyncproject/rsync
> sudo apt update && sudo apt install rsync
See [the PPA page][ppa] for current build status across architectures.
[ppa]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync
To test the upcoming release instead, there is also a [`rsync-latest`
PPA][ppa-latest] that is rebuilt from the tip of the git master branch. These
are development snapshots whose version numbers (such as
`3.5.0~git20260601...`) deliberately sort below the matching stable release, so
the stable PPA above will never silently move you from a release onto a
snapshot. Use it for testing only -- it may contain unreleased changes:
> sudo add-apt-repository ppa:rsyncproject/rsync-latest
> sudo apt update && sudo apt install rsync
[ppa-latest]: https://launchpad.net/~rsyncproject/+archive/ubuntu/rsync-latest
The rest of this document covers building from source.
## The basic setup
You need to have a C compiler installed and optionally a C++ compiler in order
+30 -280
View File
@@ -18,9 +18,6 @@ CXXFLAGS=@CXXFLAGS@
EXEEXT=@EXEEXT@
LDFLAGS=@LDFLAGS@
LIBOBJDIR=lib/
AR=@AR@
ARFLAGS=cr
RANLIB=@RANLIB@
INSTALLCMD=@INSTALL@
INSTALLMAN=@INSTALL@
@@ -41,37 +38,31 @@ GENFILES=configure.sh aclocal.m4 config.h.in rsync.1 rsync.1.html \
rsync-ssl.1 rsync-ssl.1.html rsyncd.conf.5 rsyncd.conf.5.html \
@GEN_RRSYNC@
HEADERS=byteorder.h config.h errcode.h proto.h rsync.h ifuncs.h itypes.h inums.h \
lib/pool_alloc.h lib/mdigest.h lib/md-defines.h vfs/vfs.h
lib/pool_alloc.h lib/mdigest.h lib/md-defines.h
LIBOBJ=lib/wildmatch.o lib/compat.o lib/snprintf.o lib/mdfour.o lib/md5.o \
lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o lib/acl.o @LIBOBJS@
lib/permstring.o lib/pool_alloc.o lib/sysacls.o lib/sysxattrs.o @LIBOBJS@
zlib_OBJS=zlib/deflate.o zlib/inffast.o zlib/inflate.o zlib/inftrees.o \
zlib/trees.o zlib/zutil.o zlib/adler32.o zlib/compress.o zlib/crc32.o
OBJS1_NO_MAIN=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
util1.o util2.o checksum.o match.o log.o backup.o delete.o
OBJS1=$(OBJS1_NO_MAIN) main.o
OBJS1=flist.o rsync.o generator.o receiver.o cleanup.o sender.o exclude.o \
util1.o util2.o main.o checksum.o match.o syscall.o log.o backup.o delete.o
OBJS2=options.o io.o compat.o hlink.o token.o uidlist.o socket.o hashtable.o \
usage.o fileio.o batch.o clientname.o chmod.o acls.o xattrs.o
OBJS3=progress.o pipe.o @MD5_ASM@ @ROLL_SIMD@ @ROLL_ASM@
DAEMON_OBJ = params.o loadparm.o clientserver.o access.o connection.o authenticate.o
popt_OBJS= popt/popt.o popt/poptconfig.o \
popt_OBJS=popt/findme.o popt/popt.o popt/poptconfig.o \
popt/popthelp.o popt/poptparse.o popt/poptint.o
VFS_OBJ=vfs/vfs.o vfs/dirstack.o vfs/secure_open.o vfs/owner_walk.o vfs/dircache.o vfs/stat.o vfs/rename.o vfs/unlink.o vfs/open.o vfs/chmod.o vfs/symlink.o vfs/link.o vfs/mkdir.o vfs/chown.o vfs/mknod.o vfs/times.o vfs/fileio.o vfs/make_path.o vfs/copy_file.o vfs/robust.o
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a
OBJS=$(OBJS1) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
TLS_OBJ = tls.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@ libvfs.a
TLS_OBJ = tls.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/permstring.o lib/sysxattrs.o @BUILD_POPT@
# Programs we must have to run the test cases
CHECK_PROGS = rsync$(EXEEXT) tls$(EXEEXT) getgroups$(EXEEXT) getfsdev$(EXEEXT) \
testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) t_chmod_secure$(EXEEXT) \
t_rename_secure$(EXEEXT) t_symlink_secure$(EXEEXT) t_secure_relpath$(EXEEXT) t_acl$(EXEEXT) t_hashtable_overflow$(EXEEXT) t_iwildmatch$(EXEEXT) t_clean_fname$(EXEEXT) t_safe_arg$(EXEEXT) wildtest$(EXEEXT) simdtest$(EXEEXT)
testrun$(EXEEXT) trimslash$(EXEEXT) t_unsafe$(EXEEXT) wildtest$(EXEEXT)
CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \
testsuite/xattrs-hlink_test.py testsuite/exclude-lsh_test.py
CHECK_SYMLINKS = testsuite/chown-fake.test testsuite/devices-fake.test testsuite/xattrs-hlink.test
# Objects for CHECK_PROGS to clean
CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o t_chmod_secure.o t_rename_secure.o t_symlink_secure.o t_secure_relpath.o t_acl.o t_hashtable_overflow.o t_iwildmatch.o t_clean_fname.o t_safe_arg.o trimslash.o wildtest.o
# Compile-only feature-shape checks.
CHECK_COMPILE_OBJS=vfs-no-at-fdcwd.o
CHECK_OBJS=tls.o testrun.o getgroups.o getfsdev.o t_stub.o t_unsafe.o trimslash.o wildtest.o
# note that the -I. is needed to handle config.h when using VPATH
.c.o:
@@ -84,21 +75,6 @@ CHECK_COMPILE_OBJS=vfs-no-at-fdcwd.o
all: Makefile rsync$(EXEEXT) stunnel-rsyncd.conf @MAKE_RRSYNC@ @MAKE_MAN@
.PHONY: all
# Compile-check the pre-*at() portability tier. syscall.c's *at wrappers were
# split into vfs/, so compile every vfs source with the AT_FDCWD primitives
# undefined (via vfs/vfs_internal.h's RSYNC_TEST_NO_AT_FDCWD block) and confirm
# the fallback arms still build. A shell loop keeps this portable (BSD/Solaris
# make have no pattern rules); the last object compiled is left as the target.
# $(VFS_OBJ:.o=.c) is POSIX suffix substitution, portable across makes.
vfs-no-at-fdcwd.o: $(VFS_OBJ:.o=.c) $(HEADERS) vfs/vfs.h vfs/vfs_internal.h
@rm -f $@ $@.tmp
@for f in $(VFS_OBJ:.o=.c); do \
echo " no-AT_FDCWD compile-check: $$f"; \
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \
-DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/$$f -o $@.tmp || exit 1; \
done
@mv $@.tmp $@
.PHONY: install
install: all
-$(MKDIR_P) $(DESTDIR)$(bindir)
@@ -106,19 +82,12 @@ install: all
$(INSTALLCMD) -m 755 $(srcdir)/rsync-ssl $(DESTDIR)$(bindir)
-$(MKDIR_P) $(DESTDIR)$(mandir)/man1
-$(MKDIR_P) $(DESTDIR)$(mandir)/man5
for fn in rsync.1 rsync-ssl.1; do \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
done
for fn in rsyncd.conf.5; do \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man5; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man5; fi; \
done
if test -f rsync.1; then $(INSTALLMAN) -m 644 rsync.1 $(DESTDIR)$(mandir)/man1; fi
if test -f rsync-ssl.1; then $(INSTALLMAN) -m 644 rsync-ssl.1 $(DESTDIR)$(mandir)/man1; fi
if test -f rsyncd.conf.5; then $(INSTALLMAN) -m 644 rsyncd.conf.5 $(DESTDIR)$(mandir)/man5; fi
if test "$(with_rrsync)" = yes; then \
$(INSTALLCMD) -m 755 rrsync $(DESTDIR)$(bindir); \
fn=rrsync.1; \
if test -f $$fn; then $(INSTALLMAN) -m 644 $$fn $(DESTDIR)$(mandir)/man1; \
elif test -f $(srcdir)/$$fn; then $(INSTALLMAN) -m 644 $(srcdir)/$$fn $(DESTDIR)$(mandir)/man1; fi; \
if test -f rrsync.1; then $(INSTALLMAN) -m 644 rrsync.1 $(DESTDIR)$(mandir)/man1; fi; \
fi
install-ssl-daemon: stunnel-rsyncd.conf
@@ -133,21 +102,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)
@@ -156,22 +110,11 @@ rrsync: support/rrsync
$(OBJS): $(HEADERS)
$(CHECK_OBJS): $(HEADERS)
$(VFS_OBJ): $(HEADERS)
$(VFS_OBJ): vfs/vfs_internal.h
tls.o xattrs.o: lib/sysxattrs.h
# The VFS layer is bundled into a static archive linked last on every target so
# that moving a filesystem family between files never breaks a test harness link
# (the linker pulls only the members each program references).
libvfs.a: $(VFS_OBJ)
rm -f $@
$(AR) $(ARFLAGS) $@ $(VFS_OBJ)
$(RANLIB) $@
usage.o: version.h latest-year.h help-rsync.h help-rsyncd.h git-version.h default-cvsignore.h
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
@@ -227,63 +170,14 @@ getgroups$(EXEEXT): getgroups.o
getfsdev$(EXEEXT): getfsdev.o
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ getfsdev.o $(LIBS)
TRIMSLASH_OBJ = trimslash.o util2.o t_stub.o lib/compat.o lib/snprintf.o libvfs.a
TRIMSLASH_OBJ = trimslash.o syscall.o util2.o t_stub.o lib/compat.o lib/snprintf.o
trimslash$(EXEEXT): $(TRIMSLASH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(TRIMSLASH_OBJ) $(LIBS)
T_UNSAFE_OBJ = t_unsafe.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
T_UNSAFE_OBJ = t_unsafe.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
t_unsafe$(EXEEXT): $(T_UNSAFE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_UNSAFE_OBJ) $(LIBS)
T_HASHTABLE_OVERFLOW_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS)
T_IWILDMATCH_OBJ = t_iwildmatch.o lib/wildmatch.o
t_iwildmatch$(EXEEXT): $(T_IWILDMATCH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_IWILDMATCH_OBJ) $(LIBS)
T_CLEAN_FNAME_OBJ = t_clean_fname.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o libvfs.a
t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS)
# safe_arg lives in options.c alongside the whole option parser. Rather than
# rely on a non-portable linker --gc-sections to drop the parser (GNU ld only;
# macOS ld64 and the cygwin PE linker do not), link the real rsync objects so
# every dep resolves. t_safe_arg_main.o is main.c with main() renamed out, to
# supply main.c's globals while letting t_safe_arg.o provide the test's main().
# OBJS minus main.o is spelled out via OBJS1_NO_MAIN because $(filter-out) is
# GNU-make-only; BSD and Solaris make expand it to nothing.
t_safe_arg_main.o: main.c $(HEADERS)
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) -Dmain=rsync_unused_main -c $(srcdir)/main.c -o t_safe_arg_main.o
T_SAFE_ARG_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@ libvfs.a
t_safe_arg$(EXEEXT): $(T_SAFE_ARG_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SAFE_ARG_OBJ) $(LIBS)
T_CHMOD_SECURE_OBJ = t_chmod_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_chmod_secure$(EXEEXT): $(T_CHMOD_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CHMOD_SECURE_OBJ) $(LIBS)
T_RENAME_SECURE_OBJ = t_rename_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_rename_secure$(EXEEXT): $(T_RENAME_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_RENAME_SECURE_OBJ) $(LIBS)
T_SYMLINK_SECURE_OBJ = t_symlink_secure.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_symlink_secure$(EXEEXT): $(T_SYMLINK_SECURE_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SYMLINK_SECURE_OBJ) $(LIBS)
T_SECURE_RELPATH_OBJ = t_secure_relpath.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o libvfs.a
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
# Unit test for lib/acl.c: compares our fd/at ACL ops against the system libacl
# (linked via $(LIBS), which carries -lacl). lib/acl.o references no rsync
# globals, so this links with no stubs. Self-skips (exit 77) when built
# without SUPPORT_ACL_FD.
T_ACL_OBJ = t_acl.o lib/acl.o
t_acl$(EXEEXT): $(T_ACL_OBJ)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_ACL_OBJ) $(LIBS)
.PHONY: conf
conf: configure.sh config.h.in
@@ -373,11 +267,9 @@ rrsync.1: support/rrsync.1.md md-convert Makefile
.PHONY: clean
clean: cleantests
rm -f *~ $(OBJS) $(VFS_OBJ) libvfs.a $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_COMPILE_OBJS) $(CHECK_COMPILE_OBJS:.o=.o.tmp) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
rm -f *~ $(OBJS) $(CHECK_PROGS) $(CHECK_OBJS) $(CHECK_SYMLINKS) @MAKE_RRSYNC@ \
git-version.h rounding rounding.h *.old rsync*.1 rsync*.5 @MAKE_RRSYNC_1@ \
*.html daemon-parm.h help-*.h default-*.h proto.h proto.h-tstamp
rm -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda vfs/*.gcno vfs/*.gcda
rm -rf coverage coverage-tcp coverage-all coverage-fallback
.PHONY: cleantests
cleantests:
@@ -418,172 +310,30 @@ test: check
# catch Bash-isms earlier even if we're running on GNU. Of course, we
# might lose in the future where POSIX diverges from old sh.
# `make check` runs tests in parallel by default. Override with
# `make check CHECK_J=1` (serial) or any other value.
CHECK_J = 8
# Parallelism for `make coverage`. Defaults to the same as CHECK_J: the
# coverage build sets -fprofile-update=atomic (atomic in-memory counters) and
# gcc's libgcov serializes the per-source .gcda read-modify-write merge with a
# file lock, so concurrent rsync processes (incl. the forked sender/generator/
# receiver) accumulate exactly -- verified by a count-linearity check (a hot
# line accumulates identically at -j1 and -P16). Override with
# `make coverage COVERAGE_J=1` if your libgcov does not lock .gcda merges.
COVERAGE_J = $(CHECK_J)
# Output directory and extra runtests.py flags for `make coverage`. The
# `coverage-tcp` target reuses the coverage recipe with --use-tcp (real
# loopback rsyncd, which exercises the TCP accept/auth path and the
# require_tcp-only tests) and a separate output directory.
COVERAGE_DIR = coverage
COVERAGE_RUNFLAGS =
# Excluded from the coverage report so the percentages reflect rsync's own
# runtime source. Three buckets:
# (1) Bundled third-party code rsync ships but does not own: zlib/, popt/, and
# the named lib/ imports (PostgreSQL getaddrinfo, ISC inet_ntop/inet_pton,
# standalone getpass). The other lib/*.c are rsync's own and stay in.
# (2) Test-helper / build-time programs that link against rsync objects but are
# not the rsync runtime: t_*.c, tls.c, wildtest.c, testrun.c, getgroups.c,
# getfsdev.c, trimslash.c, rounding.c. These have their own main() and are
# either driven directly by a test (counted there) or are configure-time
# probes; counting them as "rsync uncovered" is noise.
# (3) Compile-time-dead fallbacks under this build's config.h: lib/md5.c (the
# reference md5 -- openssl's EVP path is used when HAVE_OPENSSL) and
# lib/snprintf.c (only the #include line survives under
# HAVE_C99_VSNPRINTF). Covering these would mean a separate non-openssl /
# non-C99 build, which is out of scope for this report.
COVERAGE_EXCLUDE = -e '(^|/)zlib/' -e '(^|/)popt/' \
-e '(^|/)lib/(getaddrinfo|getpass|inet_ntop|inet_pton)\.' \
-e '(^|/)(t_[a-z_]+|tls|wildtest|testrun|getgroups|getfsdev|trimslash|rounding)\.c$$' \
-e '(^|/)lib/(md5|snprintf)\.c$$'
# Build everything the test suite needs (rsync + helper programs + symlinks)
# WITHOUT running it. Used by CI jobs that invoke runtests.py directly with
# custom options (e.g. the version-mix workflow's --rsync-bin2/--expect-result).
.PHONY: check-progs
check-progs: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
.PHONY: check
check: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J)
check: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh
.PHONY: check29
check29: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=29
check29: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=29
.PHONY: check30
check30: all $(CHECK_PROGS) $(CHECK_COMPILE_OBJS) $(CHECK_SYMLINKS)
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(CHECK_J) --protocol=30
# Whole-suite gcov coverage report (HTML, with branch + decision coverage).
# Requires a build configured with --enable-coverage and the `gcovr` tool
# (pip install gcovr). Runs the suite in parallel (COVERAGE_J, default CHECK_J):
# this is safe because the coverage build uses -fprofile-update=atomic and
# libgcov locks the per-source .gcda during its merge, so concurrent rsync
# processes accumulate exactly (see COVERAGE_J above). Use COVERAGE_J=1 if your
# toolchain's libgcov does not lock .gcda merges.
.PHONY: coverage
coverage: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
@case '$(CFLAGS)' in *--coverage*) ;; \
*) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac
@command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; }
find . -name '*.gcda' -delete
@# Daemon modules with `uid = <non-root>` setuid the per-connection child
@# (and so the forked generator/receiver), which then cannot create or
@# merge .gcda files in a root-owned build dir -- silently dropping ALL
@# coverage from those processes. Make every .gcno's directory
@# world-writable so any uid can create the sibling .gcda, and set a
@# default ACL of o::rw so the .gcda are world-mergeable regardless of
@# the creator's umask (every test process resets umask to 022 via
@# rsyncfns.py, so a Makefile-level `umask 0` would not survive).
@find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \
while read d; do \
chmod a+rwx "$$d"; \
setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \
done
@rc=0; "$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $(COVERAGE_RUNFLAGS) || rc=$$?; \
rm -rf $(COVERAGE_DIR) && mkdir -p $(COVERAGE_DIR); \
gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
--html-details -o $(COVERAGE_DIR)/index.html . || exit $$?; \
echo "Coverage report written to $(COVERAGE_DIR)/index.html"; \
if test $$rc != 0; then \
echo "*** test suite FAILED (status $$rc) -- coverage report still written above"; \
fi; \
exit $$rc
# Same as `make coverage` but with the daemon tests run over a real loopback
# rsyncd (--use-tcp), into a separate report directory.
.PHONY: coverage-tcp
coverage-tcp:
$(MAKE) coverage COVERAGE_RUNFLAGS=--use-tcp COVERAGE_DIR=coverage-tcp
# Comprehensive single report: run the suite under several configurations,
# accumulating into the shared .gcda counters (NOT cleared between runs), then
# emit one merged, rsync-scoped report. Covers the default (pipe) transport, the
# protocol-29/30 compat branches, and the real-TCP daemon path (which also runs
# the require_tcp-only tests). Run under sudo to additionally cover root-only
# paths (devices, chown, use-chroot, protected-regular). Local target -- CI uses
# the plain `coverage`/`coverage-tcp` targets.
.PHONY: coverage-all
coverage-all: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
@case '$(CFLAGS)' in *--coverage*) ;; \
*) echo "*** not a coverage build; reconfigure with --enable-coverage"; exit 1 ;; esac
@command -v gcovr >/dev/null 2>&1 || { echo "*** gcovr not found (pip install gcovr)"; exit 1; }
find . -name '*.gcda' -delete
@# See the `coverage` target above for why: setuid'd daemon children must
@# be able to create/merge .gcda owned by a different uid.
@find . -name '*.gcno' -printf '%h\n' 2>/dev/null | sort -u | \
while read d; do \
chmod a+rwx "$$d"; \
setfacl -m 'd:u::rwx,d:g::rwx,d:o::rwx' "$$d" 2>/dev/null || true; \
done
@rc=0; \
for cfg in '' '--protocol=30' '--protocol=29' '--use-tcp'; do \
echo "===== coverage-all: runtests.py $$cfg ====="; \
"$(srcdir)/runtests.py" --rsync-bin="`pwd`/rsync$(EXEEXT)" -j $(COVERAGE_J) $$cfg || rc=$$?; \
done; \
rm -rf coverage-all && mkdir -p coverage-all; \
gcovr --root $(srcdir) $(COVERAGE_EXCLUDE) --decisions --print-summary \
--gcov-ignore-parse-errors=negative_hits.warn_once_per_file \
--html-details -o coverage-all/index.html . || exit $$?; \
echo "Merged coverage report written to coverage-all/index.html"; \
if test $$rc != 0; then \
echo "*** some suite runs FAILED (status $$rc) -- report still written above"; \
fi; \
exit $$rc
# Coverage for the portable (non-openat2) resolver tier. Requires a SEPARATE
# build configured with --enable-coverage --disable-openat2: its .gcno differ
# from the openat2 build, so this report cannot be merged with the others.
.PHONY: coverage-fallback
coverage-fallback:
$(MAKE) coverage COVERAGE_DIR=coverage-fallback
check30: all $(CHECK_PROGS) $(CHECK_SYMLINKS)
rsync_bin=`pwd`/rsync$(EXEEXT) $(srcdir)/runtests.sh --protocol=30
wildtest.o: wildtest.c t_stub.o lib/wildmatch.c rsync.h config.h
wildtest$(EXEEXT): wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ wildtest.o lib/compat.o lib/snprintf.o @BUILD_POPT@ $(LIBS)
simdtest$(EXEEXT): simd-checksum-x86_64.cpp $(HEADERS)
@if test x"@ROLL_SIMD@" != x; then \
$(CXX) -I. $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -DTEST_SIMD_CHECKSUM1 \
-o $@ $(srcdir)/simd-checksum-x86_64.cpp @ROLL_ASM@ $(LIBS); \
else \
touch $@; \
fi
testsuite/chown-fake.test:
ln -s chown.test $(srcdir)/testsuite/chown-fake.test
testsuite/chown-fake_test.py:
ln -s chown_test.py $(srcdir)/testsuite/chown-fake_test.py
testsuite/devices-fake.test:
ln -s devices.test $(srcdir)/testsuite/devices-fake.test
testsuite/devices-fake_test.py:
ln -s devices_test.py $(srcdir)/testsuite/devices-fake_test.py
testsuite/xattrs-hlink_test.py:
ln -s xattrs_test.py $(srcdir)/testsuite/xattrs-hlink_test.py
testsuite/exclude-lsh_test.py:
ln -s exclude_test.py $(srcdir)/testsuite/exclude-lsh_test.py
testsuite/xattrs-hlink.test:
ln -s xattrs.test $(srcdir)/testsuite/xattrs-hlink.test
# This does *not* depend on building or installing: you can use it to
# check a version installed from a binary or some other source tree,
@@ -591,7 +341,7 @@ testsuite/exclude-lsh_test.py:
.PHONY: installcheck
installcheck: $(CHECK_PROGS) $(CHECK_SYMLINKS)
"$(srcdir)/runtests.py" --rsync-bin="$(bindir)/rsync$(EXEEXT)" --srcdir="$(srcdir)" --tooldir="`pwd`" -j $(CHECK_J)
POSIXLY_CORRECT=1 TOOLDIR=`pwd` rsync_bin="$(bindir)/rsync$(EXEEXT)" srcdir="$(srcdir)" $(srcdir)/runtests.sh
# TODO: Add 'dist' target; need to know which files will be included
+1 -910
View File
@@ -1,906 +1,3 @@
# NEWS for rsync 3.5.0 (13 Aug 2026)
## Changes in this version:
### Thanks!
This has been an extraordinary release developed over several months
and I'd like to thank everyone who has helped make it possible. The
volume of security issues we had to deal with would have been quite
overwhelming without the help that I've received.
I'm particularly grateful to Zen Dodd (Tao), Omar Elsayed (seks99x),
Will Sargeant, Paul Mackerras, Aleksa Sarai and Leonid Bugaev (buger)
who joined the rsync admins group helping to triage all the issues,
develop new tests, review PRs and helped develop the guidelines we used
for where to draw the line between a security issue and expected
behaviour (a surprisingly difficult thing to do in some cases). You've
all been a huge help and rsync is much better off for your assistance.
A big thank you also to Filipe Casal from Trail of Bits who worked with
us on the "Patch the Planet" program. Filipe provided a huge trove of
valuable tests and security reports.
Also a big thank you to Greg Kroah-Hartman for invaluable advice and
security reports and to Stuart Inglis for particularly high quality
bug reports and testing.
Many thanks to everyone who submitted bug reports, credits are listed
below against individual items.
Finally, thank you to everyone who joined in the discussion and
testing on the rsync-security mailing list, and to the rsync user
community for your patience in waiting for this release.
### SECURITY FIXES:
This release fixes 33 security issues found during a focused audit of rsync's
path handling and daemon protocol, a companion daemon-protocol fuzzing pass, and
reports from external researchers -- plus several robustness hardenings. CVE
IDs were assigned by VulnCheck (CNA); the precise "introduced in" version ranges
accompany each advisory, and many are much narrower than "everything before
3.5.0". Every fix ships with a regression test in the test suite that fails on
the unfixed tree. Many thanks to the external researchers credited below.
Link following (CWE-59/61) -- a local user who controls a path component plants
a symlink that a privileged rsync then follows:
- CVE-2026-53802 (HIGH): Arbitrary file read / transfer-shaping via symlinked
operator-supplied input files. rsync followed attacker-planted symlinks in
`--filter` merge files (including per-directory merges and `-C` `.cvsignore`),
`--files-from` / `--include-from` / `--exclude-from`, and the client
`--password-file` / daemon secrets file -- reading an arbitrary file as filter
rules, or sending a victim file's contents as the daemon authentication
response. Operator-supplied paths are now resolved component-by-component with
`openat(O_PATH|O_NOFOLLOW)`, allowing a symlink component only when it is owned
by uid 0 or the effective uid.
- CVE-2026-53803 (HIGH): Arbitrary file write / privilege escalation via
symlinked operator-supplied output paths -- `--log-file`,
`--write-batch`/`--read-batch`, and the daemon's motd / lock / early-input /
`--config` opens. A planted symlink (or parent component) could redirect the
write, e.g. append the log to `authorized_keys`; `--read-batch` could also feed
chosen bytes to the protocol parser. Same trusted-owner path walk, plus an
`S_ISREG` check on the `--read-batch` file.
- CVE-2026-53785 (HIGH): Under `--relative`, the receiver's implied-parent
creation (`make_path()`) built the parent chain with a plain `mkdir()` on the
full path, so a planted parent symlink placed the new directories and file
outside the destination tree. `make_path()` now creates each component through
the held-directory-fd primitive. Reported by Omar Elsayed (seks99x).
- CVE-2026-53784 (HIGH): Daemon module-root chdir escape under `use chroot =
no`: a plain `chdir()` followed a planted parent-component symlink, serving
files from outside the module. The module-root chdir now goes through the
secure resolver.
- CVE-2026-53793 (HIGH): Chroot `/./` inner-module escape -- a symlinked
parent component inside the inner module reached a sibling outside it (the
generator basis stat, the receiver write/finish path, the module chdir, and the
receiver's delta-basis open). The secure resolver is now engaged for all of
those paths.
- CVE-2026-53795 (HIGH): An absolute `--temp-dir` or `--link-dest` disabled
the receiver's rename/link confinement. `do_rename_at()`/`do_link_at()` bailed
to the unconfined path-based call whenever *either* path was absolute, so an
absolute source (the temp file, or the link-dest basis) let `finish_transfer()`'s
tmp->final rename -- or a hard-link create -- follow a destination parent
component an attacker flipped to a symlink mid-transfer, writing the file outside
the tree. Each side is now confined independently. Reported by Omar Elsayed
(seks99x).
- CVE-2026-53796 (MEDIUM): A non-daemon receiver's one-time `chdir()` into the
operator-named destination was not fully confined (a relative destination took a
plain `chdir()`), so an attacker who raced the named destination from a directory
to a symlink moved the receiver's CWD -- and every file it then created --
outside the tree. The destination chdir now uses the same ownership-checked
`O_NOFOLLOW` walk as the daemon module chdir (see BEHAVIOR CHANGES). Reported by
Omar Elsayed (seks99x).
- CVE-2026-53797 (MEDIUM): A non-daemon sender opened each transferred file's
content by path (leaf `O_NOFOLLOW` only), so a source parent component an
unprivileged user raced to a symlink after the file-list scan was followed --
reading a file from outside the source tree into an attacker-readable
destination. The content open is now anchored at the transfer root with
`secure_relative_open()`; `-L` / `--copy-unsafe-links` / `-k` still follow, and
`--insecure-links` restores the legacy open.
- CVE-2026-53799 (MEDIUM): Receiver ACL/xattr metadata application followed a
symlink race -> arbitrary ACL set (local privilege escalation). When preserving
metadata (`-A`/`--acls`, `-X`/`--xattrs`, or fake-super ACL-as-xattr), the
receiver applied each entry's ACL/xattrs by path via `acl_set_file()` /
`setxattr()`. A local user who raced a just-received entry (or a parent) into a
symlink before the apply could redirect an attacker-chosen ACL -- the bytes are
carried in the source entry -- onto a victim inode outside the destination tree,
granting rwx on a root-owned file. The apply now pins each entry's inode with an
`O_RDONLY|O_NOFOLLOW` fd and sets all metadata on the held inode (Linux 6.13+
`*xattrat` syscalls, or a patched libacl's `*_at` bindings, else the
`/proc/self/fd` compat path). Where neither primitive exists (the BSDs, Solaris,
macOS, or a `/proc`-less Linux container) it falls back to the path-based apply to
keep `--acls` functional -- a documented residual, refusable via `refuse options =
acls`.
- CVE-2026-53800 (MEDIUM): Sender `--remove-source-files` unlink followed a
parent-component symlink race -> arbitrary file deletion outside the source tree.
The post-send unlink and its same-file safety re-stat resolved by path relative to
the process CWD, so an unprivileged user who raced a source parent into a symlink
after the file was sent could make a higher-authority sender (a root
`--remove-source-files` run, or a daemon module not refusing the option) delete a
file outside the served tree. The removal is now resolved through the secure
held-dirfd walk anchored at the served module root (daemon) or transfer-root CWD
(local sender), the safety re-stat is confined likewise, and the per-file dev/ino
is only computed when `--remove-source-files` is in effect.
- CVE-2026-53801 (MEDIUM): Sender/daemon directory-scan enumeration escaped the
transfer root / module -> out-of-tree disclosure. The sender enumerated each
source directory with a plain `opendir()` on the accumulated path, not through the
secure resolver (the enumeration sibling of the previous item, which confined only
the content open). A parent component raced to a symlink between the file-list
scan and the recursive `opendir()` -- or, in daemon following mode
(`-L`/`--copy-dirlinks`/`--copy-unsafe-links`), an in-module symlinked directory
pointing outside the module -- let a higher-authority sender enumerate an
out-of-tree directory and copy its entry names, metadata and symlink targets. The
directory scan is now confined through a held `opendir` fd anchored at the transfer
root / module.
`support/rrsync` (the restricted SSH wrapper):
- CVE-2026-53783 (HIGH): rrsync restricted-directory escape. It validated each
argument with `realpath()` and then exec'd rsync against the same name (a
TOCTOU window), and left dangerous options enabled in a restricted subdir.
rrsync now inode-pins the validated path and roots the argument it hands rsync
at that pinned fd, denies `--copy-unsafe-links`, forces `--no-D`, and refuses a
symlinked `--log-file`. The pin relies on Linux's `/proc/self/fd` magic links
being bound to the open inode, so it is Linux-only; on the BSDs, macOS, Solaris
and Cygwin rrsync passes the `realpath()`-validated name as it always did.
Two limits are worth stating: under `--relative` only the anchor the
transmitted name starts from is pinned, so a component below it can still be
raced, and the final component of an ordinary sender argument is not pinned
either (rsync does not follow a symlink there, and the options that would
change that are refused in a restricted dir).
- A filter rule that failed to parse was echoed back verbatim, including when
the rule came from a merge file's contents. A per-directory merge rule names
a file the peer chooses and travels over the protocol rather than in an
argument, so this let a peer read back any line of any file the server process
could open that is not valid filter syntax -- through an `rrsync` restricted
account as well as a daemon module, since neither confined a merge open that
the wrapper never sees. A syntax error in a rule read from a file now reports
the file and line rather than the text; a rule given as an argument is still
shown. The `--debug=FILTER` traces print the same file-derived text, so
`rrsync` now refuses a peer-selected `--debug` (a stock client never sends
one). An operator who turns debugging on for their own server still sees the
rule text.
- Redacting those diagnostics did not close the merge route on its own, because
the worst shape produces no diagnostic at all: an exclude-only merge (the `-`
modifier) makes every line of the file a pattern, so nothing fails to parse
and the peer reads the contents off which of its own names went missing from
the file list. Through an `rrsync` restricted account that needs no
`--delete` and no verbosity on a pull. The open is now confined rather than
the disclosure suppressed: rsync gained `--confine-root=DIR`, which refuses an
operator- or peer-supplied path that resolves outside DIR, and `rrsync` passes
its restricted directory. A merge file inside that directory keeps working.
A daemon already had this through its module root and is unaffected.
Daemon protocol / identity:
- CVE-2026-53786 (MEDIUM): A client-supplied `--filter` merge file bypassed
the module filter list (it was checked against the module-prefixed path, which
never matched a module rule). The module-dir prefix is now stripped before the
check. Reported by Mitchell Benjamin (Revamp Studio).
- CVE-2026-53798 (MEDIUM): The daemon name converter mapped an unknown name to
uid/gid 0 (an empty response was read as `atol("") == 0`); with `fake super =
yes` the stored metadata became root-owned. An empty/non-numeric response is
now treated as a lookup failure. Reported by Mitchell Benjamin (Revamp
Studio).
- CVE-2026-53788 (MEDIUM): A peer-controlled name containing a newline/CR was
written verbatim into the name-converter line protocol, allowing request
injection. Converter tokens containing control characters are now rejected.
Reported by Mitchell Benjamin (Revamp Studio).
- CVE-2026-53789 (MEDIUM): A malicious daemon-sender could widen `--delete`
scope by omitting the "no content dir" flag on an implied parent, making the
receiver run `delete_in_dir()` on it. Implied-parent directories are now
forced non-content on the receiver. Reported by Mitchell Benjamin (Revamp
Studio).
- CVE-2026-53791 (CRITICAL): With `proxy protocol = true`, a client connecting
directly (not via the trusted proxy) could send a PROXY header to spoof its
source address and bypass host-based access control. A forwarded address is
now honoured only from a configured trusted-proxy peer.
Injection and memory safety:
- CVE-2026-53790 (HIGH): Command / argument injection via unquoted peer- or
host-controlled values -- the `RSYNC_CONNECT_PROG` `%H` host substitution, the
daemon exec-hook `%RSYNC_*%` expansions, rsync-ssl hostspecs, and a missing
newline/CR in remote-shell argument quoting. Each sink is now quoted or
validated (the hook escaping is confined to the shell-executed hooks, so
ordinary daemon string parameters such as `path` are unaffected).
- CVE-2026-53792 (MEDIUM): A malicious receiver sending a checksum header with a
block count > 0 but block length == 0 drove the sender's rolling-match
arithmetic negative. A zero block length is now rejected.
- CVE-2026-53794 (MEDIUM): `--max-alloc=0` disabled the per-allocation size
cap (the defense behind CVE-2024-12084) and could be forwarded on the wire to
an unpatched daemon. A zero max-alloc is now rejected at both the client and
the daemon. Reported by Azizcan Dastan (Milenium Security).
Peer-triggerable memory corruption in the daemon protocol, found by a
daemon-protocol fuzzing pass and reported by Greg Kroah-Hartman. Each is a
WRITE reachable from the wire, which is why these were split out from the
crash-only findings in the same pass:
- CVE-2026-70461 (HIGH): a one-byte heap out-of-bounds write in
`add_implied_include()`, driven by a peer-supplied filter rule whose trailing
backslash was not counted when sizing the copy.
- CVE-2026-70458 (HIGH): an out-of-bounds write from a file entry marked
`FLAG_HLINKED` that the receiver accepted even though `-H` was not in effect,
so the hard-link extra slots it then wrote were never allocated.
- CVE-2026-70456 (HIGH): an out-of-bounds heap write in `read_args()` when the
peer's argument count lands exactly on `maxargs` -- the trailing NULL went one
past the end of the array.
- CVE-2026-70457 (MEDIUM): an attacker-chosen-offset write in
`parse_size_arg()`'s error formatting, reachable through an over-large
`--max-size` / `--min-size` / `--max-alloc` forwarded to a daemon.
- CVE-2026-70459 (MEDIUM): a wild-pointer read crashing the per-connection
daemon child, from a crafted first incremental file list whose transfer root
is "." with a non-directory mode -- `parent_ndx` stayed 0 while `dir_flist`
was still empty, so the generator dereferenced a never-written slot.
Companion to CVE-2026-43620; reproduced on released 3.2.7, 3.4.0 and 3.4.1.
Daemon availability and access control:
- CVE-2026-70464 (HIGH): an unauthenticated peer could complete the `@RSYNCD`
greeting and then stall forever -- sending a line with no terminator, or
trickling NUL-terminated arguments into `read_args()` one byte at a time --
holding a per-connection child open past the module's `max connections`
limit. The `timeout` parameter did not cover it, because `set_io_timeout()`
ran after the `read_args()` calls that needed covering. A separate deadline
now spans both, and the early-protocol argument count is bounded. Reported
independently by Chamal De Silva and by Michal Ruprich (Red Hat QE).
- CVE-2026-70455 (HIGH): a daemon client could request an arbitrary Zstandard
worker count via `--compress-threads`; 256 was measured as 257 threads in a
single connection. Now capped at 8 on a daemon, while local and
remote-shell invocations keep the operator's value. Reported, fixed and
tested by Filipe Casal of Trail of Bits, in collaboration with OpenAI.
- CVE-2026-70453 (HIGH): quadratic CPU exhaustion in `hash_search()` from a
crafted chain of equal weak checksums. The chain walk is now bounded. First
reported as a performance problem in public rsync issue #217 by heyciao
(2021); recognised as a security issue, bounded and regression-tested by
Stuart Inglis. This one was already public and was not embargoed.
- CVE-2026-70452 (HIGH): `hosts deny` failed OPEN when a configured hostname
could not be resolved -- with `forward lookup` enabled, which is the default,
an unresolvable deny token admitted the host it was meant to block. It now
fails closed. Sibling of CVE-2026-43617. Reported by Leonid Bugaev.
- CVE-2026-70463 (HIGH): `auth users` ignored its documented comma-only
parsing. With a leading comma the split should be on commas alone, so that a
group name containing a space can be written; it split on whitespace too, so
a `deny` or `:ro` rule naming such a group was broken into two meaningless
tokens and never fired. Reported by Andres Berbescu.
- CVE-2026-70460 (HIGH): a peer-supplied `--partial-dir` or `--backup-dir` was
resolved by pathname, so an in-module symlink could redirect it and place
files outside the daemon's module root. Those paths are now confined.
Reported by Omar Elsayed (seks99x).
Client-side:
- CVE-2026-70462 (MEDIUM): a peer-supplied `MSG_IO_TIMEOUT` defeated the
client's own I/O timeout -- a large value overflowed signed arithmetic, and a
non-positive value disabled the timeout outright. The value is now capped on
receipt and the arithmetic made overflow-safe. Reported by Z3R0S! (z3r0s6);
the non-positive case was reported by Leonid Bugaev.
- CVE-2026-70454 (MEDIUM): `rsync-ssl` established an unauthenticated TLS
connection. In stunnel mode it neither required CA verification nor bound
the certificate to the requested hostname, so an active network attacker
could impersonate the server; the openssl backend had a matching hostname
gap in 3.2.0 through 3.2.3 (found and fixed in 2020 by Matt McCutchen).
stunnel mode now requires certificate verification and hostname binding
unless an explicit insecure opt-out is set, and the GnuTLS backend is
refused conservatively rather than used unverified (Greg Kroah-Hartman).
Robustness hardening (no CVE assigned): the `RSYNC_PROXY` CONNECT request and
proxy response headers are length-bounded, and peer-requested xattr expansion is
capped.
A second-pass source audit (reported by Leonid Bugaev) hardened several memory-
safety and robustness paths: the hashtable and file-list size computations are
guarded against a 32-bit integer overflow that a peer's entry count could
otherwise wrap into an under-allocation, and the
`SIGUSR2` handler is now async-signal-safe (it only sets a flag, deferring the
summary/close-out work to safe poll points). Separately, the xattr/ACL metadata
copy now reads the *source* through a held no-follow fd as well as writing the
destination through one -- closing a parent-symlink race on the `--copy-dest` and
backup source -- and the cross-tree operator-path metadata apply is now fd-pinned
under `--fake-super` too (previously it fell back to a path-based set for a
`fake super = yes` daemon staging through an absolute `--temp-dir`/`--backup-dir`).
### SECURITY RELATED:
- Mask a peer-supplied I/O-error value to the defined `IOERR_*` bits, both the
incoming `MSG_IO_ERROR` message (`io.c`) and the file-list trailer (`flist.c`),
so a malicious peer cannot set arbitrary (undefined) error flags that would be
stored in the local `io_error` and re-forwarded upstream. (Undefined bits
never reached the exit code, which maps only the defined bits.) Reported by
Leonid Bugaev.
- Escape control characters in filenames written to the log file (CWE-117 log
injection): a transferred name containing control bytes -- C0 (tab excepted)
and C1 `0x80`-`0x9f`, including CSI `0x9b` -- could otherwise inject terminal
escape sequences into an administrator's terminal when the log is viewed.
Reported by Leonid Bugaev.
- Stop `safe_arg()` leaking an uninitialized byte into a quoted filename. In
filename mode the writer suppresses the escaping backslash before a wildcard,
but the counter that sized the buffer reserved a slot for every backslash, so
the two disagreed and left an uninitialized heap byte in the returned string
-- which is handed to the remote shell when `--protect-args` is off. The
counter now mirrors the writer, and guarding the wildcard test with `f[1]`
also fixes a trailing backslash (previously `strchr()` matched the string
terminator, so the backslash was not doubled). Reported by Leonid Bugaev.
- Close a `--safe-links` bypass in `--backup`: when symlinks can be hard-linked,
`make_backup()`'s link/rename fast path hard-linked an unsafe (out-of-tree)
symlink into the backup area and skipped the `safe_symlinks` check the copy
path applies, silently preserving a link `--safe-links` was meant to drop. The
safe-links check now runs before the fast path, and a symlink whose target is
unreadable is failed closed rather than backed up unchecked. Reported by
Leonid Bugaev.
- Extend the operator-directory ownership walk to the backup leaf sinks:
`do_symlink_at()` (backing a symlink up into an operator `--backup-dir`) and
`do_rmdir_at()` (removing a pre-existing backup directory) now resolve their
parent through the same ownership walk, so a foreign-owned parent symlink no
longer redirects the backup symlink-create or directory-removal outside the
backup tree. `--insecure-links` (or a module's `insecure links = yes`) restores
the legacy follow. Reported by Omar Elsayed (seks99x).
- Confine an absolute operator source/destination through the ownership walk in
`robust_rename()`'s cross-filesystem (EXDEV) copy fallback, so a raced parent
symlink cannot redirect the fallback copy or its source unlink out of the tree.
Reported by Leonid Bugaev.
- Bound the number of equal-weak-checksum blocks examined per offset in
`hash_search()` (issue #217), so a crafted or degenerate checksum set with a
very long equal-checksum chain cannot drive the sender's per-offset
match-verify into a quadratic blow-up (CPU DoS). Fix by Stuart Inglis.
### BUG FIXES:
- Fix an off-by-one in `clean_fname()`'s `..`-collapse path normalization.
Reported by Leonid Bugaev.
- The AVX2 rolling-checksum assembly (`--enable-roll-asm`) read up to 64 bytes
past the end of the buffer it was given. The loop is software-pipelined and
preloaded the 64 bytes after the ones it was folding in, so its last iteration
always reached beyond the data -- the remainder is by construction under 64
bytes. It normally landed in slack inside rsync's map window and went
unnoticed; where the buffer ended at a page boundary it was a SIGSEGV mid
transfer, reported on macOS x86-64 by Roland Kletzing. Reported checksums are
unchanged.
- `--link-dest` no longer fails the transfer when the destination refuses to
hard-link a symlink, device node, FIFO or socket. Whether rsync hard-links
those at all was decided at build time, on whatever filesystem the source tree
happened to sit on, and one host can hold both answers -- macOS builds on
APFS, which can, and backs up to HFS+, which returns ENOTSUP. Such an entry
is now copied, exactly as it already is in a build that cannot link them and
as a regular file in the same position already was; the run used to exit 23
even though the entry was then created correctly. The fallback covers any
refusal, since the error does not identify one on its own: link(2) documents
EPERM both for a filesystem without hard links and for a permission refusal.
Still outstanding: under `-H`, a group of such entries hard-linked to each
other also needs a link within the destination, and where the destination
cannot hard-link the type at all, the members after the first are still lost.
- `--out-format` / `--log-file-format` now emit a literal `%` for `%%` instead of
mis-parsing the following character (added by Leonid Bugaev); a follow-up bounds
`log_format_has()`'s width-digit scan to match `log_formatted()`, closing a `%C`
read past the checksum field.
- A CVS `.cvsignore` (or `-C`) file containing a `!` clear-list token no longer
aborts with a spurious "rule has trailing characters" error. Reported by
Leonid Bugaev.
- `--chmod=a+s` now sets both the setuid and setgid bits, matching `chmod(1)`
(it previously set setuid only). Reported by Leonid Bugaev.
- Case-insensitive wildcard matching (used by daemon `hosts allow`/`hosts deny`
rules) now folds characters inside a `[...]` bracket expression, not just
literal pattern characters. Reported by Leonid Bugaev.
### BEHAVIOR CHANGES:
- A non-daemon receiver follows an operator-named symlinked destination directory
only when the symlink is owned by root or the running user (e.g. `rsync -a src/
/backup/` where `/backup -> /mnt/disk`); a destination symlinked by another uid
is now refused, closing a chdir TOCTOU where an attacker raced the named
destination into a symlink. `--insecure-links` restores the unconditional
follow.
- On platforms without a race-safe way to create a unix socket in a subdirectory
(the BSDs, macOS, Solaris, which lack `bindat()`), a nested socket transferred
under `--specials` is skipped with a warning instead of failing the whole
transfer. Top-level sockets are unaffected.
- `proxy protocol = true` with no `proxy protocol hosts` rejects all connections
(fail-closed); the daemon now warns about this at startup.
- `support/rrsync` in a restricted subdirectory forces `--no-D` (device/special
semantics are stripped, so a plain `rsync -a` still works) and denies
`--copy-unsafe-links`.
- The path resolver now follows in-tree directory symlinks uniformly on every
platform via a single race-free per-component `O_NOFOLLOW` walk, so `-K` /
`-L` / `-k` and `-R` through an in-tree symlinked parent behave the same
everywhere.
# NEWS for rsync 3.4.4 (8 Jun 2026)
## Changes in this version:
This is a conservative point release that backports regression fixes
on top of 3.4.3. No new features are included.
### BUG FIXES:
- Honour a relative alt-basis directory (e.g. `--link-dest=../sibling`,
`--copy-dest`, `--compare-dest`) on a daemon receiver running with
`use chroot = no`. Such a path is re-anchored at the module root but
was then rejected by the receiver's secure open; it now works where
kernel-enforced confinement is available. See the PORTABILITY note
below for the platform limitation. Fixes #915.
- sender: open a module-root-absolute path for a `path = /` module so a
daemon serving the filesystem root can satisfy absolute request
paths again. Fixes #897.
- flist: accept the missing-args mode-0 entry in recv_file_entry.
Fixes #910.
- receiver: fix a false "failed verification -- update discarded" when
resuming a delta transfer with an absolute `--partial-dir`.
- receiver: fix a NULL dereference on the delta discard path.
- generator: cap the block s2length at the negotiated checksum length.
- main: fix `--mkpath` with `--dry-run` for a file-to-file copy.
Fixes #880.
- daemon: un-backslash escaped option args. Fixes #829.
- token: drain the matched-block insert deflate. Fixes #951.
- Fix the "update skips a file of a different type" case and the
daemon upload delete stats.
- alloc: revert "zero all new memory from allocations". Fixes #959.
- Always clear the stat buffer and validate nanoseconds before use.
### PORTABILITY / BUILD:
- The relative alt-basis fix for daemon receivers (#915) relies on
kernel "stay below dirfd" path resolution -- `openat2(RESOLVE_BENEATH)`
on Linux 5.6+, or `openat()` with `O_RESOLVE_BENEATH` on FreeBSD 13+
and macOS 15+. On platforms that lack it (Solaris, OpenBSD, NetBSD,
Cygwin and older Linux) `secure_relative_open()` deliberately rejects
any path with a `..` component, so relative alt-basis directories
remain unavailable there -- function traded for safety, matching the
trade-off already documented for the #715 fix. Absolute alt-basis
paths are unaffected on every platform.
- openat2 is now autodetected at configure time (HAVE_OPENAT2): the
`openat2(RESOLVE_BENEATH)` resolver is compiled in only when both
`<linux/openat2.h>` and the `SYS_openat2` syscall number are present,
fixing the build on older kernels/headers. Fixes #924, #905, #900,
#904.
- Fall back to do_mknod() when mknodat() / mkfifoat() are unavailable.
Fixes #896.
- Install generated manpages correctly in an out-of-tree build.
### DEVELOPER RELATED:
- Added a CI workflow that builds this stable branch and runs the
`v34-stable-testsuite` regression suite against the built binary,
giving regression coverage without importing the full master test
suite into the stable branch.
- Added a check-progs target for fleettest and extended the build
workflows to run on `*-stable` release branches.
### CREDITS:
Thanks to everyone who helped with this release:
- Code contributions from Zen Dodd (steadytao), Mike-Goutokuji,
pterror, and Stiliyan Tonev (Bark).
- Zen Dodd (steadytao) also reviewed the 3.4.4 backport set (PR #980).
- Bug reports from @mmayer (#924), @fda77 (#905), @darkshram (#900),
@ketas (#904), @pkzc (#880), @brabalan (#951), @elcamlost (#829),
@debohman (#896), @guilherme-puida (#959), @fufu65 (#915),
@JetAppsClark (#928), @moonlitbugs (#897), @mgkeeley (#910), and
@sylvain-ilm (#724, #725).
# NEWS for rsync 3.4.3 (20 May 2026)
## Changes in this version:
### SECURITY FIXES:
Six CVEs are fixed in this release. All six are assigned by
VulnCheck as CNA. Affected versions are 3.4.2 and earlier in every
case. Three of the six (CVE-2026-29518, CVE-2026-43617,
CVE-2026-43619) require non-default daemon configuration to reach:
the first and third need `use chroot = no` for a module, the second
needs `daemon chroot = ...` set in rsyncd.conf. Two (CVE-2026-43618,
CVE-2026-43620) are reachable from a normal pull or a normal
authenticated daemon connection. The sixth (CVE-2026-45232) is
reachable only when `RSYNC_PROXY` is set and the proxy (or a MITM)
returns a pathological response. Many thanks to the external
researchers who reported these issues.
- CVE-2026-29518 (CVSS v4.0 7.3, HIGH): TOCTOU symlink race condition
allowing local privilege escalation in daemon mode without chroot.
An rsync daemon configured with "use chroot = no" was exposed to a
time-of-check / time-of-use race on parent path components: a local
attacker with write access to a module could replace a parent
directory component with a symlink between the receiver's check and
its open(), redirecting reads (basis-file disclosure) and writes
(file overwrite) outside the module. Default "use chroot = yes" is
not exposed. `secure_relative_open()` (added in 3.4.0 for
CVE-2024-12086) was previously unused in the daemon-no-chroot
case; the fix enables it there and reroutes the sender's
read-path opens through it. Reported by Nullx3D (Batuhan Sancak),
Damien Neil and Michael Stapelberg.
- CVE-2026-43617 (CVSS v3.1 4.8, MEDIUM): Hostname/ACL bypass on an
rsync daemon configured with `daemon chroot = /X` in rsyncd.conf
when the chroot tree lacks DNS resolution support. The
reverse-DNS lookup of the connecting client was performed *after*
the daemon chroot had been entered; if /X did not contain the
libc resolver fixtures (`/etc/resolv.conf`, `/etc/nsswitch.conf`,
`/etc/hosts`, NSS service modules) the lookup failed and the
connecting hostname was set to "UNKNOWN", causing hostname-based
deny rules to silently fail open. IP-based ACLs are unaffected.
The per-module `use chroot` setting is unrelated to this issue.
The fix performs the lookup before entering the daemon chroot.
Reported by MegaManSec.
- CVE-2026-43618 (CVSS v3.1 8.1, HIGH): Integer overflow in the
compressed-token decoder enabling remote memory disclosure to an
authenticated daemon peer. The receiver accumulated a 32-bit
signed counter without overflow checking; a malicious sender could
trigger an overflow that, with careful manipulation, leaked process
memory contents to the attacker -- environment variables,
passwords, heap and library pointers -- significantly weakening
ASLR. The fix bounds the counter and adds wire-input validation in
several adjacent places (defence-in-depth). Workaround for older
releases: `refuse options = compress` in rsyncd.conf. Reported by
Omar Elsayed.
- CVE-2026-43619 (CVSS v3.1 6.3, MEDIUM): Symlink races on path-based
system calls in "use chroot = no" daemon mode (generalisation of
CVE-2026-29518). Earlier fixes for symlink races on the receiver's
open() call missed the same race class on every other path-based
system call: chmod, lchown, utimes, rename, unlink, mkdir, symlink,
mknod, link, rmdir and lstat. The fix routes each affected
path-based syscall through a parent dirfd opened under
RESOLVE_BENEATH-equivalent kernel-enforced confinement (openat2 on
Linux 5.6+, O_RESOLVE_BENEATH on FreeBSD 13+ and macOS 15+,
per-component O_NOFOLLOW walk elsewhere). Default "use chroot =
yes" is not exposed. Reported by Andrew Tridgell as a follow-on
audit of CVE-2026-29518.
- CVE-2026-43620 (CVSS v3.1 6.5, MEDIUM): Out-of-bounds read in the
receiver's recv_files() enabling remote denial-of-service of any
client pulling from a malicious server (incomplete fix of commit
797e17f). The earlier parent_ndx<0 guard added to send_files() was
not applied to the visually-identical block in recv_files(). A
malicious rsync server can drive any connecting client into a
deterministic SIGSEGV by setting CF_INC_RECURSE in the
compatibility flags and sending a crafted file list and transfer
record. inc_recurse is the protocol-30+ default, so no special
options are required on the victim. Workaround for older
releases: `--no-inc-recursive` on the client. Reported by Pratham
Gupta.
- CVE-2026-45232 (CVSS v3.1 3.1, LOW): Off-by-one out-of-bounds stack
write in the rsync client's HTTP CONNECT proxy handler
(`establish_proxy_connection()` in `socket.c`). After issuing the
CONNECT request, rsync read the proxy's first response line one
byte at a time into a 1024-byte stack buffer with the bound
`cp < &buffer[sizeof buffer - 1]`. If the proxy (or a MITM in
front of it) returned 1023+ bytes on that first line without a
newline terminator, `cp` exited the loop pointing at a buffer slot
the loop never wrote, leaving `*cp` holding stale stack data from
the earlier `snprintf()` of the outgoing CONNECT request. The
post-loop logic then wrote a single `\0` one byte past the end of
the buffer on the stack. Reach is client-side only, and only when
`RSYNC_PROXY` is set so rsync tunnels an `rsync://` connection
through an HTTP CONNECT proxy. The written byte is always `\0`
and the offset is fixed by the buffer size, not attacker-chosen,
so this is not an arbitrary-write primitive: practical impact is
corruption of one adjacent stack byte and possible later
misbehaviour or crash. The fix detects the "buffer filled without
finding `\n`" case explicitly by position and refuses the response
with "proxy response line too long". Reported by Aisle Research
via Michal Ruprich (rsync-3.4.1-2.el10 QE).
In addition to the six CVE fixes, this release adds defence-in-depth
hardening on several adjacent paths: bounded wire-supplied counts and
lengths in flist/io/acls/xattrs, a guard against length underflow in
cumulative `snprintf()` callers, a parent block-index bounds check on
the receiver, a NULL check in `read_delay_line()`, a lower ceiling on
`MAX_WIRE_DEL_STAT` to avoid signed-int overflow in the
`read_del_stats()` accumulator, rejection of hyphen-prefixed
remote-shell hostnames (defence-in-depth against argv-injection in
tooling that forwards untrusted input into the hostspec position;
reported by Aisle Research via Michal Ruprich), and a NULL-check on
`localtime_r()` in `timestring()` to keep a malicious server from
crashing the client by advertising a file with an out-of-range
modtime.
### BUG FIXES:
- Fixed a bypass of `--safe-links` when `--backup` is also used on a system that supports hard-linking symlinks (Linux, macOS). An escaping symlink that should have been skipped was silently preserved in the backup area.
- Fixed a spurious abort when using `-C` (cvs-exclude) mode with a `.cvsignore` file that contained a `!` (clear-list) token.
- Updated the `--max-alloc` documentation to reflect that 0 is now rejected (CVE-2026-53794).
- Fixed the EXIT VALUES table: removed nonexistent code 6, added missing codes 15/16/19, corrected SIGUSR1 classification.
- Fixed a regression introduced by the 3.4.0 secure_relative_open()
CVE fix where legitimate directory symlinks on the receiver side
(e.g. when using `-K` / `--copy-dirlinks`) caused "failed
verification -- update discarded" errors on delta transfers. The
old code rejected every symlink in the path with a per-component
`O_NOFOLLOW` walk; the receiver now uses kernel-enforced "stay
below dirfd" path resolution where available. Fixes #715.
### PORTABILITY / BUILD:
- secure_relative_open() now uses `openat2(RESOLVE_BENEATH |
RESOLVE_NO_MAGICLINKS)` on Linux 5.6+, and `openat()` with
`O_RESOLVE_BENEATH` on FreeBSD 13+ and macOS 15+ (Sequoia) /
iOS 18+. The kernel rejects ".." escapes, absolute symlinks, and
symlinks whose target lies outside the starting directory, while
still following symlinks that resolve within it -- the same
trade-off that fixes the issue #715 regression without weakening
the original CVE protection. Other platforms (Solaris, OpenBSD,
NetBSD, Cygwin) retain the previous per-component `O_NOFOLLOW`
walk; on those platforms the issue #715 regression remains
visible.
- testsuite/xattrs: ignore `SUNWattr_*` in the Solaris `xls`
helper.
### DEVELOPER RELATED:
- Added testsuite/symlink-dirlink-basis.test (taken from PR #864
by Samuel Henrique) covering the issue #715 regression and
several edge cases (`--backup`, `--inplace`, `--partial-dir`
with protocol < 29, top-level files). The test skips on
platforms without a RESOLVE_BENEATH equivalent.
- Added regression tests for the new security fixes:
`chmod-symlink-race.test`, `chdir-symlink-race.test`,
`bare-do-open-symlink-race.test`, `alt-dest-symlink-race.test`,
`copy-dest-source-symlink.test`, `sender-flist-symlink-leak.test`,
`secure-relpath-validation.test`, `daemon-chroot-acl.test` and
`daemon-refuse-compress.test`. The symlink-race tests skip on
Cygwin, Solaris, OpenBSD and NetBSD (no RESOLVE_BENEATH
equivalent on those platforms).
- runtests.py now errors early with a clear message when any of
the test helper programs (`tls`, `trimslash`, `t_unsafe`,
`t_chmod_secure`, `t_secure_relpath`, `wildtest`, `getgroups`,
`getfsdev`) are missing, instead of letting many tests fail with
confusing "not found" errors.
- Added OpenBSD and NetBSD CI jobs that run `make check` on those
platforms.
- Added Ubuntu 22.04 and AlmaLinux 8 CI workflows so future
backports to the two mainstream LTS families build and test on
the same CI surface as trunk.
- testsuite/protected-regular.test now runs unprivileged via
`unshare` with user-namespace UID mapping, falling back to skip
if `unshare`/`uidmap` is not available; previously it required
real root.
- Added `symlink-dirlink-basis` to the Cygwin CI's expected-skipped
list.
- Removed the old release system (replaced by the new release
script in 3.4.2).
------------------------------------------------------------------------------
# NEWS for rsync 3.4.2 (28 Apr 2026)
## Changes in this version:
### SECURITY RELATED:
Several security-relevant defects were reported and fixed since 3.4.1.
None were assigned a CVE — rsync's fork-per-connection design scopes
the impact of each of these to the attacker's own connection, which is
equivalent to the client closing the socket itself — but they are
fixed here as a matter of hygiene and to reduce the chances of a
future exploitable combination. Many thanks to the external
researchers who reported these issues.
- Fixed a signed integer overflow in the PROXY protocol v2 header
parser: a negative `len` field could bypass the size check and cause
a stack buffer overflow in `read_buf()`. Reported by John Walker of
ZeroPath.
- Fixed an invalid access to the files array. Reported by Calum
Hutton of Rapid7.
- Reject negative token values in the compressed-stream token
decoder; a negative value could cause callers to misinterpret a
missing data pointer as literal data. Reported by Will Sergeant.
- Fixed the element count passed to the xattr `qsort()` (see
https://www.openwall.com/lists/oss-security/2026/04/16/2).
- Fixed a buffer underflow in `clean_fname()`, and added a regression
test.
- Fixed an uninitialized `mul_one` in the AVX2 get_checksum1 path
(undefined behaviour), and added a SIMD-checksum self-test that
cross-checks SSE2, SSSE3 and AVX2 against the C reference on both
aligned and unaligned buffers.
- Fixed an uninitialized `buf1` on the first call to
`get_checksum2()` in the MD4 path (fixes #673).
- Zero all new memory from internal allocations: `my_alloc()` now uses
`calloc`, and `expand_item_list()` zeros the expanded portion after
`realloc`. This gives more predictable behaviour if stale or
uninitialised memory is ever accidentally read.
### BUG FIXES:
- Call `tzset()` before chroot so that log timestamps continue to
reflect the configured local timezone after the daemon chroots
(glibc needs `/etc/localtime`, which is unreachable post-chroot).
- Use the correct time when writing to the log file.
- Do not clear `DISPLAY` unconditionally.
- Fixed a Y2038 bug in `syscall.c` by replacing the `Int32x32To64`
macro (which truncates its arguments to 32 bits) with a plain
64-bit multiplication.
- Fixed ACL ID mapping for non-root users (closes #618).
- Fixed handling of objects with many xattrs on FreeBSD.
- Fixed `--open-noatime` not taking effect when opening regular
files: `O_NOATIME` is now also passed to `do_open_nofollow()`, which
has been used for regular files since the CVE fix "fixed symlink
race condition in sender".
- Ignore "directory has vanished" errors.
- Fixed the removal of multiple leading slashes.
- Added the missing `--dirs` long option.
- Fixed a segfault if `poptGetContext()` returns NULL (e.g. under
OOM) by not passing NULL to `poptReadDefaultConfig()`. Reported by
Ronnie Sahlberg; found with `malloc-fail-tester`.
- Fixed a build error on ia64 NonStop (which treats missing
prototypes as an error, not a warning).
- Fixed a flaky hardlinks test (fixes #735).
### ENHANCEMENTS:
- Added multi-threaded `zstd` compression, gated by a new
`--compress-threads=N` option, with validation and man-page
coverage.
- Documented the `temp dir` parameter in the rsyncd.conf man page
(fixes #820).
- Improved rendering of interior dashes in long-option names in
`md-convert` (perhaps fixes #686).
### PORTABILITY / BUILD:
- Fixed glibc 2.43 const-preserving overloads of `strtok()`,
`strchr()` etc. by declaring the affected locals with the right
constness. Contributed by Holger Hoffstätte.
- Converted the bundled zlib 1.2.8 from K&R-style function
definitions to ANSI prototypes, so it builds with clang 16+.
- Avoid using `bool` as an identifier; it is a keyword in C23.
- `configure.ac`: check for xattr functions in libc first and only
fall back to `-lattr`, avoiding spurious overlinking when `-lattr`
happens to be installed. Contributed by Eli Schwartz.
- Made the build reproducible by honouring `SOURCE_DATE_EPOCH` for
the manpage date.
- Removed obsolete `popt/findme.c` and `popt/findme.h` that upstream
popt 1.14 folded into `popt.c` (fixes #710). Contributed by Alan
Coopersmith.
### INTERNAL:
- Made many module-global variables `const` so they can live in
`.rodata` and enable additional compiler optimization.
### DEVELOPER RELATED:
- Replaced `runtests.sh` with `runtests.py`, a Python test runner
that supports `--valgrind` (with per-process log files so valgrind
output no longer interferes with output comparisons) and
`-j/--parallel` execution for roughly a 7× speed-up on typical
hardware.
- Added a SIMD checksum self-test and a `clean-fname-underflow`
regression test.
- Various CI fixes for macOS and Cygwin (including adding
`simd-checksum` to the expected-skipped lists on platforms without
SIMD), and tests now run on `ubuntu-latest`.
- removed support for the unmaintained rsync-patches archive
------------------------------------------------------------------------------
# NEWS for rsync 3.4.1 (16 Jan 2025)
Release 3.4.1 is a fix for regressions introduced in 3.4.0
## Changes in this version:
### BUG FIXES:
- fixed handling of -H flag with conflict in internal flag values
- fixed a user after free in logging of failed rename
- fixed build on systems without openat()
- removed dependency on alloca() in bundled popt
### DEVELOPER RELATED:
- fix to permissions handling in the developer release script
------------------------------------------------------------------------------
# NEWS for rsync 3.4.0 (15 Jan 2025)
Release 3.4.0 is a security release that fixes a number of important vulnerabilities.
@@ -955,7 +52,6 @@ to develop and test fixes.
- added FreeBSD and Solaris CI builds
------------------------------------------------------------------------------
# NEWS for rsync 3.3.0 (6 Apr 2024)
## Changes in this version:
@@ -5720,12 +4816,7 @@ to develop and test fixes.
| RELEASE DATE | VER. | DATE OF COMMIT\* | PROTOCOL |
|--------------|--------|------------------|-------------|
| 13 Aug 2026 | 3.5.0 | | 32 |
| 08 Jun 2026 | 3.4.4 | | 32 |
| 20 May 2026 | 3.4.3 | | 32 |
| 28 Apr 2026 | 3.4.2 | | 32 |
| 16 Jan 2025 | 3.4.1 | | 32 |
| 15 Jan 2025 | 3.4.0 | 15 Jan 2025 | 32 |
| 15 Jan 2025 | 3.4.0 | | 32 |
| 06 Apr 2024 | 3.3.0 | | 31 |
| 20 Oct 2022 | 3.2.7 | | 31 |
| 09 Sep 2022 | 3.2.6 | | 31 |
-11
View File
@@ -93,15 +93,6 @@ details.
[3]: https://rsync.samba.org/lists.html
DISCORD
-------
There is also an rsync [Discord server][d] for real-time chat about rsync
and its development.
[d]: https://discord.gg/Avfvy9zhdp
BUG REPORTS
-----------
@@ -145,8 +136,6 @@ COPYRIGHT
Rsync was originally written by Andrew Tridgell and Paul Mackerras. Many
people from around the world have helped to maintain and improve it.
Special thanks go to Wayne Davison, who maintained rsync from 2004 to 2024.
Rsync may be used, modified and redistributed only under the terms of
the GNU General Public License, found in the file [COPYING][9] in this
distribution, or at [the Free Software Foundation][10].
-529
View File
@@ -11,532 +11,3 @@ Email your vulnerability information to rsync's maintainer:
Rsync Project <rsync.project@gmail.com>
## Approach to platform residuals
rsync hardens its security-sensitive operations — path resolution, metadata
application, file/socket creation — against local attacks such as parent-symlink
TOCTOU races. Some of these operations can only be made race-safe with a
primitive the underlying OS provides (an `*at()` syscall on a held directory fd,
an fdescfs-style `/proc/self/fd` magic symlink, `mknodat()`, the `*xattrat`
syscalls, and so on), and that primitive is not available on every supported
platform.
The guiding rule for those cases is:
> **On a modern Linux system every issue described in this document is fully
> addressed.** Where an operation *can* be secured on some platforms but *cannot*
> be secured on others, and the residual risk is a *local* privilege-escalation
> or data-disclosure class (an attacker who already has write access inside the
> transferred tree), rsync prefers keeping the operation functional on the
> platforms that lack the primitive over disabling a long-standing feature for
> everyone on those platforms.
So a hardened operation takes the race-safe path wherever the platform offers one
and falls back to the historical (path-based, unconfined) behaviour only where it
does not — rather than refusing the operation outright. Each such fallback is an
accepted residual, documented under "Known residuals" below, and on the daemon it
can be turned off per feature with `refuse options = ...`. The residuals are
therefore confined to non-Linux platforms (the BSDs, macOS, Solaris/illumos),
Cygwin, and — for a few features — pre-6.13 Linux kernels; a current, normally
configured Linux deployment carries none of them. (The `/proc/self/fd`-based
fallbacks assume a mounted `/proc`, which every standard Linux provides; a
deliberately `/proc`-less container is the one Linux case that can still hit a
residual.)
The one deliberate exception is an operation whose unconfined fallback would
*create a new filesystem object at an attacker-influenceable path* rather than set
metadata on the object rsync already transferred: the nested-socket `bind()` on
platforms without a race-safe socket-create (no `bindat()`). There the unsafe path
is an out-of-tree write/create primitive, not a same-object metadata race, and a
transferred socket inode is a worthless placeholder, so rsync refuses (skips) it
rather than keeping it functional. A leaf permission change is likewise failed
closed rather than applied through a raced symlink, but only as a rare backstop:
the common file/dir/FIFO case is secured on every platform via `fchmod` on a held
fd, so no real functionality is lost.
This trade-off applies only to these local-attacker residual classes. Remotely
reachable defects — memory safety, authentication bypass, protocol parsing, input
bounds — are fixed unconditionally on all platforms, never left as a residual.
## Robustness against malicious peers
rsync treats everything the peer sends — the file list, checksum headers,
multiplexed messages, forwarded daemon arguments, filter rules — as untrusted,
and bounds-checks it before use. A peer-triggerable crash of a connection's
worker process is treated as a defect to be fixed, even though the daemon's
fork-per-connection model confines such a fault to that one connection rather
than the whole service.
Alongside the issues enumerated elsewhere in this document, the code is hardened
continuously through protocol fuzzing (driving the daemon protocol against a
writable module) and static analysis, with a CI gate. This release closes a
batch of peer-triggerable faults found that way: NULL-dereference and
reachable-assert crashes from crafted file lists or indices, reads past a
file-list allocation (mostly bounded over-reads of an entry's extra slots),
unbounded merge-file and suffix-list recursion, and several bounded
out-of-bounds writes driven by peer-supplied lengths or option arguments. Each
is fixed at the root with a bounds or validity check plus a defence-in-depth
guard at the use site, and carries a regression test.
Two further peer-input hardenings in this release: a peer-supplied I/O-error
value (the `MSG_IO_ERROR` message and the file-list trailer) is masked to the
defined `IOERR_*` bits, so a peer cannot set arbitrary error flags in the local
`io_error` that would be stored and re-forwarded upstream; and control
characters in a (peer-controlled) filename written to the log file are escaped,
so a name carrying C0/C1 terminal-escape bytes cannot inject sequences into an
administrator's terminal when the log is viewed (CWE-117). The number of
equal-weak-checksum blocks `hash_search()` examines per offset is also bounded
(issue #217), so a crafted or degenerate checksum set with a very long
equal-checksum chain cannot drive the sender's per-offset match-verify into a
quadratic walk and pin one connection's CPU.
Contributors adding code that consumes peer input should validate it at the
point of receipt rather than relying on a downstream check.
## Symlink-race-safe path resolution
This section documents how rsync defends against parent-directory symlink races
(a TOCTOU / confused-deputy class) and the per-platform approach it takes, so
that contributors and automated agents extend the code consistently rather than
reintroducing the weakness.
### The threat
Many rsync operations resolve pathnames that an unprivileged party can partially
control: a receiver writing into a destination tree, a sender reading a source
tree, and temp and partial files, and so on. (The operator-chosen directory
paths — `--link-dest`/`--compare-dest`/`--copy-dest`/`--backup-dir`/`--temp-dir`/
`--partial-dir` — may legitimately point outside the tree, so they are resolved
by the ownership walk described under *Symlink defense for operator-supplied
paths* below rather than the strict transfer-path resolver here.) If someone who
can write inside that tree races a
parent directory component between a real directory and a symlink ("symlink
flipping"), a path-based syscall — `open`, `stat`, `chmod`, `chown`, `utimes`,
`rename`, `unlink`, `mkdir`, `mknod`, `symlink`, hard-link creation — can be
redirected to a target *outside* the intended tree. When rsync resolves that
path with more authority than the component's controller and without a
confinement boundary, this is a confused-deputy bug (e.g. a root nightly backup
capturing `/etc/shadow`, or a root receiver chmod/chown/unlink-ing a system
file).
`O_NOFOLLOW` on the final component is **not** sufficient: the *parent*
components must be resolved safely.
The boundary that matters is **authority plus confinement**, not "daemon vs
non-daemon". A non-chroot daemon module, a root-run local transfer, and a
two-user transfer are all unconfined privileged path resolvers. Where a real
confinement boundary already exists (e.g. a per-module `chroot`) that is the
strongest protection; otherwise rsync must resolve paths defensively.
A `chroot` is only a boundary for the *outer* path it confines. A daemon module
written as `path = /outer/./inner` (`use chroot = yes`) chroots to `/outer` but
treats `/inner` as the module root, so a symlink inside the module that points to
a sibling of `/inner` is still inside the chroot yet outside the module — the
inner module therefore needs the same defensive resolution as a non-chroot
module. The single gate that decides when hardened resolution applies is
"unconfined privileged resolver": `am_daemon && (!am_chrooted || module_dirlen)`
for the daemon (any non-chroot module, plus a `/./` inner-module chroot), and any
non-chroot receiver. The local sender's content open is confined the same way for
default symlink handling; only the symlink-following modes (`-L`/`--copy-links`/
`--copy-unsafe-links`/`-k`) and `--insecure-links` are excluded, so those keep
following symlinks by design.
### The mechanism
Resolution of attacker-influenceable paths goes through `secure_relative_open()`
and the `do_*_at()` wrappers in `syscall.c`, never a raw `open()`/`rename()`/
`chmod()` on a full path string. The principle is: **trust the operator-named
transfer root, and confine all resolution beneath it**, rejecting escapes via
`..` above the anchor, absolute symlinks, or out-of-tree symlinks.
`secure_relative_open()` resolves the parent directory by walking it one
component at a time on a stack of held directory fds, then operates on the final
component with an at-style call on the resulting directory fd.
For per-entry work the receiver and generator go one step further and hold the
parent directory open: `open_dir_secure()` resolves an entry's directory once
(via `secure_relative_open()`), `held_dfd_for()` caches that descriptor for the
duration of the entry, and every operation on the entry — `lstat`, the temp-file
`mkstemp`, the temp->final `rename`, `chmod`/`chown`/`utimes`, `mkdir`, special-
file and symlink creation, the delta-basis open, and the recursive delete — runs
through that one held fd via an `*at()` call (`do_*_atfd()`). Because the
descriptor is pinned to the directory inode, a parent component flipped to a
symlink *after* the open cannot redirect any of those operations. The alternate-
destination lookups are confined the same way (`basis_link_stat()` in
`generator.c` and `secure_basis_open()` in `receiver.c`), so a peer-chosen
`--link-dest`/`--compare-dest`/`--copy-dest` basis index cannot reach an
out-of-module file through a symlinked parent.
The sender's source-directory *enumeration* is confined the same way as its
content open. `send_directory()` opens each scanned directory through
`secure_opendir()` — which resolves it via `secure_relative_open()` /
`secure_relative_open_at()` and turns the held fd into the `DIR*` with
`fdopendir()` — so a parent component raced into a symlink, or (for a daemon
following mode) an in-module symlink pointing outside the module, cannot redirect
the scan to enumerate an out-of-tree directory and leak its entry names, metadata
and symlink targets. For a daemon, both the enumeration and the content open
anchor at the served module root **pinned by identity**: `module_dirfd` is opened
(`open(".")`) the moment the daemon `chdir`s into the module, while still
privileged, and module-relative paths resolve beneath that fd via
`secure_relative_open_at()`. Anchoring at the held fd rather than re-resolving the
absolute module path keeps the confinement working after the daemon drops to the
module uid even when the module sits under a directory that uid cannot traverse
(e.g. a `0700` home — re-resolving the absolute path would `EACCES`), and is
immune to the logical-path-versus-real-cwd skew a followed in-tree directory
symlink would otherwise introduce.
### Path resolution
`secure_relative_open()` resolves a path with a single portable mechanism on
every platform: a per-component walk on a stack of held directory fds. Each
component is opened relative to the held parent with `openat(parent_fd,
"component", O_NOFOLLOW)`; descending into a real subdirectory pushes its fd, a
`..` pops back to the already-held parent (a pop at the anchor is refused), and an
in-tree directory symlink is followed by reading its target and walking that off
the same stack (absolute targets refused, symlink hops bounded). The final
component is opened `O_NOFOLLOW`.
Because every component is opened relative to a *pinned* fd under `O_NOFOLLOW`,
and `..` is resolved by the held-fd stack rather than by the kernel, the walk is
race-free by construction: no rename or symlink swap of any path name can redirect
resolution outside the anchor subtree, and no kernel "beneath" primitive
(`openat2(RESOLVE_BENEATH)` / `openat(O_RESOLVE_BENEATH)`) is required. The
confinement is therefore uniform across Linux, the BSDs, macOS and
Solaris/illumos, on old and new kernels alike, with nothing to probe or fall back
to at runtime (and so no `openat2`/seccomp interaction to worry about in sandboxed
environments). Cygwin is the exception, because its directory descriptors and
symlink emulation do not give the held-fd walk the same inode pinning — see the
Cygwin residual below.
Legitimate *in-tree* directory symlinks are followed, so `--keep-dirlinks` /
`--copy-links` and a symlinked module path keep working. A relative alternate-dest
such as `--compare-dest=../01` may legitimately climb to a sibling still inside the
module; such a `..` path is re-anchored at the module root and its in-module climb
adjudicated by the walk (the `..` pops to the held parent), while escapes above the
anchor are still rejected.
### Leaf operations
The final operation is hardened as well, following `cp`: reads use `O_NOFOLLOW`
so a flipped leaf symlink is not followed, and new or destination files are
created with `O_CREAT|O_EXCL` (rsync's temporary files use `mkstemp`) so a
planted symlink at the target cannot be written through. A leaf `chmod` is the
one operation with no portable no-follow form: it is closed by opening the leaf
`O_RDONLY|O_NOFOLLOW` and `fchmod`-ing the held fd (refusing a symlink leaf with
`ELOOP`), falling back to `fchmodat(AT_SYMLINK_NOFOLLOW)` and then the
`fchmodat2()` syscall, and failing closed with a warning rather than ever
chmod-ing through a raced leaf symlink.
### Guidance for contributors
* When adding code that performs a path-based syscall on a path that can be
influenced by the remote peer or by another local user, use a `do_*_at()`
wrapper (or `secure_relative_open()`), not a raw full-path syscall.
* When introducing a new operation, add a matching `do_<op>_at()` wrapper that
resolves the parent with `secure_relative_open()` and acts via an at-style call
on the returned dirfd.
* Do not assume a non-daemon transfer is safe; the question is whether rsync has
more authority than whoever controls the path components.
* On platforms whose API lacks an at-style equivalent (e.g. `setattrlist()`),
follow the residuals policy at the top of this document: for a metadata
operation on the already-transferred object (ACLs, xattrs, crtimes, permissions)
fall back to the path-based call to keep the feature functional and document the
residual; but where the unsafe fallback would *create a new object on an
unconfined path* (the nested-socket `bind()` case), refuse it instead — that is
an out-of-tree write/create primitive, not a same-object metadata race, and the
lost functionality is negligible.
## Symlink defense for operator-supplied paths
rsync opens several operator-supplied paths during normal operation. These fall
into two groups, both governed by the same ownership-walk policy below:
* operator **files**: `--log-file`, `--password-file`, `--early-input` (a client
read whose contents are forwarded to the daemon's early-exec), `--files-from`,
`--include-from`, `--exclude-from`, `--filter=. file`, `--write-batch`,
`--read-batch`, per-directory filter merge files (`-C` / `-F` / `dir-merge`),
and on the daemon side `motd file =`, `secrets file =`, `lock file =`, and
`rsyncd.conf` itself.
* operator **directories**: `--backup-dir`, `--temp-dir`/`-T`, `--partial-dir`,
and the `--link-dest`/`--compare-dest`/`--copy-dest` basis lookup. These take
a directory the operator chose, which may legitimately point outside the
transfer tree (`--backup-dir=/var/backups`), so they are resolved with the
ownership walk rather than the strict transfer-path resolver.
The daemon module-root `chdir()` under
`use chroot = no` and the non-daemon receiver's `chdir()` into the
operator-named destination directory are in the same class: both follow
the operator's/root's own symlinked target (the `/backup -> /mnt/disk`
admin pattern) but refuse one an attacker raced in from another uid,
unless `--insecure-links` restores the legacy plain `chdir()`.
Each of these reads or writes a path the operator or sender chose, which
may transit attacker-influenceable parent directories (the `/tmp/somedir/`
class) or be planted directly (the `/home/$user/.cvsignore` class when
root runs `rsync -a /home /backup`).
rsync's defense, applied uniformly to all of the above, is a
component-by-component path walk (`open_no_attacker_symlinks` in
`util1.c`) that allows symlinks **only** when the symlink itself is owned
by uid 0 or the running process's effective uid. Symlinks owned by any
other uid are refused with `ELOOP` at any path component (parent or leaf).
Plain `O_NOFOLLOW` would be leaf-only and would not defend the
`/tmp/somedir/log` parent-component plant; this walk does.
The trust model preserves legitimate setups such as `/var/log -> /data/log`
(root-owned dir-symlink) and a non-root user's own `~/log -> /data/me`
symlink; it refuses an attacker's `/tmp/somedir -> /attack/path` plant.
For `--read-batch` an additional `fstat()` check refuses non-regular
files (FIFOs, devices) at the batch path, since the batch content drives
the receiver's protocol parser.
**Policy.** A symlink at **any** path component (parent or leaf) is **followed
iff it is owned by uid 0 or the process's effective uid, and refused (`ELOOP`)
otherwise**, identically for **absolute and relative** operator paths. The trust
signal is **authority (ownership)**, not **location**: an operator path may
legitimately point outside the transfer tree, so it cannot be confined by
location the way a transfer path is. This is deliberately distinct from the
transfer-path resolver `secure_relative_open()` (see *Symlink-race-safe path
resolution* above), which refuses **all** symlinks and anchors **beneath the
transfer root** — correct for peer-named paths, which never legitimately escape.
For the operator directory paths, a refused symlink simply makes the target look
absent (no backup/temp/basis is taken through it) and the transfer proceeds
normally; the operator's own symlinked target keeps working.
**The daemon `exclude`/`filter` chain is not a symlink boundary.** The daemon
filter chain (`exclude`, `exclude from`, `filter`, …) matches the *logical*
module-relative **name** of each item, not the physical file it resolves to. It
is a visibility/tamper filter — a peer cannot *name* a daemon-excluded path to
pull, push to, or delete it — but it is **not** a security boundary against
symlinks: an in-module symlink whose own name is not excluded can be followed to
an excluded target (the name the filter sees, e.g. `link`, is not the excluded
name, e.g. `secret`). This is by design and is the long-standing behaviour of
stock rsync; the defense for a writable module against symlink trickery is
`munge symlinks` (enabled by default for a writable, non-chrooted module), **not**
the filter. Do not rely on `exclude`/`filter` to confine a peer who can introduce
or traverse a symlink; see `rsyncd.conf(5)` ("filter" and "munge symlinks").
What *is* enforced for a *peer-supplied* operator path (`--partial-dir`,
`--backup-dir`, the alt-dest basis) is confinement to the **module root**: the
ownership walk refuses a foreign-uid symlink (the symlink-race defense) and
refuses a resolved target *outside* the module. That module-boundary confinement
is independent of `exclude`/`filter` — it holds whether or not the module sets an
exclude — and is what the operator-path tests cover.
**`--insecure-links`.** This flag is a **local** opt-out that restores the legacy
follow-any-symlink behaviour for the paths above. It is **not forwarded** to the
remote (a remote-shell peer that wants the opt-out must set it on its own side,
e.g. via `--rsync-path`), and a **daemon never honors it**: the opt-out predicate
reads the client-controllable flag only off a daemon, so a peer-forwarded or
`-M`-injected `--insecure-links` cannot weaken a daemon's confinement — the daemon
additionally hard-refuses it (drops the connection) via the refused-options path.
A daemon admin who wants the legacy behaviour for one isolated/trusted module
sets `insecure links = yes` in that module's `rsyncd.conf` stanza (see
`rsyncd.conf(5)`); this is admin-only and re-opens the symlink-escape
vulnerabilities for that module on purpose. The `operator-path-*` and
`insecure-links-*` tests enforce this consistency across every path-taking
option and across absolute/relative, leaf/parent, and same-uid/cross-uid plants.
For `support/rrsync` (the SSH-restricted-rsync wrapper), the same TOCTOU
class is closed in Python by opening each validated path component with
`O_RDONLY|O_NOFOLLOW`, verifying via `readlink('/proc/self/fd/N')` that the
pinned inode is still in-tree, and passing `/proc/self/fd/N` as the exec'd
rsync's argument (so the kernel routes the child's open through the pinned
inode rather than re-resolving the path). A receiver-side new destination
has no inode of its own yet, so its existing parent directory is pinned the
same way and the leaf is created at `/proc/self/fd/<parent>/<leaf>`. This pin
relies on an fdescfs-style magic symlink and is not available on every
platform -- see the rrsync residual below.
### Known residuals
The following are documented as out of scope for this release:
* The source-directory *enumeration* confinement needs `fdopendir()` (to form a
`DIR*` from the securely-resolved held fd) and `dirfd()`; on a platform lacking
either, `send_directory()` falls back to the legacy `opendir()` on the path, so
the scan is unconfined there — the same resolver-fallback shape as the other
`*at()`-less residuals. Every current target provides both; the per-entry
operations and the content open remain confined regardless.
* On **Cygwin**, the per-component held-fd walk does not provide the same
inode-pinning guarantee as on a POSIX kernel: Cygwin tracks a process's
current directory and resolves directory descriptors by path name rather than
by a pinned inode, and emulates symlinks as special files. Static out-of-tree
symlinks are still refused (the walk sees and rejects them), and a daemon
module path anchored at an absolute `module_dir` is confined; but an entry
whose parent component is *raced* from a directory to a symlink mid-resolution
can still slip past confinement that is anchored at the process CWD (e.g. the
sender's content open), because the descriptor is not bound to the original
inode. The parent-component symlink-race tests are therefore not enforced on
Cygwin (see `RSYNC_EXPECT_SKIPPED` in `.github/workflows/cygwin-build.yml` and
the Cygwin-only xfail in `symlink-race-source_test.py`). Cygwin is a
development/interoperability target, not a privilege boundary host, so this is
accepted for this release.
* On a platform with no `mknodat()` at all -- macOS before 13 is the
supported example, where `mknod()` and `mkfifo()` exist but neither
`mknodat()` nor `mkfifoat()` does -- creating a device node or FIFO
falls back to plain `do_mknod()`, which resolves the whole path by name.
What is lost is the *pinned parent*: the directory components are
re-resolved by the kernel at create time, so an attacker who can swap a
parent component races the create and can place the node outside the
transfer. The final component is not at risk -- `mknod()` and
`mkfifo()` do not follow a symlink at the leaf, they fail `EEXIST`.
Where `AT_FDCWD` exists -- which is every platform rsync 3.5.0 supports,
macOS 10.13 included -- fake-super placeholders still return through
`openat(..., O_NOFOLLOW)`, reached before either `*at` primitive is
tested, so ordinary in-tree placeholder creation stays confined;
fake-super loses parent confinement and the `O_NOFOLLOW` leaf only on the
paths that reach plain `do_mknod()` (the cache-declined/cross-tree
wrapper and the backup paths). On a build with no `AT_FDCWD` at all
there is no fd-relative primitive of any kind, so nothing above applies
and every special-file create, fake-super included, is unconfined. Transferring specials there (`--devices`, `--specials`) carries
the parent-component race. `symlink-mknod-fakesuper-symlink-race` skips
itself on such a build, since the property it asserts is one the build
deliberately does not have.
* On platforms where `mknod()`/`mknodat()` cannot create a socket inode
(the BSDs, macOS, Solaris), a transferred socket is recreated with
`socket()` + `unlink` + `bind(path)`, which cannot be confined (there is
no portable `bindat()`). Linux creates it race-safely with `mknodat()`
on a held dirfd; on the others a *nested* socket is skipped with a
warning rather than bound on an unconfined path, leaving only a
top-level, operator-named socket binding by path.
* `support/rrsync`'s race-free inode-pin -- of both existing path
components and a new destination's parent -- depends on materialising a
held fd as a path that the exec'd rsync re-resolves to the same inode.
rrsync validates and pins in its own process, but it then *exec*s a
separate rsync that re-resolves the paths from `argv`, so the confining
reference must be expressible as an argument. A held dirfd is not: it is
usable as a path only through an fdescfs-style magic symlink. rrsync
implements this for Linux only, via `/proc/self/fd/N`; it does not use the
`/dev/fd/N` equivalent that macOS/FreeBSD expose with `fdescfs` mounted. So on
every non-Linux platform (the BSDs, macOS, Solaris -- whose `/proc/self/fd`
entries are not magic symlinks -- and Cygwin), and on a `/proc`-less Linux
namespace, rrsync falls through to the realpath-validated path unpinned,
so a parent-component or between-pin-and-exec flip remains possible there;
a deeper `-R` new path whose parent does not exist yet is likewise
unpinned. The portable closure is an rsync-side fd-passing API -- rrsync
hands rsync the confined dirfd (inherited across `exec`) and rsync
resolves that argument relative to it with the same `secure_relative_open`
resolver the daemon uses, needing no magic-symlink filesystem -- a
protocol/CLI addition under discussion on the rsync-security list.
* The operator-directory ownership walk refuses a foreign-owned symlink on a
`--backup-dir`/`--temp-dir`/`--partial-dir`/`--link/compare/copy-dest` path, so
a *statically planted* symlink is rejected and the dependent operation does not
escape. Both the data writes and the *source-metadata reads* of those
operations are now confined to held no-follow fds: the `--copy-dest`
`copy_file()`/`copy_xattrs()` source read goes through the held basis content fd
(`sys_fgetxattr`), and `make_backup()` reads the backed-up file's ACL/xattrs
through a `backup_source_fd()`-pinned fd -- so a parent-component flip can no
longer redirect them to disclose an out-of-module value. The cross-tree
metadata *apply* on those leaves (the `%stat`/ACL/xattr write on a
`--temp-dir`/`--backup-dir` staging file) is fd-pinned the same way, now
including under `--fake-super`: the `set_file_attrs()` no-follow leaf fd was
previously opened only when `am_root >= 0`, so a `fake super = yes` daemon fell
back to a path-based `sys_lsetxattr()`/chmod a raced parent could redirect; the
pin is now opened for fake-super too (a raced leaf is refused, not redirected).
Two narrow follow-ons
re-resolve the (now-validated) operator path by name and remain a
*post-validation* parent-component race:
* the in-place backup (`--inplace --backup`) writes the backup file's data
through a confined create, but its `set_file_attrs()` metadata set
(chmod/chown/times) re-resolves the `--backup-dir` path by name afterwards
(it is not placed under operator mode, which would force the shared
`set_file_attrs()` path off its held-O_NOFOLLOW-fd xattr write and re-open
the very parent-symlink xattr race `copy-xattrs-symlink-race` pins closed); and
* the abbreviated-xattr optimisation reuses a basis xattr value for the
destination only when its checksum matches the digest the sender sent; that
basis read (`rsync_xal_set()`) re-resolves the basis path by name. This is a
*constrained checksum-oracle*, not a disclosure: it confirms that some raced
out-of-module xattr hashes to a value the sender already chose, rather than
copying an unknown value onto a readable file, and needs a colluding sender
plus a local racer.
An attacker who flips a parent component in the window *after* the confined data
write/stat can thus still affect those narrow metadata/oracle operations. This
is the same local-attacker post-confinement TOCTOU class as the ACL/crtimes
residuals below; the data-write and direct source-read escapes are closed, and
`--insecure-links` (or a module's `insecure links = yes`) is orthogonal to it.
* POSIX ACL application (`-A`/`--acls`) is race-safe on every Linux kernel —
6.13+ via the `*xattrat` syscalls (or a patched libacl's `*_at` bindings), and
older kernels via the `/proc/self/fd` compat that pins the same inode, provided
`procfs` is mounted — and a transferred file/dir/FIFO has its xattrs (`-X`)
applied through the held no-follow fd, so the apply cannot be redirected by a
raced parent component. Where neither primitive is available — the BSDs,
Solaris and macOS (no `*xattrat` syscalls and no `/proc/self/fd` magic
symlinks), plus the edge case of a Linux instance with no usable `/proc` (a
`/proc`-less container/namespace) — the ACL apply falls back to the path-based
`acl_set_file()` /
`sys_acl_*file()` calls — the long-standing 3.4.x behaviour — to keep `--acls`
functional rather than silently skipping it, so a parent-component flip can
have the received ACL written onto an object outside the module/destination
boundary (and, because the attacker controls the ACL bytes, granted to a chosen
uid). As with the macOS crtime tier below, this is an accepted residual under
the functionality-over-refusal policy; a daemon operator who does not want it
can disable the feature with `refuse options = acls`.
* macOS creation-time (`--crtimes`) preservation uses the path-based
`setattrlist()`/`getattrlist()` with `FSOPT_NOFOLLOW`, which protects only
the final component; there is no `setattrlistat()` targeting
`ATTR_CMN_CRTIME`. As with POSIX ACLs where the OS offers no race-safe
primitive, `--crtimes` is kept functional (daemon and non-daemon) and the
parent-component symlink race is an accepted residual: an attacker who
flips a parent component can have a crtime read/write target an object
outside the module/destination boundary. The mtime/atime path is *not*
affected -- `set_times()` resolves it race-safely through `utimensat()` on a
held dirfd in hardened mode. A daemon operator who does not want the crtime
residual can disable the feature with `refuse options = crtimes` in
`rsyncd.conf`.
* Pulling with `-o`/`-g` (or `-a`) **as root from an untrusted sender** is by
design a trust relationship, not a confinement boundary: the sender dictates
each received file's owner/group, including uid/gid 0. rsync maps the
sender's id/name pairs through the local id database; an empty or unknown
sender name falls back to the sender's numeric id (the value `--numeric-ids`
would use), and a sender can equally request root via the literal name
`root`. A root receiver must therefore only pull with `-o`/`-g` from a
trusted source (or use a non-root receiver / a uid-gid policy). The daemon
*name-converter* path is guarded separately — an unknown name there maps to
the sender's numeric id rather than 0 (see `clientserver.c`).
## Daemon authentication digest
Daemon authentication is a secret-prefix challenge-response: the client returns
`base64(H(secret || challenge))`, where `H` is a digest the two sides negotiate.
The negotiation is unauthenticated and ordered by the connecting side, and the
`md5`/`md4` digests remain available for backward compatibility, so a peer that
sends no digest list (any rsync before 3.2.0, including the openrsync that ships
with macOS) falls back to `md5` (or `md4` below protocol 30), and an on-path
attacker can rewrite the negotiation to force `md5`/`md4` even between two modern
peers. This is **not** an authentication bypass — `md4`/`md5` have no practical
preimage break — but a weak digest makes a *captured* `(challenge, response)`
pair far cheaper to brute-force offline, recovering a guessable shared secret.
The challenge itself is seeded from the kernel CSPRNG (`/dev/urandom`), so it is
an unpredictable per-connection nonce. An earlier time/pid-based challenge was
low-entropy enough (~35 bits) that recovering the `(sec, usec, pid)` tuple from
one observed challenge let an on-path observer predict every subsequent challenge
from that daemon process and pre-compute a dictionary against a captured
response. (If `/dev/urandom` is unavailable the daemon logs a warning and falls
back to the legacy time-based challenge rather than a constant.)
A daemon operator whose clients are all modern (rsync 3.2.7+ built with openssl,
when the SHA digests were added) can require a strong digest with the `auth
digest` module parameter, e.g. `auth digest = sha256`, which refuses any
connection that negotiates — or falls back to — a weaker digest (see
`rsyncd.conf`).
Residual: there is **no default floor**, because requiring one would break every
pre-3.2.0 client (notably the macOS-bundled openrsync, which authenticates only
with `md4`). An operator who cannot raise the floor should run the daemon behind
a verified TLS transport (`rsync-ssl`/stunnel) or over ssh — which removes the
on-path capture/downgrade vector at the transport layer — and should use a
high-entropy shared secret, which is infeasible to brute-force regardless of the
digest.
+11
View File
@@ -15,6 +15,7 @@ Create more granular verbosity 2003/05/15
DOCUMENTATION --------------------------------------------------------
Keep list of open issues and todos on the web site
Perhaps redo manual as SGML
LOGGING --------------------------------------------------------------
Memory accounting
@@ -212,6 +213,16 @@ DOCUMENTATION --------------------------------------------------------
Keep list of open issues and todos on the web site
-- --
Perhaps redo manual as SGML
The man page is getting rather large, and there is more information
that ought to be added.
TexInfo source is probably a dying format.
Linuxdoc looks like the most likely contender. I know DocBook is
favoured by some people, but it's so bloody verbose, even with emacs
support.
+8 -22
View File
@@ -28,7 +28,7 @@ static int allow_forward_dns;
extern const char undetermined_hostname[];
static int match_hostname(const char **host_ptr, const char *addr, const char *tok, int deny)
static int match_hostname(const char **host_ptr, const char *addr, const char *tok)
{
struct hostent *hp;
unsigned int i;
@@ -54,14 +54,8 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
return 0;
/* Now try forward-DNS on the token (config-specified hostname) and see if the IP matches. */
if (!(hp = gethostbyname(tok))) {
/* A deny-list hostname token we cannot resolve must fail CLOSED:
* we can't prove the peer isn't the denied host, so treat the
* unresolvable token as a match (deny). Allow-list tokens keep
* failing as a non-match. Sibling of CVE-2026-43617, which fixed
* only the reverse-lookup path. */
return deny;
}
if (!(hp = gethostbyname(tok)))
return 0;
for (i = 0; hp->h_addr_list[i] != NULL; i++) {
if (strcmp(addr, inet_ntoa(*(struct in_addr*)(hp->h_addr_list[i]))) == 0) {
@@ -105,7 +99,7 @@ static void make_mask(char *mask, int plen, int addrlen)
return;
}
static int match_address(const char *addr, char *tok)
static int match_address(const char *addr, const char *tok)
{
char *p;
struct addrinfo hints, *resa, *rest;
@@ -249,7 +243,7 @@ static int match_address(const char *addr, char *tok)
return ret;
}
static int access_match(const char *list, const char *addr, const char **host_ptr, int deny)
static int access_match(const char *list, const char *addr, const char **host_ptr)
{
char *tok;
char *list2 = strdup(list);
@@ -257,7 +251,7 @@ static int access_match(const char *list, const char *addr, const char **host_pt
strlower(list2);
for (tok = strtok(list2, " ,\t"); tok; tok = strtok(NULL, " ,\t")) {
if (match_hostname(host_ptr, addr, tok, deny) || match_address(addr, tok)) {
if (match_hostname(host_ptr, addr, tok) || match_address(addr, tok)) {
free(list2);
return 1;
}
@@ -281,7 +275,7 @@ int allow_access(const char *addr, const char **host_ptr, int i)
/* If we match an allow-list item, we always allow access. */
if (allow_list) {
if (access_match(allow_list, addr, host_ptr, 0))
if (access_match(allow_list, addr, host_ptr))
return 1;
/* For an allow-list w/o a deny-list, disallow non-matches. */
if (!deny_list)
@@ -290,17 +284,9 @@ int allow_access(const char *addr, const char **host_ptr, int i)
/* If we match a deny-list item (and got past any allow-list
* items), we always disallow access. */
if (deny_list && access_match(deny_list, addr, host_ptr, 1))
if (deny_list && access_match(deny_list, addr, host_ptr))
return 0;
/* Allow all other access. */
return 1;
}
int allow_proxy_protocol_peer(const char *list, const char *addr, const char **host_ptr)
{
if (!list || !*list)
return 0;
allow_forward_dns = 0;
return access_match(list, addr, host_ptr, 0);
}
+16 -385
View File
@@ -21,10 +21,6 @@
#include "rsync.h"
#include "lib/sysacls.h"
#include "lib/acl.h"
#ifdef HAVE_LIBACL_AT
#include <fcntl.h> /* AT_EMPTY_PATH / AT_SYMLINK_NOFOLLOW */
#endif
#ifdef SUPPORT_ACLS
@@ -473,129 +469,11 @@ static int find_matching_rsync_acl(const rsync_acl *racl, SMB_ACL_TYPE_T type,
return *match;
}
/* These two bridge lib/acl.c's neutral (tag,perm,id) entry array; with
* HAVE_LIBACL_AT the libacl *_at path uses unpack_smb_acl/pack_smb_acl directly,
* so they are unused there. */
#if defined(SUPPORT_ACL_FD) && !defined(HAVE_LIBACL_AT)
/* Convert a packed system ACL into the neutral (tag,perm,id) entry array that
* lib/acl.c serializes. Reuses pack_smb_acl()+change_sacl_perms() output so
* the bytes we write match exactly what acl_set_file() would have written.
* Returns the entry count and a malloc'd array in *ents_p, or -1 on error. */
static int sacl_to_entries(SMB_ACL_T sacl, rsync_acl_ent **ents_p)
{
static item_list ent_list = EMPTY_ITEM_LIST;
SMB_ACL_ENTRY_T entry;
rsync_acl_ent *out;
int rc;
ent_list.count = 0;
for (rc = sys_acl_get_entry(sacl, SMB_ACL_FIRST_ENTRY, &entry); rc == 1;
rc = sys_acl_get_entry(sacl, SMB_ACL_NEXT_ENTRY, &entry)) {
SMB_ACL_TAG_T tag_type;
uint32 access;
id_t g_u_id;
rsync_acl_ent *e;
uint16_t tag;
if ((rc = sys_acl_get_info(entry, &tag_type, &access, &g_u_id)) != 0)
break;
switch (tag_type) {
case SMB_ACL_USER_OBJ: tag = RACL_USER_OBJ; break;
case SMB_ACL_USER: tag = RACL_USER; break;
case SMB_ACL_GROUP_OBJ: tag = RACL_GROUP_OBJ; break;
case SMB_ACL_GROUP: tag = RACL_GROUP; break;
case SMB_ACL_MASK: tag = RACL_MASK; break;
case SMB_ACL_OTHER: tag = RACL_OTHER; break;
default: continue; /* skip an unrecognized tag */
}
e = EXPAND_ITEM_LIST(&ent_list, rsync_acl_ent, -10);
e->tag = tag;
e->perm = access & 7;
e->id = (tag == RACL_USER || tag == RACL_GROUP) ? (uint32_t)g_u_id : RACL_UNDEFINED_ID;
}
if (rc) {
rsyserr(FERROR_XFER, errno, "sacl_to_entries: sys_acl_get_entry/info()");
return -1;
}
out = new_array(rsync_acl_ent, ent_list.count ? ent_list.count : 1);
if (ent_list.count)
memcpy(out, ent_list.items, ent_list.count * sizeof (rsync_acl_ent));
*ents_p = out;
return ent_list.count;
}
/* Unpack a neutral entry array (from lib/acl.c) into an rsync_acl, mirroring
* unpack_smb_acl()'s tag handling. */
static BOOL unpack_acl_entries(const rsync_acl_ent *ents, int n, rsync_acl *racl)
{
static item_list temp_ida_list = EMPTY_ITEM_LIST;
int i;
temp_ida_list.count = 0;
for (i = 0; i < n; i++) {
uint32 access = ents[i].perm & 7;
id_access *ida;
switch (ents[i].tag) {
case RACL_USER_OBJ:
if (racl->user_obj == NO_ENTRY)
racl->user_obj = access;
continue;
case RACL_GROUP_OBJ:
if (racl->group_obj == NO_ENTRY)
racl->group_obj = access;
continue;
case RACL_MASK:
if (racl->mask_obj == NO_ENTRY)
racl->mask_obj = access;
continue;
case RACL_OTHER:
if (racl->other_obj == NO_ENTRY)
racl->other_obj = access;
continue;
case RACL_USER:
access |= NAME_IS_USER;
break;
case RACL_GROUP:
break;
default:
continue;
}
ida = EXPAND_ITEM_LIST(&temp_ida_list, id_access, -10);
ida->id = ents[i].id;
ida->access = access;
}
if (temp_ida_list.count) {
#ifdef SMB_ACL_NEED_SORT
if (temp_ida_list.count > 1)
qsort(temp_ida_list.items, temp_ida_list.count, sizeof (id_access), id_access_sorter);
#endif
racl->names.idas = new_array(id_access, temp_ida_list.count);
memcpy(racl->names.idas, temp_ida_list.items, temp_ida_list.count * sizeof (id_access));
} else
racl->names.idas = NULL;
racl->names.count = temp_ida_list.count;
temp_ida_list.count = 0;
return True;
}
#endif /* SUPPORT_ACL_FD */
static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
rsync_acl *racl, SMB_ACL_TYPE_T type, mode_t mode)
static int get_rsync_acl(const char *fname, rsync_acl *racl,
SMB_ACL_TYPE_T type, mode_t mode)
{
SMB_ACL_T sacl;
#ifndef SUPPORT_ACL_FD
#ifndef HAVE_SOLARIS_ACLS
(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */
#endif
(void)dirfd;
(void)leaf;
#endif
#ifdef SUPPORT_XATTRS
/* --fake-super support: load ACLs from an xattr. */
if (am_root < 0) {
@@ -603,7 +481,7 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
size_t len;
int cnt;
if ((buf = get_xattr_acl(fname, fd, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)
if ((buf = get_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, &len)) == NULL)
return 0;
cnt = (len - 4*4) / (4+4);
if (len < 4*4 || len != (size_t)cnt*(4+4) + 4*4) {
@@ -636,107 +514,6 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
}
#endif
#ifdef HAVE_SOLARIS_ACLS
/* Solaris has no libacl *_at; read the ACL through the held fd via facl(2)
* when we have one. With no held fd this branch is skipped and the path-based
* call below reads the ACL (acceptable: a read can't redirect a write out of
* the tree). */
if (fd >= 0) {
if ((sacl = sys_acl_get_fd_type(fd, type)) != 0) {
BOOL ok = unpack_smb_acl(sacl, racl);
sys_acl_free_acl(sacl);
if (!ok) {
rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname);
return -1;
}
return 0;
}
if (no_acl_syscall_error(errno)) {
if (type == SMB_ACL_TYPE_ACCESS)
rsync_acl_fake_perms(racl, mode);
return 0;
}
rsyserr(FERROR_XFER, errno, "get_acl: sys_acl_get_fd_type(%s, %s)",
fname, str_acl_type(type));
return -1;
}
#endif
#ifdef SUPPORT_ACL_FD
#ifdef HAVE_LIBACL_AT
/* Read the ACL via the new libacl *_at calls; fd<0 && dirfd<0
* (e.g. a synthetic dir) falls through to the path-based call below. */
if (fd >= 0 || dirfd >= 0) {
if (fd >= 0)
sacl = sys_acl_get_file_at(fd, "", AT_EMPTY_PATH, type);
else
sacl = sys_acl_get_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type);
if (sacl != 0) {
BOOL ok = unpack_smb_acl(sacl, racl);
sys_acl_free_acl(sacl);
if (!ok) {
rsyserr(FERROR_XFER, errno, "get_acl: unpack_smb_acl(%s)", fname);
return -1;
}
return 0;
}
if (no_acl_syscall_error(errno)) {
if (type == SMB_ACL_TYPE_ACCESS)
rsync_acl_fake_perms(racl, mode);
return 0;
}
rsyserr(FERROR_XFER, errno, "get_acl: acl_get_file_at(%s, %s)",
fname, str_acl_type(type));
return -1;
}
#else
/* Race-safe path: read the ACL through the held O_NOFOLLOW fd, or via
* setxattrat(AT_SYMLINK_NOFOLLOW) on dirfd+leaf, instead of re-resolving
* fname. Only for real-root ACLs (am_root >= 0; the fake-super branch
* above already returned). */
if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {
int is_def = type == SMB_ACL_TYPE_DEFAULT;
rsync_acl_ent *ents = NULL;
int n = 0, rc;
if (fd >= 0)
rc = xacl_get_fd(fd, is_def, &ents, &n);
else
rc = xacl_get_at(dirfd, leaf, is_def, &ents, &n);
if (rc < 0) {
if (no_acl_syscall_error(errno)) {
if (type == SMB_ACL_TYPE_ACCESS)
rsync_acl_fake_perms(racl, mode);
return 0;
}
rsyserr(FERROR_XFER, errno, "get_acl: xacl_get(%s, %s)",
fname, str_acl_type(type));
return -1;
}
if (n == 0) {
/* No explicit ACL: mirror libacl's mode-derived access ACL
* (an absent default ACL stays empty). */
if (type == SMB_ACL_TYPE_ACCESS)
rsync_acl_fake_perms(racl, mode);
} else if (!unpack_acl_entries(ents, n, racl)) {
if (ents)
free(ents);
rsyserr(FERROR_XFER, errno, "get_acl: unpack_acl_entries(%s)", fname);
return -1;
}
if (ents)
free(ents);
return 0;
}
/* Neither a held fd nor a usable dirfd path (xacl_at_available() covers the
* *xattrat syscalls AND the pre-6.13 /proc/self/fd compat, so this is the
* BSDs / a /proc-less namespace / an un-pinnable entry): read the real
* destination ACL via the path-based call rather than a mode-only fake, so
* --acls stays functional where the race-safe primitive is unavailable. */
#endif /* HAVE_LIBACL_AT */
#endif
if ((sacl = sys_acl_get_file(fname, type)) != 0) {
BOOL ok = unpack_smb_acl(sacl, racl);
@@ -758,10 +535,8 @@ static int get_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
return 0;
}
/* Return the Access Control List for the given filename. When a held
* O_NOFOLLOW fd (or a dirfd+leaf) is available, the ACL is read race-safely
* through it; otherwise (fd < 0 && dirfd < 0) the path-based fallback is used. */
int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x *sxp)
/* Return the Access Control List for the given filename. */
int get_acl(const char *fname, stat_x *sxp)
{
sxp->acc_acl = create_racl();
@@ -782,7 +557,7 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x
} else if (IS_MISSING_FILE(sxp->st))
return 0;
if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,
if (get_rsync_acl(fname, sxp->acc_acl, SMB_ACL_TYPE_ACCESS,
sxp->st.st_mode) < 0) {
free_acl(sxp);
return -1;
@@ -790,7 +565,7 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x
if (S_ISDIR(sxp->st.st_mode)) {
sxp->def_acl = create_racl();
if (get_rsync_acl(fd, dirfd, leaf, fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,
if (get_rsync_acl(fname, sxp->def_acl, SMB_ACL_TYPE_DEFAULT,
sxp->st.st_mode) < 0) {
free_acl(sxp);
return -1;
@@ -800,11 +575,6 @@ int get_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname, stat_x
return 0;
}
int get_acl(const char *fname, stat_x *sxp)
{
return get_acl_fdat(-1, -1, NULL, fname, sxp);
}
/* === Send functions === */
/* Send the ida list over the file descriptor. */
@@ -927,7 +697,7 @@ static uint32 recv_acl_access(int f, uchar *name_follows_ptr)
static uchar recv_ida_entries(int f, ida_entries *ent)
{
uchar computed_mask_bits = 0;
int i, count = read_varint_bounded(f, 0, MAX_WIRE_ACL_COUNT, "ACL count");
int i, count = read_varint(f);
ent->idas = count ? new_array(id_access, count) : NULL;
ent->count = count;
@@ -943,7 +713,7 @@ static uchar recv_ida_entries(int f, ida_entries *ent)
else
id = recv_group_name(f, id, NULL);
} else if (access & NAME_IS_USER) {
if (inc_recurse && !numeric_ids)
if (inc_recurse && am_root && !numeric_ids)
id = match_uid(id);
} else {
if (inc_recurse && (!am_root || !numeric_ids))
@@ -1163,60 +933,17 @@ static mode_t change_sacl_perms(SMB_ACL_T sacl, rsync_acl *racl, mode_t old_mode
}
#endif
static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
acl_duo *duo_item, SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)
static int set_rsync_acl(const char *fname, acl_duo *duo_item,
SMB_ACL_TYPE_T type, stat_x *sxp, mode_t mode)
{
#ifndef SUPPORT_ACL_FD
#ifndef HAVE_SOLARIS_ACLS
(void)fd; /* Solaris drives the ACL via facl(2) on fd but has no SUPPORT_ACL_FD. */
#endif
(void)dirfd;
(void)leaf;
#endif
if (type == SMB_ACL_TYPE_DEFAULT
&& duo_item->racl.user_obj == NO_ENTRY) {
int rc;
#ifdef SUPPORT_XATTRS
/* --fake-super support: delete default ACL from xattrs. */
if (am_root < 0)
rc = del_def_xattr_acl(fd, fname);
rc = del_def_xattr_acl(fname);
else
#endif
#ifdef SUPPORT_ACL_FD
#ifdef HAVE_LIBACL_AT
/* Race-safe default-ACL delete via the new libacl *_at
* calls (held fd via AT_EMPTY_PATH, dirfd+leaf via AT_SYMLINK_NOFOLLOW)
* -- race-safe on every Linux kernel. fd<0 && dirfd<0 falls to path. */
if (fd >= 0)
rc = sys_acl_delete_def_file_at(fd, "", AT_EMPTY_PATH);
else if (dirfd >= 0)
rc = sys_acl_delete_def_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW);
else
#else
/* Race-safe default-ACL delete via the held fd or dirfd+leaf. Where
* neither is available (xacl_at_available() is false -- the BSDs, a
* /proc-less namespace, an un-pinnable entry; every Linux with procfs
* takes the dirfd path via *xattrat or the /proc/self/fd compat) -- fall
* back to the path-based call, preferring the documented --acls behaviour
* over refusing it where the race-safe primitive is unavailable. */
if (fd >= 0)
rc = xacl_del_default_fd(fd);
else if (dirfd >= 0 && xacl_at_available())
rc = xacl_del_default_at(dirfd, leaf);
else
#endif /* HAVE_LIBACL_AT */
#endif
#ifdef HAVE_SOLARIS_ACLS
/* Solaris: delete the default ACL through the held fd via facl(2). For a
* root receiver a missing held fd means the leaf was raced, so refuse rather
* than let the path-based delete follow it; a plain non-root receiver keeps
* the legacy path fallback (op_pin am_root != 0 rule). */
if (fd >= 0)
rc = sys_acl_delete_def_fd(fd);
else if (vfs_relpath_active() && am_root) {
errno = ELOOP;
rc = -1;
} else
#endif
rc = sys_acl_delete_def_file(fname);
if (rc < 0) {
@@ -1245,7 +972,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
SIVAL(bp, 4, ida->access);
}
}
rc = set_xattr_acl(fd, fname, type == SMB_ACL_TYPE_ACCESS, buf, len);
rc = set_xattr_acl(fname, type == SMB_ACL_TYPE_ACCESS, buf, len);
free(buf);
return rc;
#endif
@@ -1262,92 +989,6 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
if (cur_mode == (mode_t)-1)
return 0;
}
#endif
#ifdef SUPPORT_ACL_FD
#ifdef HAVE_LIBACL_AT
/* Apply the packed/perm-reconciled ACL (duo_item->sacl)
* through the new libacl *_at calls -- held fd via AT_EMPTY_PATH,
* dirfd+leaf via AT_SYMLINK_NOFOLLOW -- race-safe on every Linux kernel,
* and byte-identical to the path-based sys_acl_set_file() below. */
if (fd >= 0 || dirfd >= 0) {
int rc;
if (fd >= 0)
rc = sys_acl_set_file_at(fd, "", AT_EMPTY_PATH, type, duo_item->sacl);
else
rc = sys_acl_set_file_at(dirfd, leaf, AT_SYMLINK_NOFOLLOW, type, duo_item->sacl);
if (rc < 0) {
rsyserr(FERROR_XFER, errno, "set_acl: acl_set_file_at(%s, %s)",
fname, str_acl_type(type));
return -1;
}
if (type == SMB_ACL_TYPE_ACCESS)
sxp->st.st_mode = cur_mode;
return 0;
}
#else
/* Race-safe write: serialize the packed (and perm-reconciled)
* system ACL to the kernel xattr format and apply it through the
* held fd or dirfd+leaf -- never re-resolving fname. This matches
* exactly what sys_acl_set_file() would have written. */
if (fd >= 0 || (dirfd >= 0 && xacl_at_available())) {
int is_def = type == SMB_ACL_TYPE_DEFAULT;
rsync_acl_ent *ents;
int n = sacl_to_entries(duo_item->sacl, &ents);
int rc;
if (n < 0)
return -1;
if (fd >= 0)
rc = xacl_set_fd(fd, is_def, ents, n);
else
rc = xacl_set_at(dirfd, leaf, is_def, ents, n);
free(ents);
if (rc < 0) {
rsyserr(FERROR_XFER, errno, "set_acl: xacl_set(%s, %s)",
fname, str_acl_type(type));
return -1;
}
if (type == SMB_ACL_TYPE_ACCESS)
sxp->st.st_mode = cur_mode;
return 0;
}
/* No held fd and no usable dirfd path (xacl_at_available() is false --
* the BSDs, a /proc-less namespace, an un-pinnable entry; every Linux
* with procfs took xacl_set_at() above via *xattrat or the /proc/self/fd
* compat): prefer the documented --acls behaviour over refusing it and
* fall back to the path-based set. This re-resolves fname, so it still
* carries the parent-symlink-race exposure on those remaining platforms;
* it is the only way to honour --acls where no race-safe primitive
* exists. */
#endif /* HAVE_LIBACL_AT */
#endif
#ifdef HAVE_SOLARIS_ACLS
/* Solaris: apply the ACL through the held fd via facl(2). */
if (fd >= 0) {
if (sys_acl_set_fd_type(fd, type, duo_item->sacl) < 0) {
rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_fd_type(%s, %s)",
fname, str_acl_type(type));
return -1;
}
if (type == SMB_ACL_TYPE_ACCESS)
sxp->st.st_mode = cur_mode;
return 0;
}
if (vfs_relpath_active() && am_root) {
/* Real root always can open its own freshly-staged reg/dir/fifo leaf,
* so a missing held fd on a confined receiver means the leaf was raced
* to a symlink; sys_acl_set_file() follows the leaf, so refuse rather
* than write the attacker-supplied ACL onto a redirected inode (covers
* the top-level no-slash entry the caller's slashed-path xattr_refuse
* gate misses). A plain non-root receiver keeps the path-based fallback
* for a legitimately un-pinnable owned leaf (e.g. a 0300 dir), matching
* the operator-path op_pin rule (am_root != 0). */
errno = ELOOP;
rsyserr(FERROR_XFER, errno, "set_acl: refusing path-based ACL on %s (no held fd)",
fname);
return -1;
}
#endif
if (sys_acl_set_file(fname, type, duo_item->sacl) < 0) {
rsyserr(FERROR_XFER, errno, "set_acl: sys_acl_set_file(%s, %s)",
@@ -1365,16 +1006,11 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
* dir), and the regular mode bits on the file. Call this with fname set to
* NULL to just check if the ACL is different.
*
* When a held O_NOFOLLOW fd (or a dirfd+leaf) is supplied, the ACL is applied
* race-safely through it; otherwise (fd < 0 && dirfd < 0) the path-based
* fallback is used.
*
* If the ACL operation has a side-effect of changing the file's mode, the
* sxp->st.st_mode value will be changed to match.
*
* Returns 0 for an unchanged ACL, 1 for changed, -1 for failed. */
int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,
const struct file_struct *file, stat_x *sxp, mode_t new_mode)
int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)
{
int changed = 0;
int32 ndx;
@@ -1394,7 +1030,7 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,
if (!eq) {
changed = 1;
if (!dry_run && fname
&& set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_ACCESS,
&& set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_ACCESS,
sxp, new_mode) < 0)
return -1;
}
@@ -1411,7 +1047,7 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,
if (!eq) {
changed = 1;
if (!dry_run && fname
&& set_rsync_acl(fd, dirfd, leaf, fname, duo_item, SMB_ACL_TYPE_DEFAULT,
&& set_rsync_acl(fname, duo_item, SMB_ACL_TYPE_DEFAULT,
sxp, new_mode) < 0)
return -1;
}
@@ -1420,11 +1056,6 @@ int set_acl_fdat(int fd, int dirfd, const char *leaf, const char *fname,
return changed;
}
int set_acl(const char *fname, const struct file_struct *file, stat_x *sxp, mode_t new_mode)
{
return set_acl_fdat(-1, -1, NULL, fname, file, sxp, new_mode);
}
/* Non-incremental recursion needs to convert all the received IDs.
* This is done in a single pass after receiving the whole file-list. */
static void match_racl_ids(const item_list *racl_list)
+6 -104
View File
@@ -22,13 +22,6 @@
#include "itypes.h"
#include "ifuncs.h"
/* O_CLOEXEC is absent on some still-supported targets. The random-source fd
* is read and closed synchronously, so the established zero-value fallback is
* sufficient without adding a configure dependency. */
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
extern int read_only;
extern char *password_file;
extern struct name_num_obj valid_auth_checksums;
@@ -64,31 +57,10 @@ void base64_encode(const char *buf, int len, char *out, int pad)
out[i] = '\0';
}
/* Fill buf with len bytes from the kernel CSPRNG. Returns 1 on success.
* We read /dev/urandom directly rather than depending on getrandom()/
* arc4random_buf() availability so this works on every platform rsync
* targets without new configure probes. */
static int get_random_bytes(char *buf, int len)
{
int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
int got = 0;
if (fd < 0)
return 0;
while (got < len) {
int n = read(fd, buf + got, len - got);
if (n <= 0)
break;
got += n;
}
close(fd);
return got == len;
}
/* Generate a challenge buffer and return it base64-encoded. */
static void gen_challenge(const char *addr, char *challenge)
{
char input[32];
char rnd[32];
char digest[MAX_DIGEST_LEN];
struct timeval tv;
int len;
@@ -102,16 +74,6 @@ static void gen_challenge(const char *addr, char *challenge)
SIVAL(input, 24, getpid());
len = sum_init(valid_auth_checksums.negotiated_nni, 0);
/* The challenge must be unpredictable to a network observer; addr+time
* +pid alone is ~35 bits and lets an attacker enumerate the preimage
* offline. Hash 32 bytes from the kernel RNG first so the digest
* carries full entropy, keeping the legacy inputs as a mix-in so a
* urandom failure degrades to (never below) the old behaviour. */
if (get_random_bytes(rnd, sizeof rnd))
sum_update(rnd, sizeof rnd);
else
rprintf(FWARNING, "gen_challenge: /dev/urandom unavailable, "
"falling back to time-based challenge\n");
sum_update(input, sizeof input);
sum_end(digest);
@@ -148,25 +110,10 @@ static const char *check_secret(int module, const char *user, const char *group,
char *err;
FILE *fh;
/* Daemon 'secrets file = PATH' open. A planted symlink would be
* followed and the strict-modes fstat() check below runs on the target
* inode, so a symlink to /etc/shadow (0640 root:shadow) would pass and
* the daemon would auth against shadow hashes. Refuse symlinks not
* owned by uid 0 or our euid. */
if (!fname || !*fname)
if (!fname || !*fname || (fh = fopen(fname, "r")) == NULL)
return "no secrets file";
{
int fd = vfs_open_owner_walk(fname, O_RDONLY, 0, 0);
if (fd < 0)
return "no secrets file";
fh = fdopen(fd, "r");
if (!fh) {
close(fd);
return "no secrets file";
}
}
if (vfs_fstat(fileno(fh), &st) == -1) {
if (do_fstat(fileno(fh), &st) == -1) {
rsyserr(FLOG, errno, "fstat(%s)", fname);
ok = 0;
} else if (lp_strict_modes(module)) {
@@ -237,23 +184,13 @@ static const char *getpassf(const char *filename)
} else {
int fd;
/* --password-file=PATH client open. Its first line is sent as the
* auth response, so a planted symlink leaks the target's content
* (e.g. shadow hashes) to a malicious daemon; the vfs_stat()
* other-access check runs on the target mode and passes 0640
* root:shadow. Refuse symlinks not owned by uid 0 or our euid. */
if ((fd = vfs_open_owner_walk(filename, O_RDONLY, 0, 0)) < 0) {
if ((fd = open(filename,O_RDONLY)) < 0) {
rsyserr(FERROR, errno, "could not open password file %s", filename);
exit_cleanup(RERR_SYNTAX);
}
/* fstat the opened fd, not the pathname: a same-object check
* (matching check_secret() above) so an attacker who swaps the
* path between open and check can't make the owner/mode test
* validate a different inode than the one we read the password
* from. */
if (vfs_fstat(fd, &st) == -1) {
rsyserr(FERROR, errno, "fstat(%s)", filename);
if (do_stat(filename, &st) == -1) {
rsyserr(FERROR, errno, "stat(%s)", filename);
exit_cleanup(RERR_SYNTAX);
}
if ((st.st_mode & 06) != 0) {
@@ -303,35 +240,6 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
return "";
negotiate_daemon_auth(f_out, 0);
/* Enforce a configured minimum auth digest (default: none). This refuses
* a peer that negotiated -- or, via an omitted digest list / old protocol,
* fell back to -- a digest weaker than the operator-required floor, e.g. a
* client downgraded to md5/md4. Lower rank == stronger (the auth list is
* ordered strongest-first), so a higher rank than the floor is too weak. */
{
const char *min_digest = lp_auth_digest(module);
if (min_digest && *min_digest) {
int floor_rank = auth_digest_rank(min_digest);
int got_rank = auth_digest_rank(valid_auth_checksums.negotiated_nni->name);
if (floor_rank < 0) {
rprintf(FLOG, "auth failed on module %s from %s (%s): the "
"configured 'auth digest = %s' is not a supported digest "
"on this build\n",
lp_name(module), host, addr, min_digest);
return NULL;
}
if (got_rank < 0 || got_rank > floor_rank) {
rprintf(FLOG, "auth failed on module %s from %s (%s): negotiated "
"auth digest %s is weaker than the required "
"'auth digest = %s'\n",
lp_name(module), host, addr,
valid_auth_checksums.negotiated_nni->name, min_digest);
return NULL;
}
}
}
gen_challenge(addr, challenge);
io_printf(f_out, "%s%s\n", leader, challenge);
@@ -347,13 +255,7 @@ char *auth_server(int f_in, int f_out, int module, const char *host,
users = strdup(users);
/* conf_strtok() honours the documented leading-comma form: a value that
* starts with a comma splits on commas ALONE, so an entry may contain
* spaces -- which is how a group name with a space is written. Splitting
* on whitespace here tore such an entry apart, so the rule the admin wrote
* never matched and a rule they never wrote appeared from its tail. The
* daemon's gid field already uses this parser (clientserver.c). */
for (tok = conf_strtok(users); tok; tok = conf_strtok(NULL)) {
for (tok = strtok(users, " ,\t"); tok; tok = strtok(NULL, " ,\t")) {
char *opts;
/* See if the user appended :deny, :ro, or :rw. */
if ((opts = strchr(tok, ':')) != NULL) {
+42 -133
View File
@@ -34,33 +34,12 @@ extern char backup_dir_buf[MAXPATHLEN];
extern char *backup_suffix;
extern char *backup_dir;
/* Pin a backup SOURCE leaf with a confined O_NOFOLLOW fd (via the operator
* owner-walk resolver, like set_file_attrs's op_leaf_fd) so the ACL/xattr the
* backup caches off it are read through the held fd -- a parent-symlink race
* can't redirect the read out of the module. Returns -1 for a non-hardened
* receiver (caller path-reads) or for a raced/absent leaf on a hardened one
* (caller skips the cache rather than read through a flippable path; use
* backup_metadata_hardened() to tell the two -1 cases apart). */
int backup_metadata_hardened(void)
{
return vfs_relpath_active() && !vfs_symlink_optout_allowed();
}
int backup_source_fd(const char *path)
{
#if defined AT_FDCWD && defined O_NOFOLLOW
if (backup_metadata_hardened() && path && *path)
return vfs_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH);
#endif
return -1;
}
/* Returns -1 on error, 0 on missing dir, and 1 on present dir. */
static int validate_backup_dir(void)
{
STRUCT_STAT st;
if (vfs_lstat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_OPERATOR_PATH) < 0) {
if (do_lstat(backup_dir_buf, &st) < 0) {
if (errno == ENOENT)
return 0;
rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf);
@@ -119,7 +98,7 @@ static BOOL copy_valid_path(const char *fname)
for ( ; b; name = b + 1, b = strchr(name, '/')) {
*b = '\0';
while (vfs_mkdir(VFS_AT_FDCWD, backup_dir_buf, ACCESSPERMS, VFS_OPERATOR_PATH) < 0) {
while (do_mkdir(backup_dir_buf, ACCESSPERMS) < 0) {
if (errno == EEXIST) {
val = validate_backup_dir();
if (val > 0)
@@ -135,36 +114,27 @@ static BOOL copy_valid_path(const char *fname)
/* Try to transfer the directory settings of the actual dir
* that the files are coming from. */
if (x_stat(rel, &sx.st, NULL, VFS_OPERATOR_PATH) < 0)
if (x_stat(rel, &sx.st, NULL) < 0)
rsyserr(FERROR, errno, "backup stat %s failed", full_fname(rel));
else {
struct file_struct *file;
if (!(file = make_file(rel, NULL, NULL, 0, NO_FILTERS)))
continue;
#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS
{ /* read the source dir's ACL/xattr through a confined fd */
int bfd = backup_source_fd(rel);
if (!backup_metadata_hardened() || bfd >= 0) {
# ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode)) {
get_acl_fdat(bfd, -1, NULL, rel, &sx);
cache_tmp_acl(file, &sx);
free_acl(&sx);
}
# endif
# ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
get_xattr(rel, bfd, &sx);
cache_tmp_xattr(file, &sx);
free_xattr(&sx);
}
# endif
}
if (bfd >= 0)
close(bfd);
#ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode)) {
get_acl(rel, &sx);
cache_tmp_acl(file, &sx);
free_acl(&sx);
}
#endif
set_file_attrs(backup_dir_buf, file, NULL, NULL, ATTRS_OPERATOR_PATH);
#ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
get_xattr(rel, &sx);
cache_tmp_xattr(file, &sx);
free_xattr(&sx);
}
#endif
set_file_attrs(backup_dir_buf, file, NULL, NULL, 0);
unmake_file(file);
}
@@ -189,15 +159,12 @@ char *get_backup_name(const char *fname)
if (backup_dir) {
static int initialized = 0;
if (!initialized) {
char dirbuf[MAXPATHLEN];
int ret;
if (strlcpy(dirbuf, backup_dir_buf, sizeof dirbuf) >= sizeof dirbuf) {
errno = ENAMETOOLONG;
return NULL;
}
if (backup_dir_len > 1)
dirbuf[backup_dir_len-1] = '\0';
ret = vfs_make_path(dirbuf, 0, VFS_OPERATOR_PATH);
backup_dir_buf[backup_dir_len-1] = '\0';
ret = make_path(backup_dir_buf, 0);
if (backup_dir_len > 1)
backup_dir_buf[backup_dir_len-1] = '/';
if (ret < 0)
return NULL;
initialized = 1;
@@ -230,11 +197,7 @@ static inline int link_or_rename(const char *from, const char *to,
if (IS_SPECIAL(stp->st_mode) || IS_DEVICE(stp->st_mode))
return 0; /* Use copy code. */
#endif
/* from = the live dest file being backed up (a transfer path); to = the
* --backup-dir path (operator). Per-operand policy keeps the transfer
* source under the secure receiver resolve and only owner-walks the
* operator backup parent. */
if (vfs_link_at(from, to, 0, VFS_OPERATOR_PATH) == 0) {
if (do_link(from, to) == 0) {
if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: HLINK %s successful.\n", from);
return 2;
@@ -244,12 +207,11 @@ static inline int link_or_rename(const char *from, const char *to,
return 0;
}
#endif
if (vfs_rename_at(from, to, 0, VFS_OPERATOR_PATH) == 0) {
if (do_rename(from, to) == 0) {
if (stp->st_nlink > 1 && !S_ISDIR(stp->st_mode)) {
/* If someone has hard-linked the file into the backup
* dir, rename() might return success but do nothing! from is the
* transfer-side source, so unlink it under the secure resolve (0). */
robust_unlink(from, 0); /* Just in case... */
* dir, rename() might return success but do nothing! */
robust_unlink(from); /* Just in case... */
}
if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: RENAME %s successful.\n", from);
@@ -261,7 +223,7 @@ static inline int link_or_rename(const char *from, const char *to,
/* Hard-link, rename, or copy an item to the backup name. Returns 0 for
* failure, 1 if item was moved, 2 if item was duplicated or hard linked
* into backup area, or 3 if item doesn't exist or isn't a regular file. */
static int make_backup_inner(const char *fname, BOOL prefer_rename)
int make_backup(const char *fname, BOOL prefer_rename)
{
stat_x sx;
struct file_struct *file;
@@ -271,44 +233,12 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
init_stat_x(&sx);
/* Return success if no file to keep. */
if (x_lstat(fname, &sx.st, NULL, VFS_OPERATOR_PATH) < 0)
if (x_lstat(fname, &sx.st, NULL) < 0)
return 3;
if (!(buf = get_backup_name(fname)))
return 0;
#ifdef SUPPORT_LINKS
/* Honor --safe-links BEFORE the hard-link / rename fast path. When
* CAN_HARDLINK_SYMLINK is defined, link_or_rename() would otherwise
* hard-link an escaping symlink (e.g. ../../etc/passwd) into the backup
* area and "goto success", skipping the safe_symlinks check in the
* copy-fallback path below -- silently preserving an unsafe link that
* --safe-links was meant to drop. Match the copy path: don't back up an
* unsafe symlink. */
if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) {
char lnkbuf[MAXPATHLEN];
int llen = vfs_readlink(fname, lnkbuf, MAXPATHLEN - 1);
/* A failed readlink means we can't verify the target, so fail
* closed: skip the backup rather than let the hard-link fast path
* preserve a possibly-unsafe symlink unchecked. */
if (llen <= 0) {
if (INFO_GTE(SYMSAFE, 1))
rprintf(FINFO, "not backing up symlink with unreadable target \"%s\"\n", fname);
ret = 2;
goto success;
}
lnkbuf[llen] = '\0';
if (unsafe_symlink(lnkbuf, fname)) {
if (INFO_GTE(SYMSAFE, 1)) {
rprintf(FINFO, "not backing up unsafe symlink \"%s\" -> \"%s\"\n",
fname, lnkbuf);
}
ret = 2;
goto success;
}
}
#endif
/* Try a hard-link or a rename first. Using rename is not atomic, but
* is more efficient than forcing a copy for larger files when no hard-
* linking is possible. */
@@ -316,7 +246,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
goto success;
if (errno == EEXIST || errno == EISDIR) {
STRUCT_STAT bakst;
if (vfs_lstat(VFS_AT_FDCWD, buf, &bakst, VFS_OPERATOR_PATH) == 0) {
if (do_lstat(buf, &bakst) == 0) {
int flags = get_del_for_flag(bakst.st_mode) | DEL_FOR_BACKUP | DEL_RECURSE;
if (delete_item(buf, bakst.st_mode, flags) != 0)
return 0;
@@ -329,34 +259,25 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
if (!(file = make_file(fname, NULL, &sx.st, 0, NO_FILTERS)))
return 3; /* the file could have disappeared */
#if defined SUPPORT_ACLS || defined SUPPORT_XATTRS
{ /* read the source file's ACL/xattr through a confined fd */
int bfd = backup_source_fd(fname);
if (!backup_metadata_hardened() || bfd >= 0) {
# ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode)) {
get_acl_fdat(bfd, -1, NULL, fname, &sx);
cache_tmp_acl(file, &sx);
free_acl(&sx);
}
# endif
# ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
get_xattr(fname, bfd, &sx);
cache_tmp_xattr(file, &sx);
free_xattr(&sx);
}
# endif
#ifdef SUPPORT_ACLS
if (preserve_acls && !S_ISLNK(file->mode)) {
get_acl(fname, &sx);
cache_tmp_acl(file, &sx);
free_acl(&sx);
}
if (bfd >= 0)
close(bfd);
#endif
#ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
get_xattr(fname, &sx);
cache_tmp_xattr(file, &sx);
free_xattr(&sx);
}
#endif
/* Check to see if this is a device file, or link */
if ((am_root && preserve_devices && IS_DEVICE(file->mode))
|| (preserve_specials && IS_SPECIAL(file->mode))) {
if (vfs_mknod(VFS_AT_FDCWD, buf, file->mode, sx.st.st_rdev, VFS_OPERATOR_PATH) < 0)
if (do_mknod(buf, file->mode, sx.st.st_rdev) < 0)
rsyserr(FERROR, errno, "mknod %s failed", full_fname(buf));
else if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: DEVICE %s successful.\n", fname);
@@ -373,7 +294,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
}
ret = 2;
} else {
if (vfs_symlink(sl, VFS_AT_FDCWD, buf, VFS_OPERATOR_PATH) < 0)
if (do_symlink(sl, buf) < 0)
rsyserr(FERROR, errno, "link %s -> \"%s\"", full_fname(buf), sl);
else if (DEBUG_GTE(BACKUP, 1))
rprintf(FINFO, "make_backup: SYMLINK %s successful.\n", fname);
@@ -397,7 +318,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
/* Copy to backup tree if a file. */
if (!ret) {
if (copy_file(fname, buf, -1, file->mode, VFS_OPERATOR_PATH) < 0) {
if (copy_file(fname, buf, -1, file->mode) < 0) {
rsyserr(FERROR, errno, "keep_backup failed: %s -> \"%s\"",
full_fname(fname), buf);
unmake_file(file);
@@ -416,7 +337,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
save_preserve_xattrs = preserve_xattrs;
preserve_xattrs = 0;
set_file_attrs(buf, file, NULL, fname, ATTRS_OPERATOR_PATH | ATTRS_ACCURATE_TIME);
set_file_attrs(buf, file, NULL, fname, ATTRS_ACCURATE_TIME);
preserve_xattrs = save_preserve_xattrs;
unmake_file(file);
@@ -432,15 +353,3 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
rprintf(FINFO, "backed up %s to %s\n", fname, buf);
return ret;
}
int make_backup(const char *fname, BOOL prefer_rename)
{
int ret;
/* The --backup-dir is an operator-supplied path: resolve it (and the
* tail/rename beneath it) with the ownership walk so a foreign-owned
* symlink component is refused while the operator's own is followed --
* absolute and relative alike. --insecure-links / "insecure links ="
* restores legacy following. */
ret = make_backup_inner(fname, prefer_rename);
return ret;
}
+20 -56
View File
@@ -75,7 +75,7 @@ static int *flag_ptr[] = {
NULL
};
static const char *const flag_name[] = {
static char *flag_name[] = {
"--recurse (-r)",
"--owner (-o)",
"--group (-g)",
@@ -166,33 +166,25 @@ static int write_arg(const char *arg)
const char *x, *s;
int len, err = 0;
/* Emit a "--opt=" prefix unquoted only when it is a plain option token;
* a metacharacter before '=' (an attacker-shaped arg) must be quoted
* along with the rest, or it would run raw in the replay script. */
if (*arg == '-' && (x = strchr(arg, '=')) != NULL) {
const char *p = arg;
while (p < x && (*p == '-' || *p == '_'
|| (*p >= '0' && *p <= '9')
|| (*p >= 'A' && *p <= 'Z')
|| (*p >= 'a' && *p <= 'z')))
p++;
if (p == x) {
err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;
arg += x - arg + 1;
}
err |= write(batch_sh_fd, arg, x - arg + 1) != x - arg + 1;
arg += x - arg + 1;
}
/* Single-quote unconditionally so every shell metacharacter (backtick,
* newline, redirection, ...) stays literal in the replay script. An
* embedded ' is emitted as the '\'' close/escape/reopen sequence. */
err |= write(batch_sh_fd, "'", 1) != 1;
for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) {
err |= write(batch_sh_fd, s, x - s) != x - s;
err |= write(batch_sh_fd, "'\\''", 4) != 4;
if (strpbrk(arg, " \"'&;|[]()$#!*?^\\") != NULL) {
err |= write(batch_sh_fd, "'", 1) != 1;
for (s = arg; (x = strchr(s, '\'')) != NULL; s = x + 1) {
err |= write(batch_sh_fd, s, x - s + 1) != x - s + 1;
err |= write(batch_sh_fd, "'", 1) != 1;
}
len = strlen(s);
err |= write(batch_sh_fd, s, len) != len;
err |= write(batch_sh_fd, "'", 1) != 1;
return err;
}
len = strlen(s);
err |= write(batch_sh_fd, s, len) != len;
err |= write(batch_sh_fd, "'", 1) != 1;
len = strlen(arg);
err |= write(batch_sh_fd, arg, len) != len;
return err;
}
@@ -202,7 +194,7 @@ static int write_opt(const char *opt, const char *arg)
{
int len = strlen(opt);
int err = write(batch_sh_fd, " ", 1) != 1;
err |= write(batch_sh_fd, opt, len) != len;
err = write(batch_sh_fd, opt, len) != len ? 1 : 0;
if (arg) {
err |= write(batch_sh_fd, "=", 1) != 1;
err |= write_arg(arg);
@@ -218,16 +210,6 @@ static void write_filter_rules(int fd)
for (ent = filter_list.head; ent; ent = ent->next) {
unsigned int plen;
char *p = get_rule_prefix(ent, "- ", 0, &plen);
/* A filter pattern is one here-doc line; an embedded newline would let
* a crafted pattern (e.g. from a dir-merge/--exclude-from file in an
* untrusted tree) forge the "#E#" terminator on its own line and inject
* shell commands into the generated replay script. Such a pattern also
* can't round-trip the line-delimited here-doc, so refuse it fail-closed
* rather than emit an injectable script. */
if (ent->pattern && strchr(ent->pattern, '\n')) {
rprintf(FERROR, "cannot write a filter rule containing a newline to the batch replay script\n");
exit_cleanup(RERR_SYNTAX);
}
write_buf(fd, p, plen);
write_sbuf(fd, ent->pattern);
if (ent->rflags & FILTRULE_DIRECTORY)
@@ -242,45 +224,27 @@ static void write_filter_rules(int fd)
/* This sets batch_fd and (for --write-batch) batch_sh_fd. */
void open_batch_files(void)
{
/* --write-batch/--read-batch are operator-supplied; a planted symlink
* could truncate+overwrite an arbitrary file (write side) or stream
* attacker bytes into the protocol parser (read side). Refuse symlinks
* not owned by uid 0 or our euid anywhere in the path. */
if (write_batch) {
char filename[MAXPATHLEN];
stringjoin(filename, sizeof filename, batch_name, ".sh", NULL);
batch_sh_fd = vfs_open_owner_walk(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR | S_IXUSR, 0);
batch_sh_fd = do_open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IXUSR);
if (batch_sh_fd < 0) {
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(filename));
exit_cleanup(RERR_FILESELECT);
}
/* O_BINARY: the batch stream is binary protocol data; without it
* Cygwin et al apply CRLF translation and corrupt it. Unlike
* vfs_open(), vfs_open_owner_walk passes flags verbatim. */
batch_fd = vfs_open_owner_walk(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, S_IRUSR | S_IWUSR, 0);
batch_fd = do_open(batch_name, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
} else if (strcmp(batch_name, "-") == 0)
batch_fd = STDIN_FILENO;
else
batch_fd = vfs_open_owner_walk(batch_name, O_RDONLY | O_BINARY, S_IRUSR | S_IWUSR, 0);
batch_fd = do_open(batch_name, O_RDONLY, S_IRUSR | S_IWUSR);
if (batch_fd < 0) {
rsyserr(FERROR, errno, "Batch file %s open error", full_fname(batch_name));
exit_cleanup(RERR_FILEIO);
}
/* --read-batch: the file's bytes drive the protocol parser, so refuse
* non-regular files (FIFO, device, socket) at the batch path. */
if (!write_batch && batch_fd != STDIN_FILENO) {
STRUCT_STAT st;
if (vfs_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode)) {
rprintf(FERROR, "Batch file %s is not a regular file\n",
full_fname(batch_name));
exit_cleanup(RERR_FILEIO);
}
}
}
/* This routine tries to write out an equivalent --read-batch command
-31
View File
@@ -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)
{
+5 -22
View File
@@ -87,24 +87,6 @@ struct name_num_obj valid_auth_checksums = {
"daemon auth checksum", NULL, 0, 0, valid_auth_checksums_items
};
/* Return the strength rank (0 = strongest) of a daemon-auth digest by name in
* valid_auth_checksums_items[], which is listed strongest-first; -1 if the name
* is not a supported auth digest on this build. Used by the daemon's
* "auth digest" floor to compare the negotiated digest against the minimum. */
int auth_digest_rank(const char *name)
{
struct name_num_item *nni;
int rank = 0;
if (!name || !*name)
return -1;
for (nni = valid_auth_checksums_items; nni->name; nni++, rank++) {
if (strcasecmp(nni->name, name) == 0)
return rank;
}
return -1;
}
/* These cannot make use of openssl, so they're marked just as built-in */
struct name_num_item implied_checksum_md4 =
{ CSUM_MD4, NNI_BUILTIN, "md4", NULL };
@@ -194,7 +176,7 @@ void parse_checksum_choice(int final_call)
if (valid_checksums.negotiated_nni)
xfer_sum_nni = file_sum_nni = valid_checksums.negotiated_nni;
else {
const char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL;
char *cp = checksum_choice ? strchr(checksum_choice, ',') : NULL;
if (cp) {
xfer_sum_nni = parse_csum_name(checksum_choice, cp - checksum_choice);
file_sum_nni = parse_csum_name(cp+1, -1);
@@ -384,8 +366,9 @@ void get_checksum2(char *buf, int32 len, char *sum)
mdfour_begin(&m);
if (len > len1 || !buf1) {
free(buf1);
if (len > len1) {
if (buf1)
free(buf1);
buf1 = new_array(char, len+4);
len1 = len;
}
@@ -423,7 +406,7 @@ void file_checksum(const char *fname, const STRUCT_STAT *st_p, char *sum)
int32 remainder;
int fd;
fd = vfs_open_checklinks(fname);
fd = do_open_checklinks(fname);
if (fd == -1) {
memset(sum, 0, file_sum_len);
return;
+7 -89
View File
@@ -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) {
@@ -164,7 +132,6 @@ struct chmod_mode_struct *parse_chmod(const char *modestr,
break;
case 'a':
where |= 0111;
topbits |= 06000; /* a+s sets BOTH setuid and setgid (like chmod(1)) */
break;
case '+':
op = CHMOD_ADD;
@@ -192,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;
@@ -272,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. */
@@ -293,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;
+3 -11
View File
@@ -58,7 +58,7 @@ void close_all(void)
max_fd = sysconf(_SC_OPEN_MAX) - 1;
for (fd = max_fd; fd >= 0; fd--) {
if ((ret = vfs_fstat(fd, &st)) == 0) {
if ((ret = do_fstat(fd, &st)) == 0) {
if (is_a_socket(fd))
ret = shutdown(fd, 2);
ret = close(fd);
@@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
switch_step++;
if (cleanup_fname)
vfs_unlink(VFS_AT_FDCWD, cleanup_fname, 0);
do_unlink(cleanup_fname);
if (exit_code)
kill_all(SIGUSR1);
if (cleanup_pid && cleanup_pid == getpid()) {
@@ -269,16 +269,8 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
break;
}
if (called_from_signal_handler) {
#ifdef GCOV_COVERAGE
/* _exit() bypasses the gcov atexit flush; rsync's generator (and
* other processes) normally finish via the signal handler, so
* without this they would write no .gcda. Harmless otherwise. */
extern void __gcov_dump(void);
__gcov_dump();
#endif
if (called_from_signal_handler)
_exit(exit_code);
}
exit(exit_code);
}
+1 -1
View File
@@ -167,7 +167,7 @@ int read_proxy_protocol_header(int fd)
char sig[PROXY_V2_SIG_SIZE];
char ver_cmd;
char fam;
unsigned char len[2];
char len[2];
union {
struct {
char src_addr[4];
+21 -269
View File
@@ -30,7 +30,6 @@ extern int list_only;
extern int am_sender;
extern int am_server;
extern int am_daemon;
extern int am_chrooted;
extern int am_root;
extern int msgs2stderr;
extern int rsync_port;
@@ -39,10 +38,8 @@ extern int ignore_errors;
extern int preserve_xattrs;
extern int kluge_around_eof;
extern int munge_symlinks;
extern int use_secure_symlinks;
extern int open_noatime;
extern int sanitize_paths;
extern int daemon_config_filter_file;
extern int numeric_ids;
extern int filesfrom_fd;
extern int remote_protocol;
@@ -71,8 +68,6 @@ extern gid_t our_gid;
char *auth_user;
char *daemon_auth_choices;
/* read_args() enforces MAX_DAEMON_ARGS and reports "too many daemon arguments"
* before a daemon client can grow argv without bound. */
int read_only = 0;
int module_id = -1;
int pid_file_fd = -1;
@@ -84,32 +79,11 @@ struct chmod_mode_struct *daemon_chmod_modes;
#define EARLY_INPUT_CMD "#early_input="
#define EARLY_INPUT_CMDLEN (sizeof EARLY_INPUT_CMD - 1)
/* Fallback bound on each peer-driven daemon handshake phase when no positive
* "timeout" is configured. A module value can shorten the pre-auth and
* argument-read phases, but cannot extend either beyond this limit. */
#define DAEMON_HANDSHAKE_TIMEOUT 60
static int daemon_handshake_timeout(int module)
{
int timeout = lp_timeout(module);
/* "timeout" is parsed with atoi(), so negative values are possible. */
if (timeout <= 0 || timeout > DAEMON_HANDSHAKE_TIMEOUT)
timeout = DAEMON_HANDSHAKE_TIMEOUT;
return timeout;
}
/* module_dirlen is the length of the module_dir string when in daemon
* mode and module_dir is not "/"; otherwise 0. (Note that a chroot-
* enabled module can have a non-"/" module_dir these days.) */
char *module_dir = NULL;
unsigned int module_dirlen = 0;
/* An fd held open on the served module root, captured while the daemon is still
* positioned there (and privileged) -- so the sender's directory scan can be
* confined beneath the module by resolving module-relative paths against this fd,
* without re-walking (and re-permission-checking) the absolute module path as the
* dropped-privilege module uid. -1 when not a daemon or not yet captured. */
int module_dirfd = -1;
char *full_module_path;
@@ -182,12 +156,7 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int
if (!am_client) {
char *motd = lp_motd_file();
if (motd && *motd) {
/* 'motd file = PATH': motd content is sent to every client, so
* a planted symlink would leak the target's bytes. Refuse
* symlinks not owned by uid 0 or our euid. */
int motd_fd = vfs_open_owner_walk(motd, O_RDONLY, 0, 0);
FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL;
if (!f && motd_fd >= 0) close(motd_fd);
FILE *f = fopen(motd, "r");
while (f && !feof(f)) {
int len = fread(buf, 1, bufsiz - 1, f);
if (len > 0)
@@ -291,30 +260,19 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
if (!user)
user = getenv("LOGNAME");
if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0) {
free(modname);
if (exchange_protocols(f_in, f_out, line, sizeof line, 1) < 0)
return -1;
}
if (early_input_file) {
STRUCT_STAT st;
/* --early-input-file=PATH: refuse symlinks not owned by uid 0 or
* our euid anywhere in the path. */
int ei_fd = vfs_open_owner_walk(early_input_file, O_RDONLY, 0, 0);
FILE *f = ei_fd >= 0 ? fdopen(ei_fd, "rb") : NULL;
if (!f && ei_fd >= 0) close(ei_fd);
if (!f || vfs_fstat(fileno(f), &st) < 0) {
FILE *f = fopen(early_input_file, "rb");
if (!f || do_fstat(fileno(f), &st) < 0) {
rsyserr(FERROR, errno, "failed to open %s", early_input_file);
if (f)
fclose(f);
free(modname);
return -1;
}
early_input_len = st.st_size;
if (early_input_len > (int)sizeof line) {
rprintf(FERROR, "%s is > %d bytes.\n", early_input_file, (int)sizeof line);
fclose(f);
free(modname);
return -1;
}
if (early_input_len > 0) {
@@ -323,8 +281,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
int len;
if (feof(f)) {
rprintf(FERROR, "Early EOF in %s\n", early_input_file);
fclose(f);
free(modname);
return -1;
}
len = fread(line, 1, early_input_len, f);
@@ -401,7 +357,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
while (1) {
if (!read_line_old(f_in, line, sizeof line, 0)) {
rprintf(FERROR, "rsync: didn't get server startup line\n");
free(modname);
return -1;
}
@@ -425,7 +380,6 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
rprintf(FERROR, "%s\n", line);
/* This is always fatal; the server will now
* close the socket. */
free(modname);
return -1;
}
@@ -587,7 +541,6 @@ static pid_t start_pre_exec(const char *cmd, int *arg_fd_ptr, int *error_fd_ptr)
status = shell_exec(cmd);
gcov_flush();
if (!WIFEXITED(status))
_exit(1);
_exit(WEXITSTATUS(status));
@@ -803,9 +756,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
}
read_only = lp_read_only(i); /* may also be overridden by auth_server() */
/* The module is now known, so its local timeout policy can tighten the
* absolute deadline while the claimed slot is awaiting authentication. */
set_daemon_handshake_timeout(daemon_handshake_timeout(i));
auth_user = auth_server(f_in, f_out, i, host, addr, "@RSYNCD: AUTHREQD ");
if (!auth_user) {
@@ -813,10 +763,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
return -1;
}
set_env_str("RSYNC_USER_NAME", auth_user);
/* Do not count local setup or operator hooks against a peer's read time.
* In particular, the post-xfer parent and pre-xfer/name-converter children
* are forked below and must never inherit an armed asynchronous deadline. */
set_daemon_handshake_timeout(0);
module_id = i;
@@ -925,17 +871,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
} else
set_filter_dir(module_dir, module_dirlen);
/* Snapshot the module root for the VFS confinement checks now that the
* path is final. The root dirfd is pinned later (below); this first call
* must precede any VFS open of an operator-supplied path -- the filter/
* include files just below, and the log file -- so they see the boundary. */
vfs_set_module_root(module_dir, module_dirlen, -1);
/* Everything loaded from here to the end of the exclude block is the
* operator's own configuration, so it keeps the ownership walk without the
* module-confinement parse_filter_file() applies to peer-driven merges. */
daemon_config_filter_file = 1;
p = lp_filter(module_id);
parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),
XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3);
@@ -957,8 +892,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT),
XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3 | XFLG_OLD_PREFIXES);
daemon_config_filter_file = 0;
log_init(1);
#if defined HAVE_SETENV || defined HAVE_PUTENV
@@ -992,7 +925,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
set_env_num("RSYNC_EXIT_STATUS", status);
if (shell_exec(lp_postxfer_exec(module_id)) < 0)
status = -1;
gcov_flush();
_exit(status);
}
}
@@ -1044,34 +976,16 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
}
if (use_chroot) {
/* Cache timezone data before chroot makes /etc/localtime inaccessible */
tzset();
/* Flush gcov counters now: after chroot the build-tree .gcda
* paths are unreachable, so everything this child has executed
* so far (the whole rsync_module() pre-chroot path) would
* otherwise be lost. Post-chroot coverage from this child is
* still unrecordable -- accepted, documented in
* testsuite/COVERAGE.md. */
gcov_flush();
if (chroot(module_chdir)) {
rsyserr(FLOG, errno, "chroot(\"%s\") failed", module_chdir);
io_printf(f_out, "@ERROR: chroot failed\n");
return -1;
}
am_chrooted = 1;
module_chdir = module_dir;
}
if (!change_dir(module_chdir, CD_NORMAL))
return path_failure(f_out, module_chdir, True);
/* Pin the module root by identity now -- cwd is the served root and we are
* still privileged -- so the sender's later directory scans resolve against
* this fd rather than re-walking the absolute module path post-setuid. */
#if defined HAVE_FDOPENDIR && defined O_DIRECTORY
module_dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
#endif
/* Update the VFS snapshot with the now-pinned root dirfd. */
vfs_set_module_root(module_dir, module_dirlen, module_dirfd);
if (module_dirlen)
sanitize_paths = 1;
@@ -1081,7 +995,7 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
STRUCT_STAT st;
char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */
strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */
if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) {
if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) {
rprintf(FLOG, "Symlink munging is unsafe when a %s directory exists.\n",
prefix);
io_printf(f_out, "@ERROR: daemon security issue -- contact admin\n", name);
@@ -1089,18 +1003,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
}
}
/* Enable secure symlink handling for any non-chrooted daemon module, and
* for a chroot module with a /./ inner boundary (module_dirlen) -- there
* the kernel chroot confines the outer path but not the inner module, so
* the receiver finish/rename path must still resolve beneath the module
* root. This prevents TOCTOU race attacks where an attacker could switch a
* directory to a symlink between path validation and file open. Match the
* gate in vfs_relpath_active() (syscall.c) -- the protection has nothing
* to do with symlink munging, so a module configured with "munge symlinks =
* false" must still get the secure-open path. */
use_secure_symlinks = am_daemon && (!am_chrooted || module_dirlen)
&& !vfs_symlink_optout_allowed();
if (gid_list.count) {
gid_t *gid_array = gid_list.items;
if (setgid(gid_array[0])) {
@@ -1152,14 +1054,9 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
}
}
/* This deadline is checked only in the read path, so the preceding local
* setup and hooks can take as long as necessary. Keep one absolute bound
* across both read_args() calls: anonymous modules must not be able to pin
* a max-connections slot by trickling an unterminated argument forever. */
set_daemon_handshake_timeout(daemon_handshake_timeout(module_id));
io_printf(f_out, "@RSYNCD: OK\n");
read_args(f_in, name, line, sizeof line, rl_nulls, 1, &argv, &argc, &request);
read_args(f_in, name, line, sizeof line, rl_nulls, &argv, &argc, &request);
orig_argv = argv;
save_munge_symlinks = munge_symlinks;
@@ -1169,12 +1066,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
if (protect_args && ret) {
orig_early_argv = orig_argv;
protect_args = 2;
read_args(f_in, name, line, sizeof line, 1, 0, &argv, &argc, &request);
read_args(f_in, name, line, sizeof line, 1, &argv, &argc, &request);
orig_argv = argv;
ret = parse_arguments(&argc, (const char ***) &argv);
} else
orig_early_argv = NULL;
set_daemon_handshake_timeout(0);
/* The default is to use the user's setting unless the module sets True or False. */
if (lp_open_noatime(module_id) >= 0)
@@ -1314,20 +1210,14 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
return 0;
}
static BOOL namecvt_safe_token(const char *s);
BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)
{
char buf[1024];
int got, len;
if (*name_p) {
if (!namecvt_safe_token(*name_p)) {
rprintf(FERROR, "invalid name-converter token: %s\n", *name_p);
return False;
}
if (*name_p)
len = snprintf(buf, sizeof buf, "%s %s\n", cmd, *name_p);
} else
else
len = snprintf(buf, sizeof buf, "%s %ld\n", cmd, (long)*id_p);
if (len >= (int)sizeof buf) {
rprintf(FERROR, "namecvt_call() request was too large.\n");
@@ -1344,39 +1234,14 @@ BOOL namecvt_call(const char *cmd, const char **name_p, id_t *id_p)
if (!read_line_old(namecvt_fd_ans, buf, sizeof buf, 0))
return False;
if (*name_p) {
/* Name-to-id: an unknown name returns an empty line and atol("")=0
* would map it to root, so validate strictly below (all digits, no
* ERANGE, fits id_t). */
const char *p;
unsigned long v;
if (!*buf)
return False;
for (p = buf; *p; p++) {
if (*p < '0' || *p > '9')
return False;
}
errno = 0;
v = strtoul(buf, NULL, 10);
if (errno == ERANGE || v > (unsigned long)(id_t)-1)
return False;
*id_p = (id_t)v;
} else
if (*name_p)
*id_p = (id_t)atol(buf);
else
*name_p = strdup(buf);
return True;
}
static BOOL namecvt_safe_token(const char *s)
{
for (; *s; s++) {
unsigned char ch = (unsigned char)*s;
if (ch < ' ' || ch == 0x7f)
return False;
}
return True;
}
/* send a list of available modules to the client. Don't list those
with "list = False". */
static void send_listing(int fd)
@@ -1393,18 +1258,6 @@ static void send_listing(int fd)
io_printf(fd,"@RSYNCD: EXIT\n");
}
static int proxy_peer_allowed(int fd)
{
const char *host = undetermined_hostname;
const char *addr = client_addr(fd);
if (!allow_proxy_protocol_peer(lp_proxy_protocol_hosts(), addr, &host)) {
rprintf(FLOG, "proxy protocol rejected from untrusted peer %s (%s)\n", host, addr);
return 0;
}
return 1;
}
static int load_config(int globals_only)
{
if (!config_file) {
@@ -1442,60 +1295,16 @@ int start_daemon(int f_in, int f_out)
if (!load_config(0))
exit_cleanup(RERR_SYNTAX);
/* Bound the handshake before ANY peer input is read -- the PROXY-protocol
* header below is peer-supplied too, and was previously unbounded. An
* rsh-run daemon is not a listener and has no shared slot to exhaust. */
if (am_daemon > 0)
set_daemon_handshake_timeout(daemon_handshake_timeout(-1));
if (lp_proxy_protocol()) {
if (!proxy_peer_allowed(f_in) || !read_proxy_protocol_header(f_in))
return -1;
}
/* Do reverse DNS lookup before chroot/setuid. The result is cached,
* so the later client_name() call will use this cached value. This
* ensures hostname-based ACLs work even when DNS is unavailable
* after chroot.
*
* "reverse lookup" can be set globally OR per-module, so we also
* scan each module: a deployment with "reverse lookup = no" in the
* global section but "reverse lookup = yes" in a specific module
* still triggers a post-chroot lookup at access-check time
* (rsync_module() in this file), which would also fail in the
* chroot and turn hostname-based deny rules into silent bypasses. */
{
int need_reverse = lp_reverse_lookup(-1);
int j, num_modules = lp_num_modules();
for (j = 0; !need_reverse && j < num_modules; j++) {
if (lp_reverse_lookup(j))
need_reverse = 1;
}
if (need_reverse)
(void)client_name(client_addr(f_in));
}
if (lp_proxy_protocol() && !read_proxy_protocol_header(f_in))
return -1;
p = lp_daemon_chroot();
if (*p) {
log_init(0); /* Make use we've initialized syslog before chrooting. */
tzset();
if (chroot(p) < 0) {
rsyserr(FLOG, errno, "daemon chroot(\"%s\") failed", p);
return -1;
}
/* Deliberately do NOT set am_chrooted here. am_chrooted
* gates the per-module symlink-race defenses
* (vfs_resolve_open() and the do_*_at() wrappers in
* syscall.c) and means "the kernel is enforcing path
* confinement at the module boundary". The daemon chroot
* confines path resolution to the daemon-chroot directory,
* not to any individual module path -- modules sharing the
* daemon chroot are still distinguishable filesystem
* subtrees and a sender-controlled symlink in module A
* could redirect a syscall to module B (or to other files
* inside the daemon chroot) without the per-module
* defenses. Leave am_chrooted=0 here so vfs_resolve_open()
* still fires for "use chroot = no" modules. */
if (chdir("/") < 0) {
rsyserr(FLOG, errno, "daemon chdir(\"/\") failed");
return -1;
@@ -1538,7 +1347,6 @@ int start_daemon(int f_in, int f_out)
set_nonblocking(f_in);
}
if (exchange_protocols(f_in, f_out, line, sizeof line, 0) < 0)
return -1;
@@ -1593,73 +1401,36 @@ static void create_pid_file(void)
char pidbuf[32];
STRUCT_STAT st1, st2;
char *fail = NULL;
const char *base = pid_file;
int pdfd = -1;
if (!pid_file || !*pid_file)
return;
#ifdef O_NOFOLLOW
#define SAFE_NOFOLLOW O_NOFOLLOW
#define SAFE_OPEN_FLAGS (O_CREAT|O_NOFOLLOW)
#else
#define SAFE_NOFOLLOW 0
#endif
#ifdef AT_FDCWD
/* Pin the parent directory so the existence check, open and re-stat below
* all resolve the leaf against one stable directory inode, removing the
* lstat->open path race. The parent is operator-configured and trusted, so
* it is opened following symlinks (e.g. a /var/run -> /run); only the leaf
* is opened/checked O_NOFOLLOW (the do_*_atfd wrappers force that). */
{
const char *slash = strrchr(pid_file, '/');
char dirbuf[MAXPATHLEN];
const char *dir = ".";
if (slash) {
size_t dlen = slash == pid_file ? 1 : (size_t)(slash - pid_file);
if (dlen >= sizeof dirbuf) {
rprintf(FLOG, "pid file path is too long: %s\n", pid_file);
exit_cleanup(RERR_FILEIO);
}
memcpy(dirbuf, pid_file, dlen);
dirbuf[dlen] = '\0';
dir = dirbuf;
base = slash + 1;
}
if ((pdfd = vfs_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) {
rsyserr(FLOG, errno, "failed to open pid-file directory \"%s\"", dir);
exit_cleanup(RERR_FILEIO);
}
}
#define PID_LSTAT(stp) vfs_lstat(pdfd, base, stp, 0)
#define PID_UNLINK() vfs_unlink(pdfd, base, 0)
#define PID_OPEN() vfs_open_atfd(pdfd, base, O_RDWR|O_CREAT, 0664)
#else
#define PID_LSTAT(stp) vfs_lstat(VFS_AT_FDCWD, base, stp, VFS_ALLOW_SYMLINK)
#define PID_UNLINK() unlink(base)
#define PID_OPEN() vfs_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
#define SAFE_OPEN_FLAGS (O_CREAT)
#endif
/* These tests make sure that a temp-style lock dir is handled safely. */
st1.st_mode = 0;
if (PID_LSTAT(&st1) == 0 && !S_ISREG(st1.st_mode) && PID_UNLINK() < 0)
if (do_lstat(pid_file, &st1) == 0 && !S_ISREG(st1.st_mode) && unlink(pid_file) < 0)
fail = "unlink";
else if ((pid_file_fd = PID_OPEN()) < 0)
else if ((pid_file_fd = do_open(pid_file, O_RDWR|SAFE_OPEN_FLAGS, 0664)) < 0)
fail = S_ISREG(st1.st_mode) ? "open" : "create";
else if (!lock_range(pid_file_fd, 0, 4))
fail = "lock";
else if (vfs_fstat(pid_file_fd, &st1) < 0)
else if (do_fstat(pid_file_fd, &st1) < 0)
fail = "fstat opened";
else if (st1.st_size > (int)sizeof pidbuf)
fail = "find small";
else if (PID_LSTAT(&st2) < 0)
else if (do_lstat(pid_file, &st2) < 0)
fail = "lstat";
else if (!S_ISREG(st1.st_mode))
fail = "avoid file overwrite race for";
else if (st1.st_dev != st2.st_dev || st1.st_ino != st2.st_ino)
fail = "verify stat info for";
#ifdef HAVE_FTRUNCATE
else if (vfs_ftruncate(pid_file_fd, 0) < 0)
else if (do_ftruncate(pid_file_fd, 0) < 0)
fail = "truncate";
#endif
else {
@@ -1676,13 +1447,6 @@ static void create_pid_file(void)
cleanup_set_pid(pid); /* Mark the file for removal on exit, even if the write failed. */
}
#undef PID_LSTAT
#undef PID_UNLINK
#undef PID_OPEN
#undef SAFE_NOFOLLOW
if (pdfd >= 0)
close(pdfd);
if (fail) {
char msg[1024];
snprintf(msg, sizeof msg, "failed to %s pid file %s: %s\n",
@@ -1706,7 +1470,6 @@ static void become_daemon(void)
fprintf(stderr, "failed to fork: %s\n", strerror(errno));
exit_cleanup(RERR_FILEIO);
}
gcov_flush();
_exit(0);
}
@@ -1752,17 +1515,6 @@ int daemon_main(void)
}
set_dparams(0);
/* "proxy protocol = true" with no trusted-proxy list rejects every
* connection as an untrusted proxy peer (fail-closed). That is intended,
* but silent at startup, so warn the operator while stderr is still open. */
if (lp_proxy_protocol()
&& (!lp_proxy_protocol_hosts() || !*lp_proxy_protocol_hosts())) {
rprintf(FWARNING,
"\"proxy protocol = true\" but \"proxy protocol hosts\" is unset:"
" all connections will be rejected as untrusted proxy peers."
" Set \"proxy protocol hosts\" to your trusted proxy's address.\n");
}
if (no_detach)
create_pid_file();
else
+13 -25
View File
@@ -52,7 +52,6 @@ extern int need_messages_from_generator;
extern int delete_mode, delete_before, delete_during, delete_after;
extern int do_compression;
extern int do_compression_level;
extern int do_compression_threads;
extern int saw_stderr_opt;
extern int msgs2stderr;
extern char *shell_cmd;
@@ -132,7 +131,7 @@ static const char *client_info;
* of that protocol for it to be advertised as available. */
static void check_sub_protocol(void)
{
const char *dot;
char *dot;
int their_protocol, their_sub;
int our_sub = get_subprotocol_version();
@@ -351,7 +350,7 @@ static int parse_negotiate_str(struct name_num_obj *nno, char *tmpbuf)
continue;
ret = nni;
best = nno->saw[nni->num];
if (best == 1) /* Can't improve on our own #1 preference */
if (best == 1 || am_server) /* The server side stops at the first acceptable client choice */
break;
}
if (ret) {
@@ -415,7 +414,7 @@ static const char *getenv_nstr(int ntype)
env_str = ntype == NSTR_COMPRESS ? "zlib" : protocol_version >= 30 ? "md5" : "md4";
if (am_server && env_str) {
const char *cp = strchr(env_str, '&');
char *cp = strchr(env_str, '&');
if (cp)
env_str = cp + 1;
}
@@ -526,11 +525,8 @@ static void send_negotiate_str(int f_out, struct name_num_obj *nno, int ntype)
rprintf(FINFO, "Client %s list (on client): %s\n", nno->type, tmpbuf);
}
/* Each side sends their list of valid names to the other side and then each
* side picks its own most-preferred name that also appears in the peer's
* list. Honest peers emit their list in table (strongest-first) order via
* get_default_nno_list(), so both sides converge on the strongest mutual
* choice; a peer that front-loads a weaker name only desyncs itself. */
/* Each side sends their list of valid names to the other side and then both sides
* pick the first name in the client's list that is also in the server's list. */
if (do_negotiated_strings)
write_vstring(f_out, tmpbuf, len);
}
@@ -588,13 +584,14 @@ void setup_protocol(int f_out,int f_in)
pathname_ndx = (file_extra_cnt += PTR_EXTRA_CNT);
else
depth_ndx = ++file_extra_cnt;
/* uid_ndx/gid_ndx/acls_ndx/xattrs_ndx are assigned AFTER
* check_batch_flags() below: a batch file's stream-flags can flip
* preserve_uid/gid/acls/xattrs on, and computing the *_ndx slots
* before that leaves e.g. preserve_xattrs=1 with xattrs_ndx=0 -- so
* F_XATTR(file) (= REQ_EXTRA(file, 0)) writes at offset 0 of every
* file_struct, clobbering file->dirname. Nothing between here and
* check_batch_flags() reads file_extra_cnt or the *_ndx values. */
if (preserve_uid)
uid_ndx = ++file_extra_cnt;
if (preserve_gid)
gid_ndx = ++file_extra_cnt;
if (preserve_acls && !am_sender)
acls_ndx = ++file_extra_cnt;
if (preserve_xattrs)
xattrs_ndx = ++file_extra_cnt;
if (am_server)
set_allow_inc_recurse();
@@ -641,15 +638,6 @@ void setup_protocol(int f_out,int f_in)
if (read_batch)
check_batch_flags();
if (preserve_uid)
uid_ndx = ++file_extra_cnt;
if (preserve_gid)
gid_ndx = ++file_extra_cnt;
if (preserve_acls && !am_sender)
acls_ndx = ++file_extra_cnt;
if (preserve_xattrs)
xattrs_ndx = ++file_extra_cnt;
if (!saw_stderr_opt && protocol_version <= 28 && am_server)
msgs2stderr = 0; /* The client side may not have stderr setup for us. */
+49 -160
View File
@@ -5,7 +5,7 @@ AC_INIT([rsync],[ ],[https://rsync.samba.org/bug-tracking.html])
AC_C_BIGENDIAN
AC_HEADER_DIRENT
AC_HEADER_SYS_WAIT
AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
AC_CHECK_HEADERS(sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h \
unistd.h utime.h compat.h sys/param.h ctype.h sys/wait.h sys/stat.h \
sys/ioctl.h sys/filio.h string.h stdlib.h sys/socket.h sys/mode.h grp.h \
sys/un.h sys/attr.h arpa/inet.h arpa/nameser.h locale.h sys/types.h \
@@ -13,7 +13,7 @@ AC_CHECK_HEADERS(poll.h sys/fcntl.h sys/select.h fcntl.h sys/time.h sys/unistd.h
sys/acl.h acl/libacl.h attr/xattr.h sys/xattr.h sys/extattr.h dl.h \
popt.h popt/popt.h linux/falloc.h netinet/in_systm.h netgroup.h \
zlib.h xxhash.h openssl/md4.h openssl/md5.h zstd.h lz4.h sys/file.h \
sys/resource.h bsd/string.h)
bsd/string.h)
AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include <netinet/in.h>]])
AC_HEADER_MAJOR_FIXED
@@ -60,8 +60,6 @@ AC_PROG_AWK
AC_PROG_EGREP
AC_PROG_INSTALL
AC_PROG_MKDIR_P
AC_CHECK_TOOL([AR], [ar], [ar])
AC_PROG_RANLIB
AC_SUBST(SHELL)
AC_PATH_PROG([PERL], [perl])
AC_PATH_PROG([PYTHON3], [python3])
@@ -84,34 +82,6 @@ if test x"$enable_profile" = x"yes"; then
CFLAGS="$CFLAGS -pg"
fi
dnl Coverage build (gcov) for `make coverage`. NOTE: --enable-profile above is
dnl gprof (-pg) and is NOT coverage. -O0 keeps branch coverage meaningful;
dnl -fprofile-update=atomic keeps the shared .gcda counters correct while the
dnl suite runs many rsync processes in parallel.
AC_ARG_ENABLE(coverage,
AS_HELP_STRING([--enable-coverage],[build with gcov instrumentation for `make coverage`]))
if test x"$enable_coverage" = x"yes"; then
CFLAGS="$CFLAGS --coverage -fprofile-update=atomic -O0"
CXXFLAGS="$CXXFLAGS --coverage -fprofile-update=atomic -O0"
LDFLAGS="$LDFLAGS --coverage"
AC_DEFINE([GCOV_COVERAGE], 1,
[Flush gcov counters at exit_cleanup: rsync's children exit via _exit(), which bypasses the gcov atexit handler, so without this no .gcda is written for the receiver/generator/daemon-worker processes.])
fi
dnl openat2(RESOLVE_BENEATH) is used on Linux 5.6+ for the secure resolver.
dnl --disable-openat2 forces the portable per-component O_NOFOLLOW fallback to
dnl run as the primary resolver on ordinary Linux, so that tier is exercised
dnl (and coverage-counted) without needing a pre-5.6 kernel. Behaviour-neutral
dnl by default (the knob only REMOVES a tier when explicitly disabled).
AC_ARG_ENABLE(openat2,
AS_HELP_STRING([--disable-openat2],[do not use Linux openat2(RESOLVE_BENEATH); force the portable resolver (for exercising the fallback tier)]))
AC_ARG_ENABLE(strict-confinement,
AS_HELP_STRING([--enable-strict-confinement],[abort if a confined receiver ever does a raw path-based metadata op (a CI/dev hardening check; no effect on a normal build)]))
if test x"$enable_strict_confinement" = x"yes"; then
AC_DEFINE(STRICT_CONFINEMENT, 1, [Define to abort on a confined-regime raw path-based metadata op (CI hardening check)])
fi
AC_MSG_CHECKING([if md2man can create manpages])
if test x"$ac_cv_path_PYTHON3" = x; then
AC_MSG_RESULT(no - python3 not found)
@@ -361,28 +331,6 @@ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ ]], [[return 0;]])],
CFLAGS="$OLD_CFLAGS"
AC_SUBST(NOEXECSTACK)
dnl We need both the SYS_openat2 syscall number and <linux/openat2.h> (for
dnl struct open_how / RESOLVE_BENEATH); some setups have one without the other.
AC_CACHE_CHECK([for openat2],rsync_cv_HAVE_OPENAT2,[
AC_COMPILE_IFELSE([
AC_LANG_PROGRAM([[
#include <sys/syscall.h>
#include <linux/openat2.h>
]], [[
struct open_how how;
how.resolve = RESOLVE_BENEATH;
return SYS_openat2 + (int)how.resolve;
]])
],
[rsync_cv_HAVE_OPENAT2=yes], [rsync_cv_HAVE_OPENAT2=no])
])
if test x"$enable_openat2" != x"no"; then
if test x"$rsync_cv_HAVE_OPENAT2" = x"yes"; then
AC_DEFINE([HAVE_OPENAT2], 1,
[Define to use Linux openat2(RESOLVE_BENEATH) in vfs_resolve_open where available.])
fi
fi
# arrgh. libc in some old debian version screwed up the largefile
# stuff, getting byte range locking wrong
AC_CACHE_CHECK([for broken largefile support],rsync_cv_HAVE_BROKEN_LARGEFILE,[
@@ -440,17 +388,21 @@ AS_HELP_STRING([--disable-ipv6],[disable to omit ipv6 support]),
;;
esac ],
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
AC_RUN_IFELSE([AC_LANG_SOURCE([[ /* AF_INET6 availability check */
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
]], [[
struct sockaddr_in6 sa6;
(void)sa6;
(void)AF_INET6;
int main()
{
if (socket(AF_INET6, SOCK_STREAM, 0) < 0)
exit(1);
else
exit(0);
}
]])],
[AC_MSG_RESULT(yes)
AC_DEFINE(INET6, 1, [true if you have IPv6])],
AC_DEFINE(INET6, 1, true if you have IPv6)],
[AC_MSG_RESULT(no)],
[AC_MSG_RESULT(no)]
))
@@ -917,19 +869,6 @@ AC_HAVE_TYPE([struct stat64], [#include <stdio.h>
# if we can't find strcasecmp, look in -lresolv (for Unixware at least)
#
dnl rsync's I/O readiness loops use poll() rather than select() so that a
dnl file descriptor at or above FD_SETSIZE cannot overflow an fd_set (which
dnl is undefined behaviour and could hang the transfer). poll() is in
dnl POSIX.1-2001; fail early and clearly if this target lacks it.
dnl
dnl io.c and socket.c include <poll.h> unconditionally, so the HEADER has to
dnl be required too: a system that exposes poll() through some other header
dnl would otherwise pass configure and then fail to compile.
AC_CHECK_FUNCS([poll], , [AC_MSG_ERROR([rsync requires poll(); please report the platform to the rsync developers])])
if test x"$ac_cv_header_poll_h" != x"yes"; then
AC_MSG_ERROR([rsync requires <poll.h>; please report the platform to the rsync developers])
fi
AC_CHECK_FUNCS(strcasecmp)
if test x"$ac_cv_func_strcasecmp" = x"no"; then
AC_CHECK_LIB(resolv, strcasecmp)
@@ -947,30 +886,17 @@ dnl AC_FUNC_MEMCMP
AC_FUNC_UTIME_NULL
AC_FUNC_ALLOCA
AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo fdopendir \
getrlimit setrlimit \
AC_CHECK_FUNCS(waitpid wait4 getcwd chown chmod lchmod mknod mkfifo \
fchmod fstat ftruncate strchr readlink link utime utimes lutimes strftime \
chflags getattrlist mktime innetgr linkat mknodat mkfifoat \
chflags getattrlist mktime innetgr linkat \
memmove lchown vsnprintf snprintf vasprintf asprintf setsid strpbrk \
strlcat strlcpy stpcpy strtol mallinfo mallinfo2 getgroups setgroups geteuid getegid \
setlocale setmode open64 lseek64 mkstemp64 mtrace va_copy __va_copy \
seteuid strerror putenv iconv_open locale_charset nl_langinfo getxattr \
extattr_get_link sigaction sigprocmask setattrlist getgrouplist \
initgroups utimensat futimens posix_fallocate attropen setvbuf nanosleep usleep \
initgroups utimensat posix_fallocate attropen setvbuf nanosleep usleep \
setenv unsetenv)
dnl dirfd() is a macro or static inline on several systems (the BSDs), so the
dnl default AC_CHECK_FUNCS link probe -- which declares `char dirfd(void);` and
dnl links against a bare symbol -- gives a false negative there. Probe it with a
dnl real compile+link that includes <dirent.h> and actually calls dirfd().
AC_CACHE_CHECK([for dirfd], rsync_cv_HAVE_DIRFD,
[AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <dirent.h>]],
[[DIR *d = opendir("."); return d ? dirfd(d) < -1 : 0;]])],
[rsync_cv_HAVE_DIRFD=yes], [rsync_cv_HAVE_DIRFD=no])])
if test x"$rsync_cv_HAVE_DIRFD" = x"yes"; then
AC_DEFINE([HAVE_DIRFD], 1, [Define to 1 if you have a working dirfd() (function or macro).])
fi
dnl cygwin iconv.h defines iconv_open as libiconv_open
if test x"$ac_cv_func_iconv_open" != x"yes"; then
AC_CHECK_FUNC(libiconv_open, [ac_cv_func_iconv_open=yes; AC_DEFINE(HAVE_ICONV_OPEN, 1)])
@@ -1292,14 +1218,37 @@ if test x"$rsync_cv_HAVE_SECURE_MKSTEMP" = x"yes"; then
fi
# Whether mknod()/mknodat() can create a FIFO or a unix-domain socket is a
# property of the target filesystem, not a build-time constant -- e.g. mknod
# makes sockets on Linux but not the BSDs/macOS/Solaris, and a single transfer
# can write to filesystems with different capabilities. So rsync no longer
# probes this at configure time (a run-test that also misfired when cross-
# compiling); do_mknod*() just try mknod[at]() and, on failure, fall back to
# mkfifo[at]()/socket+bind() per call. We only need the libc symbols, checked
# above via AC_CHECK_FUNCS (mknod mknodat mkfifo mkfifoat) -- all link tests.
AC_CACHE_CHECK([if mknod creates FIFOs],rsync_cv_MKNOD_CREATES_FIFOS,[
AC_RUN_IFELSE([AC_LANG_SOURCE([[
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#if HAVE_UNISTD_H
# include <unistd.h>
#endif
int main(void) { int rc, ec; char *fn = "fifo-test";
unlink(fn); rc = mknod(fn,S_IFIFO,0600); ec = errno; unlink(fn);
if (rc) {printf("(%d %d) ",rc,ec); return ec;}
return 0;}]])],[rsync_cv_MKNOD_CREATES_FIFOS=yes],[rsync_cv_MKNOD_CREATES_FIFOS=no],[rsync_cv_MKNOD_CREATES_FIFOS=cross])])
if test x"$rsync_cv_MKNOD_CREATES_FIFOS" = x"yes"; then
AC_DEFINE(MKNOD_CREATES_FIFOS, 1, [Define to 1 if mknod() can create FIFOs.])
fi
AC_CACHE_CHECK([if mknod creates sockets],rsync_cv_MKNOD_CREATES_SOCKETS,[
AC_RUN_IFELSE([AC_LANG_SOURCE([[
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
#if HAVE_UNISTD_H
# include <unistd.h>
#endif
int main(void) { int rc, ec; char *fn = "sock-test";
unlink(fn); rc = mknod(fn,S_IFSOCK,0600); ec = errno; unlink(fn);
if (rc) {printf("(%d %d) ",rc,ec); return ec;}
return 0;}]])],[rsync_cv_MKNOD_CREATES_SOCKETS=yes],[rsync_cv_MKNOD_CREATES_SOCKETS=no],[rsync_cv_MKNOD_CREATES_SOCKETS=cross])])
if test x"$rsync_cv_MKNOD_CREATES_SOCKETS" = x"yes"; then
AC_DEFINE(MKNOD_CREATES_SOCKETS, 1, [Define to 1 if mknod() can create sockets.])
fi
#
# The following test was mostly taken from the tcl/tk plus patches
@@ -1443,7 +1392,7 @@ else
AC_DEFINE(HAVE_LINUX_XATTRS, 1, [True if you have Linux xattrs (or equivalent)])
AC_DEFINE(SUPPORT_XATTRS, 1)
AC_DEFINE(NO_SYMLINK_USER_XATTRS, 1, [True if symlinks do not support user xattrs])
AC_SEARCH_LIBS(getxattr,attr)
AC_CHECK_LIB(attr,getxattr)
;;
darwin*)
AC_MSG_RESULT(Using OS X xattrs)
@@ -1473,66 +1422,6 @@ else
esac
fi
#################################################
# On Linux, POSIX ACLs are stored as the "system.posix_acl_{access,default}"
# xattrs, so we can get/set them through a held O_NOFOLLOW fd (fsetxattr) or a
# dirfd+leaf (setxattrat, AT_SYMLINK_NOFOLLOW) instead of the path-based libacl
# acl_*_file() calls -- making the operation safe against a parent-symlink race.
# This needs POSIX ACLs and the f/at xattr syscalls, which on Linux are
# available whenever <sys/xattr.h> (or <attr/xattr.h>) is -- independent of the
# -X feature (--disable-xattr-support), so we gate on the header, not
# enable_xattr_support.
AH_TEMPLATE([SUPPORT_ACL_FD],
[Define to 1 to do POSIX ACL ops via fd/at xattr syscalls (lib/acl.c)])
AH_TEMPLATE([HAVE_XATTRAT_SYSCALLS],
[Define to 1 if the setxattrat/getxattrat/removexattrat syscalls are available])
if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes" \
&& { test x"$ac_cv_header_sys_xattr_h" = x"yes" || test x"$ac_cv_header_attr_xattr_h" = x"yes"; }; then
case "$host_os" in
*linux*)
AC_DEFINE(SUPPORT_ACL_FD, 1)
AC_CACHE_CHECK([for SYS_setxattrat],rsync_cv_have_sys_setxattrat,[
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[#include <sys/syscall.h>
#include <stdint.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
struct xattr_args { uint64_t value; uint32_t size; uint32_t flags; };]],
[[struct xattr_args a; a.value = 0; a.size = 0; a.flags = 0;
syscall(SYS_setxattrat, 0, ".", 0, "n", &a, sizeof a);
syscall(SYS_getxattrat, 0, ".", 0, "n", &a, sizeof a);
syscall(SYS_removexattrat, 0, ".", 0, "n");]])],[rsync_cv_have_sys_setxattrat=yes],[rsync_cv_have_sys_setxattrat=no])])
if test x"$rsync_cv_have_sys_setxattrat" = x"yes"; then
AC_DEFINE(HAVE_XATTRAT_SYSCALLS, 1)
fi
;;
esac
fi
#################################################
# Detect a patched libacl providing the race-safe
# *_at ACL entry points (acl_get_file_at/acl_set_file_at/acl_delete_def_file_at,
# ACL_1.3, unreleased upstream). When present we route the race-safe ACL get/
# set/delete through them on Linux -- race-safe on every kernel (6.13+ uses
# *xattrat; older uses libacl's /proc/self/fd compat). A stock -lacl lacks these
# symbols, so this stays undefined and the build falls back to lib/acl.c;
# detection must therefore run against the patched lib (CPPFLAGS/LDFLAGS).
AH_TEMPLATE([HAVE_LIBACL_AT],
[Define to 1 if libacl provides acl_get_file_at/acl_set_file_at/acl_delete_def_file_at])
if test x"$samba_cv_HAVE_POSIX_ACLS" = x"yes"; then
case "$host_os" in
*linux*)
AC_CHECK_LIB(acl, acl_get_file_at, [rsync_have_libacl_at=yes], [rsync_have_libacl_at=no])
if test x"$rsync_have_libacl_at" = x"yes"; then
AC_CHECK_FUNCS([acl_set_file_at acl_delete_def_file_at], [], [rsync_have_libacl_at=no])
fi
if test x"$rsync_have_libacl_at" = x"yes"; then
AC_DEFINE(HAVE_LIBACL_AT, 1)
fi
;;
esac
fi
if test x"$enable_acl_support" = x"no" || test x"$enable_xattr_support" = x"no" || test x"$enable_iconv" = x"no"; then
AC_MSG_CHECKING([whether $CC supports -Wno-unused-parameter])
OLD_CFLAGS="$CFLAGS"
@@ -1550,7 +1439,7 @@ case "$CC" in
;;
esac
AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy vfs/dummy shconfig])
AC_CONFIG_FILES([Makefile lib/dummy zlib/dummy popt/dummy shconfig])
AC_OUTPUT
AC_MSG_RESULT()
+1 -3
View File
@@ -30,9 +30,7 @@ int claim_connection(char *fname, int max_connections)
if (max_connections == 0)
return 1;
/* 'lock file = PATH': refuse symlinks not owned by uid 0 or our euid so
* a planted parent can't redirect the root daemon's O_CREAT open. */
if ((fd = vfs_open_owner_walk(fname, O_RDWR|O_CREAT, 0600, 0)) < 0)
if ((fd = open(fname, O_RDWR|O_CREAT, 0600)) < 0)
return 0;
/* Find a free spot. */
+2 -9
View File
@@ -6,7 +6,7 @@
BEGIN {
heading = "/* DO NOT EDIT THIS FILE! It is auto-generated from a list of values in " ARGV[1] "! */\n\n"
sect = psect = defines = accessors = prior_ptype = ""
parms = "\nstatic const struct parm_struct parm_table[] = {"
parms = "\nstatic struct parm_struct parm_table[] = {"
comment_fmt = "\n/********** %s **********/\n"
tdstruct = "typedef struct {"
}
@@ -84,14 +84,7 @@ BEGIN {
defines = defines "\t" vtype " " name ";\n"
values = values "\t" $0 ", /* " name " */\n"
parms = parms " {\"" pubname "\", P_" ptype psect name ", " enum ", 0},\n"
# The shell-executed hook params (whose %RSYNC_*% expansion is fed to
# /bin/sh) use the _SHELL accessor, which single-quotes peer-controlled
# values to prevent injection. Ordinary string params must NOT quote --
# it would corrupt a documented `path = /home/%RSYNC_USER_NAME%` etc.
if (atype == "STRING" && (name == "early_exec" || name == "prexfer_exec" || name == "postxfer_exec" || name == "name_converter"))
accessors = accessors "FN_" sect "_STRING_SHELL(lp_" name ", " name ")\n"
else
accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n"
accessors = accessors "FN_" sect "_" atype "(lp_" name ", " name ")\n"
if (vtype == "char*") {
exps = exps "\tBOOL " name "_EXP;\n"
-3
View File
@@ -6,7 +6,6 @@ STRING daemon_gid NULL
STRING daemon_uid NULL
STRING motd_file NULL
STRING pid_file NULL
STRING proxy_protocol_hosts NULL
STRING socket_options NULL
INTEGER listen_backlog 5
@@ -16,7 +15,6 @@ BOOL proxy_protocol False
Locals: =================================================================
STRING auth_digest NULL
STRING auth_users NULL
STRING charset NULL
STRING comment NULL
@@ -57,7 +55,6 @@ BOOL fake_super False
BOOL forward_lookup True
BOOL ignore_errors False
BOOL ignore_nonreadable False
BOOL insecure_links False
BOOL list True
BOOL read_only True
BOOL reverse_lookup True
+5 -75
View File
@@ -34,54 +34,6 @@ int ignore_perishable = 0;
int non_perishable_cnt = 0;
int skipped_deletes = 0;
/* Held fd of the directory whose contents delete_dir_contents() is currently
* removing, so delete_item()'s per-entry rmdir/unlink/chmod go through it
* instead of re-resolving the full path for every entry. Set (with save/
* restore across the recursion) around the delete loop; -1 outside a recursive
* delete or when the secure resolver is gated off (chroot / non-receiver) or
* the path doesn't live directly in that dir. */
static int del_dirfd = -1;
static const char *del_dir_prefix;
static int del_dir_prefix_len;
/* If `path` is a single component directly inside the dir being deleted,
* point *leaf at its basename and return the held dir fd; else return -1. */
static int del_held_dfd(const char *path, const char **leaf)
{
if (del_dirfd >= 0
&& strncmp(path, del_dir_prefix, del_dir_prefix_len) == 0
&& path[del_dir_prefix_len] == '/'
&& strchr(path + del_dir_prefix_len + 1, '/') == NULL) {
*leaf = path + del_dir_prefix_len + 1;
return del_dirfd;
}
return -1;
}
static void del_chmod(const char *fbuf, mode_t mode)
{
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
if (dfd >= 0)
vfs_chmod(dfd, leaf, mode, 0);
else
vfs_chmod(VFS_AT_FDCWD, fbuf, mode, 0);
}
/* vfs_flags carries VFS_OPERATOR_PATH for a backup-tree delete (DEL_FOR_BACKUP):
* the path-based fallback then resolves the leaf's parent via the ownership walk,
* matching the confinement the base gives this unlink under make_backup() (where
* the held dirfd is absent for a cross-tree --backup-dir leaf). A held-dirfd
* delete is already confined, so it ignores the flag. */
static int del_unlink(const char *fbuf, int vfs_flags)
{
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
if (dfd >= 0 && vfs_unlink(dfd, leaf, 0) == 0)
return 0;
return robust_unlink(fbuf, vfs_flags); /* fall back (ETXTBSY retry, or not held) */
}
static inline int is_backup_file(char *fn)
{
int k = strlen(fn) - backup_suffix_len;
@@ -131,18 +83,6 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
flags = (flags & ~(DEL_RECURSE|DEL_MAKE_ROOM|DEL_NO_UID_WRITE))
| DEL_DIR_IS_EMPTY;
/* Hold this dir open so the per-entry chmod/rmdir/unlink below (and in
* delete_item) become *at() calls against it rather than re-resolving the
* full path for every entry. Save/restore around the recursion. */
int save_del_dirfd = del_dirfd;
const char *save_del_prefix = del_dir_prefix;
int save_del_prefix_len = del_dir_prefix_len;
fname[dlen] = '\0';
del_dirfd = vfs_opendir(fname);
fname[dlen] = '/';
del_dir_prefix = fname;
del_dir_prefix_len = dlen;
for (j = dirlist->used; j--; ) {
struct file_struct *fp = dirlist->files[j];
@@ -158,7 +98,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
strlcpy(p, fp->basename, remainder);
if (!(fp->mode & S_IWUSR) && !am_root && fp->flags & FLAG_OWNED_BY_US)
del_chmod(fname, fp->mode | S_IWUSR);
do_chmod(fname, fp->mode | S_IWUSR);
/* Save stack by recursing to ourself directly. */
if (S_ISDIR(fp->mode)) {
if (delete_dir_contents(fname, flags | DEL_RECURSE) != DR_SUCCESS)
@@ -168,12 +108,6 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
ret = DR_NOT_EMPTY;
}
if (del_dirfd >= 0)
close(del_dirfd);
del_dirfd = save_del_dirfd;
del_dir_prefix = save_del_prefix;
del_dir_prefix_len = save_del_prefix_len;
fname[dlen] = '\0';
done:
@@ -205,7 +139,7 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
}
if (flags & DEL_NO_UID_WRITE)
del_chmod(fbuf, mode | S_IWUSR);
do_chmod(fbuf, mode | S_IWUSR);
if (S_ISDIR(mode) && !(flags & DEL_DIR_IS_EMPTY)) {
/* This only happens on the first call to delete_item() since
@@ -225,23 +159,19 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
}
if (S_ISDIR(mode)) {
const char *leaf;
int dfd = del_held_dfd(fbuf, &leaf);
what = "rmdir";
ok = (dfd >= 0 ? vfs_unlink(dfd, leaf, VFS_REMOVEDIR)
: vfs_unlink(VFS_AT_FDCWD, fbuf,
VFS_REMOVEDIR | ((flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0))) == 0;
ok = do_rmdir(fbuf) == 0;
} else {
if (make_backups > 0 && !(flags & DEL_FOR_BACKUP) && (backup_dir || !is_backup_file(fbuf))) {
what = "make_backup";
ok = make_backup(fbuf, True);
if (ok == 2) {
what = "unlink";
ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0;
ok = robust_unlink(fbuf) == 0;
}
} else {
what = "unlink";
ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0;
ok = robust_unlink(fbuf) == 0;
}
}
+20
View File
@@ -0,0 +1,20 @@
Handling the rsync SGML documentation
rsync documentation is now primarily in Docbook format. Docbook is an
SGML/XML documentation format that is becoming standard on free
operating systems. It's also used for Samba documentation.
The SGML files are source code that can be translated into various
useful output formats, primarily PDF, HTML, Postscript and plain text.
To do this transformation on Debian, you should install the
docbook-utils package. Having done that, you can say
docbook2pdf rsync.sgml
and so on.
On other systems you probably need James Clark's "sp" and "JadeTeX"
packages. Work it out for yourself and send a note to the mailing
list.
+42
View File
@@ -0,0 +1,42 @@
Notes on rsync profiling
strlcpy is hot:
0.00 0.00 1/7735635 push_dir [68]
0.00 0.00 1/7735635 pop_dir [71]
0.00 0.00 1/7735635 send_file_list [15]
0.01 0.00 18857/7735635 send_files [4]
0.04 0.00 129260/7735635 send_file_entry [18]
0.04 0.00 129260/7735635 make_file [20]
0.04 0.00 141666/7735635 send_directory <cycle 1> [36]
2.29 0.00 7316589/7735635 f_name [13]
[14] 11.7 2.42 0.00 7735635 strlcpy [14]
Here's the top few functions:
46.23 9.57 9.57 13160929 0.00 0.00 mdfour64
14.78 12.63 3.06 13160929 0.00 0.00 copy64
11.69 15.05 2.42 7735635 0.00 0.00 strlcpy
10.05 17.13 2.08 41438 0.05 0.38 sum_update
4.11 17.98 0.85 13159996 0.00 0.00 mdfour_update
1.50 18.29 0.31 file_compare
1.45 18.59 0.30 129261 0.00 0.01 send_file_entry
1.23 18.84 0.26 2557585 0.00 0.00 f_name
1.11 19.07 0.23 1483750 0.00 0.00 u_strcmp
1.11 19.30 0.23 118129 0.00 0.00 writefd_unbuffered
0.92 19.50 0.19 1085011 0.00 0.00 writefd
0.43 19.59 0.09 156987 0.00 0.00 read_timeout
0.43 19.68 0.09 129261 0.00 0.00 clean_fname
0.39 19.75 0.08 32887 0.00 0.38 matched
0.34 19.82 0.07 1 70.00 16293.92 send_files
0.29 19.89 0.06 129260 0.00 0.00 make_file
0.29 19.95 0.06 75430 0.00 0.00 read_unbuffered
mdfour could perhaps be made faster:
/* NOTE: This code makes no attempt to be fast! */
There might be an optimized version somewhere that we can borrow.
+351
View File
@@ -0,0 +1,351 @@
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook V4.1//EN">
<book id="rsync">
<bookinfo>
<title>rsync</title>
<copyright>
<year>1996 -- 2002</year>
<holder>Martin Pool</holder>
<holder>Andrew Tridgell</holder>
</copyright>
<author>
<firstname>Martin</firstname>
<surname>Pool</surname>
</author>
</bookinfo>
<chapter>
<title>Introduction</title>
<para>rsync is a flexible program for efficiently copying files or
directory trees.
<para>rsync has many options to select which files will be copied
and how they are to be transferred. It may be used as an
alternative to ftp, http, scp or rcp.
<para>The rsync remote-update protocol allows rsync to transfer just
the differences between two sets of files across the network link,
using an efficient checksum-search algorithm described in the
technical report that accompanies this package.</para>
<para>Some of the additional features of rsync are:</para>
<itemizedlist>
<listitem>
<para>support for copying links, devices, owners, groups and
permissions
</para>
</listitem>
<listitem>
<para>
exclude and exclude-from options similar to GNU tar
</para>
</listitem>
<listitem>
<para>
a CVS exclude mode for ignoring the same files that CVS would ignore
</listitem>
<listitem>
<para>
can use any transparent remote shell, including rsh or ssh
</listitem>
<listitem>
<para>
does not require root privileges
</listitem>
<listitem>
<para>
pipelining of file transfers to minimize latency costs
</listitem>
<listitem>
<para>
support for anonymous or authenticated rsync servers (ideal for
mirroring)
</para>
</listitem>
</itemizedlist>
</chapter>
<chapter>
<title>Using rsync</title>
<section>
<title>
Introductory example
</title>
<para>
Probably the most common case of rsync usage is to copy files
to or from a remote machine using
<application>ssh</application> as a network transport. In
this situation rsync is a good alternative to
<application>scp</application>.
</para>
<para>
The most commonly used arguments for rsync are
</para>
<variablelist>
<varlistentry>
<term><option>-v</option></term>
<listitem>
<para>Be verbose. Primarily, display the name of each file as it is copied.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-a</option></term>
<listitem>
<para>
Reproduce the structure and attributes of the origin files as exactly
as possible: this includes copying subdirectories, symlinks, special
files, ownership and permissions. (@xref{Attributes to
copy}.)
</para>
</listitem>
</varlistentry>
</variablelist>
<para><option>-v </option>
<para><option>-z</option>
Compress network traffic, using a modified version of the
@command{zlib} library.</para>
<para><option>-P</option>
Display a progress indicator while files are transferred. This should
normally be omitted if rsync is not run on a terminal.
</para>
</section>
<section>
<title>Local and remote</title>
<para>There are six different ways of using rsync. They
are:</para>
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM REFENTRY SECTION) -->
<orderedlist>
<listitem>
<para>
for copying local files. This is invoked when neither
source nor destination path contains a @code{:} separator
<listitem>
<para>
for copying from the local machine to a remote machine using
a remote shell program as the transport (such as rsh or
ssh). This is invoked when the destination path contains a
single @code{:} separator.
<listitem>
<para>
for copying from a remote machine to the local machine
using a remote shell program. This is invoked when the source
contains a @code{:} separator.
<listitem>
<para>
for copying from a remote rsync server to the local
machine. This is invoked when the source path contains a @code{::}
separator or a @code{rsync://} URL.
<listitem>
<para>
for copying from the local machine to a remote rsync
server. This is invoked when the destination path contains a @code{::}
separator.
<listitem>
<para>
for listing files on a remote machine. This is done the
same way as rsync transfers except that you leave off the
local destination.
</listitem>
</orderedlist>
<para>
Note that in all cases (other than listing) at least one of the source
and destination paths must be local.
<para>
Any one invocation of rsync makes a copy in a single direction. rsync
currently has no equivalent of @command{ftp}'s interactive mode.
@cindex @sc{nfs}
@cindex network filesystems
@cindex remote filesystems
<para>
rsync's network protocol is generally faster at copying files than
network filesystems such as @sc{nfs} or @sc{cifs}. It is better to
run rsync on the file server either as a daemon or over ssh than
running rsync giving the network directory.
</para>
</section>
</chapter>
<chapter>
<title>Frequently asked questions</title>
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE MSGSET PROCEDURE SIDEBAR QANDASET ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS ABSTRACT AUTHORBLURB EPIGRAPH INDEXTERM SECTION SIMPLESECT REFENTRY SECT1) -->
<qandaset>
<!-- one of (QANDADIV QANDAENTRY) -->
<qandaentry>
<question>
<!-- one of (CALLOUTLIST GLOSSLIST ITEMIZEDLIST ORDEREDLIST
SEGMENTEDLIST SIMPLELIST VARIABLELIST CAUTION IMPORTANT NOTE
TIP WARNING LITERALLAYOUT PROGRAMLISTING PROGRAMLISTINGCO
SCREEN SCREENCO SCREENSHOT SYNOPSIS CMDSYNOPSIS FUNCSYNOPSIS
CLASSSYNOPSIS FIELDSYNOPSIS CONSTRUCTORSYNOPSIS
DESTRUCTORSYNOPSIS METHODSYNOPSIS FORMALPARA PARA SIMPARA
ADDRESS BLOCKQUOTE GRAPHIC GRAPHICCO MEDIAOBJECT
MEDIAOBJECTCO INFORMALEQUATION INFORMALEXAMPLE
INFORMALFIGURE INFORMALTABLE EQUATION EXAMPLE FIGURE TABLE
PROCEDURE ANCHOR BRIDGEHEAD REMARK HIGHLIGHTS INDEXTERM) -->
<para>Are there mailing lists for rsync?
</question>
<answer>
<para>Yes, and you can subscribe and unsubscribe through a
web interface at
<ulink
url="http://lists.samba.org/">http://lists.samba.org/</ulink>
</para>
<para>
If you are having trouble with the mailing list, please
send mail to the administrator
<email>rsync-admin@lists.samba.org</email>
not to the list itself.
</para>
<para>
The mailing list archives are searchable. Use
<ulink url="http://google.com/">Google</ulink> and prepend
the search with <userinput>site:lists.samba.org
rsync</userinput>, plus relevant keywords.
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>
Why is rsync so much bigger when I build it with
<command>gcc</command>?
</para>
</question>
<answer>
<para>
On gcc, rsync builds by default with debug symbols
included. If you strip both executables, they should end
up about the same size. (Use <command>make
install-strip</command>.)
</para>
</answer>
</qandaentry>
<qandaentry>
<question>
<para>Is rsync useful for a single large file like an ISO image?</para>
</question>
<answer>
<para>
Yes, but note the following:
<para>
Background: A common use of rsync is to update a file (or set of files) in one location from a more
correct or up-to-date copy in another location, taking advantage of portions of the files that are
identical to speed up the process. (Note that rsync will transfer a file in its entirety if no copy
exists at the destination.)
<para>
(This discussion is written in terms of updating a local copy of a file from a correct file in a
remote location, although rsync can work in either direction.)
<para>
The file to be updated (the local file) must be in a destination directory that has enough space for
two copies of the file. (In addition, keep an extra copy of the file to be updated in a different
location for safety -- see the discussion (below) about rsync's behavior when the rsync process is
interrupted before completion.)
<para>
The local file must have the same name as the remote file being sync'd to (I think?). If you are
trying to upgrade an iso from, for example, beta1 to beta2, rename the local file to the same name
as the beta2 file. *(This is a useful thing to do -- only the changed portions will be
transmitted.)*
<para>
The extra copy of the local file kept in a different location is because of rsync's behavior if
interrupted before completion:
<para>
* If you specify the --partial option and rsync is interrupted, rsync will save the partially
rsync'd file and throw away the original local copy. (The partially rsync'd file is correct but
truncated.) If rsync is restarted, it will not have a local copy of the file to check for duplicate
blocks beyond the section of the file that has already been rsync'd, thus the remainder of the rsync
process will be a "pure transfer" of the file rather than taking advantage of the rsync algorithm.
<para>
* If you don't specify the --partial option and rsync is interrupted, rsync will throw away the
partially rsync'd file, and, when rsync is restarted starts the rsync process over from the
beginning.
<para>
Which of these is most desirable depends on the degree of commonality between the local and remote
copies of the file *and how much progress was made before the interruption*.
<para>
The ideal approach after an interruption would be to create a new file by taking the original file
and deleting a portion equal in size to the portion already rsync'd and then appending *the
remaining* portion to the portion of the file that has already been rsync'd. (There has been some
discussion about creating an option to do this automatically.)
The --compare-dest option is useful when transferring multiple files, but is of no benefit in
transferring a single file. (AFAIK)
*Other potentially useful information can be found at:
-[3]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFile
This answer, formatted with "real" bullets, can be found at:
-[4]http://twiki.org/cgi-bin/view/Wikilearn/RsyncingALargeFileFAQ*
</para>
</answer>
</qandaentry>
</qandaset>
</chapter>
<appendix>
<title>Other Resources</title>
<para><ulink url="http://www.ccp14.ac.uk/ccp14admin/rsync/"></ulink></para>
</appendix>
</book>
+59 -349
View File
@@ -42,99 +42,8 @@ extern int protocol_version;
extern int trust_sender_args;
extern int module_id;
/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */
int daemon_config_filter_file = 0;
/* Where the rule text now being parsed came from, when that is a file's
* CONTENTS rather than an argument. A rule that fails to parse used to be
* echoed back verbatim, and the peer chooses which file gets merged (a
* per-directory merge rule travels over the protocol, so no argument of ours
* ever names it), which made the filter parser a read-any-line oracle: any
* line that is not valid filter syntax came straight back in the error.
* Report where the bad rule is, not what it says. */
static int rule_src_in_file = 0; /* parsing a file's contents right now */
static const char *rule_src_file = NULL; /* ...and its name is safe to show */
static int rule_src_line = 0;
/* Where a file whose own name we must NOT print was named, which is a location
* we CAN print: it keeps the diagnostic useful without echoing the pathname a
* merge rule supplied. */
static const char *rule_src_named_at = NULL;
/* True while the text we are handling came out of a file's contents: either we
* are parsing that file right now, or this is a deferred per-dir merge whose
* NAME came from one and which carries the provenance on the rule. */
#define TEXT_FROM_FILE(template) \
(rule_src_in_file \
|| ((template) && (template)->rflags & FILTRULE_FROM_FILE))
/* "FILE line N", or just "FILE" when the count is not a line count. */
static const char *rule_src_where(void)
{
static char buf[MAXPATHLEN + 32];
if (!rule_src_file) {
if (!rule_src_named_at)
return "a file read earlier"; /* origin not retained */
snprintf(buf, sizeof buf, "a file named at %s", rule_src_named_at);
return buf;
}
if (rule_src_line < 0)
return rule_src_file;
snprintf(buf, sizeof buf, "%s line %d", rule_src_file, rule_src_line);
return buf;
}
/* THE chokepoint. Every diagnostic string that is, or is built from, a filter
* rule's own text -- a pattern, a merge-file name, a path composed from one --
* must be passed through rule_text() on its way to rprintf(). When the rule
* came from an argument the text is returned unchanged, because it is the
* user's own and hiding it only makes typos harder to fix. When it came from
* a FILE's contents it is replaced by a description of where it came from,
* because the peer chooses which file gets merged and any line of it that
* reaches a message is a line the peer can read back.
*
* Doing it here rather than at each site is the point: a message added later
* cannot reintroduce the leak by forgetting to check, and there is one place
* to audit. `template' is the rule the text belongs to, or NULL when the only
* thing that matters is whether we are parsing a file right now.
*
* The returned buffer is rotated, so two calls in one rprintf() are safe. */
static const char *rule_text_len(const filter_rule *template,
const char *text, int len)
{
static char buf[2][BIGPATHBUFLEN];
static int which = 0;
char *b = buf[which];
which ^= 1;
if (!TEXT_FROM_FILE(template)) {
if (len < 0)
return text;
snprintf(b, sizeof buf[0], "%.*s", len, text);
return b;
}
snprintf(b, sizeof buf[0], "<rule from %s>", rule_src_where());
return b;
}
static const char *rule_text(const filter_rule *template, const char *text)
{
return rule_text_len(template, text, -1);
}
/* For the extra detail some messages add ABOUT the text -- a character of it,
* an offset into it. Dropped along with the text it describes. */
static const char *rule_detail(const filter_rule *template, const char *detail)
{
return TEXT_FROM_FILE(template) ? "" : detail;
}
static void filter_rule_err(const char *msg, const char *rulestr)
{
rprintf(FERROR, "%s: %s\n", msg, rule_text(NULL, rulestr));
exit_cleanup(RERR_SYNTAX);
}
extern char curr_dir[MAXPATHLEN];
extern unsigned int curr_dir_len;
extern unsigned int module_dirlen;
filter_rule_list filter_list = { .debug_type = "" };
@@ -152,7 +61,7 @@ int trust_sender_filter = 0;
#define SLASH_WILD3_SUFFIX "/***"
/* The dirbuf is set by push_local_filters() to the current subdirectory
* relative to vfs.curr_dir that is being processed. The path always has a
* relative to curr_dir that is being processed. The path always has a
* trailing slash appended, and the variable dirbuf_len contains the length
* of this path prefix. The path is always absolute. */
static char dirbuf[MAXPATHLEN+1];
@@ -162,9 +71,6 @@ static int dirbuf_depth;
/* This is True when we're scanning parent dirs for per-dir merge-files. */
static BOOL parent_dirscan = False;
#define MAX_MERGE_DEPTH 32
static int merge_depth = 0;
/* This array contains a list of all the currently active per-dir merge
* files. This makes it easier to save the appropriate values when we
* "push" down into each subdirectory. */
@@ -265,10 +171,10 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_
else
mention_rule_suffix = DEBUG_GTE(FILTER, 2) ? "" : NULL;
if (mention_rule_suffix) {
rprintf(FINFO, "[%s] add_rule(%s%s)%s%s\n",
who_am_i(), rule_detail(rule, get_rule_prefix(rule, pat, 0, NULL)),
rule_text_len(rule, pat, (int)pat_len),
listp->debug_type, rule_detail(rule, mention_rule_suffix));
rprintf(FINFO, "[%s] add_rule(%s%.*s%s)%s%s\n",
who_am_i(), get_rule_prefix(rule, pat, 0, NULL),
(int)pat_len, pat, (rule->rflags & FILTRULE_DIRECTORY) ? "/" : "",
listp->debug_type, mention_rule_suffix);
}
/* These flags also indicate that we're reading a list that
@@ -373,7 +279,7 @@ static void add_rule(filter_rule_list *listp, const char *pat, unsigned int pat_
}
lp = new_array0(filter_rule_list, 1);
if (asprintf(&lp->debug_type, " [per-dir %s]", rule_text(rule, cp)) < 0)
if (asprintf(&lp->debug_type, " [per-dir %s]", cp) < 0)
out_of_memory("add_rule");
rule->u.mergelist = lp;
@@ -521,7 +427,7 @@ void add_implied_include(const char *arg, int skip_daemon_module)
if (cp[1] == ']') {
if (!saw_wild)
cp++; /* A \] in a non-wild filter causes a problem, so drop the \ . */
} else if (!cp[1] || !strchr("*[?", cp[1])) {
} else if (!strchr("*[?", cp[1])) {
backslash_cnt++;
if (saw_wild)
*p++ = '\\';
@@ -690,8 +596,7 @@ static void pop_filter_list(filter_rule_list *listp)
* value and will be updated with the length of the resulting name. We
* always return a name that is null terminated, even if the merge_file
* name was not. */
static char *parse_merge_name(const filter_rule *template,
const char *merge_file, unsigned int *len_ptr,
static char *parse_merge_name(const char *merge_file, unsigned int *len_ptr,
unsigned int prefix_skip)
{
static char buf[MAXPATHLEN];
@@ -722,7 +627,7 @@ static char *parse_merge_name(const filter_rule *template,
}
if (!sanitize_path(fn, merge_file, r, dirbuf_depth, SP_DEFAULT)) {
rprintf(FERROR, "merge-file name overflows: %s\n",
rule_text(template, merge_file));
merge_file);
return NULL;
}
fn_len = strlen(fn);
@@ -735,8 +640,7 @@ static char *parse_merge_name(const filter_rule *template,
if (fn != buf) {
int d_len = dirbuf_len - prefix_skip;
if (d_len + fn_len >= MAXPATHLEN) {
rprintf(FERROR, "merge-file name overflows: %s\n",
rule_text(template, fn));
rprintf(FERROR, "merge-file name overflows: %s\n", fn);
return NULL;
}
memcpy(buf, dirbuf + prefix_skip, d_len);
@@ -754,9 +658,9 @@ void set_filter_dir(const char *dir, unsigned int dirlen)
{
unsigned int len;
if (*dir != '/') {
memcpy(dirbuf, vfs.curr_dir, vfs.curr_dir_len);
dirbuf[vfs.curr_dir_len] = '/';
len = vfs.curr_dir_len + 1;
memcpy(dirbuf, curr_dir, curr_dir_len);
dirbuf[curr_dir_len] = '/';
len = curr_dir_len + 1;
if (len + dirlen >= MAXPATHLEN)
dirlen = 0;
} else
@@ -786,7 +690,7 @@ static BOOL setup_merge_file(int mergelist_num, filter_rule *ex,
char *x, *y, *pat = ex->pattern;
unsigned int len;
if (!(x = parse_merge_name(ex, pat, NULL, 0)) || *x != '/')
if (!(x = parse_merge_name(pat, NULL, 0)) || *x != '/')
return 0;
if (DEBUG_GTE(FILTER, 2)) {
@@ -850,7 +754,7 @@ struct local_filter_state {
/* Each time rsync changes to a new directory it call this function to
* handle all the per-dir merge-files. The "dir" value is the current path
* relative to vfs.curr_dir (which might not be null-terminated). We copy it
* relative to curr_dir (which might not be null-terminated). We copy it
* into dirbuf so that we can easily append a file name on the end. */
void *push_local_filters(const char *dir, unsigned int dirlen)
{
@@ -912,7 +816,7 @@ void *push_local_filters(const char *dir, unsigned int dirlen)
io_error |= IOERR_GENERAL;
rprintf(FERROR,
"cannot add local filter rules in long-named directory: %s\n",
rule_text(ex, full_fname(dirbuf)));
full_fname(dirbuf));
}
dirbuf[dirbuf_len] = '\0';
}
@@ -1000,7 +904,7 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags)
{
int slash_handling, str_cnt = 0, anchored_match = 0;
int ret_match = ex->rflags & FILTRULE_NEGATE ? 0 : 1;
const char *p, *pattern = ex->pattern;
char *p, *pattern = ex->pattern;
const char *strings[16]; /* more than enough */
const char *name = fname + (*fname == '/');
@@ -1017,10 +921,10 @@ static int rule_matches(const char *fname, filter_rule *ex, int name_flags)
if ((p = strrchr(name,'/')) != NULL)
name = p+1;
} else if (ex->rflags & FILTRULE_ABS_PATH && *fname != '/'
&& vfs.curr_dir_len > module_dirlen + 1) {
&& curr_dir_len > module_dirlen + 1) {
/* If we're matching against an absolute-path pattern,
* we need to prepend our full path info. */
strings[str_cnt++] = vfs.curr_dir + module_dirlen + 1;
strings[str_cnt++] = curr_dir + module_dirlen + 1;
strings[str_cnt++] = "/";
} else if (ex->rflags & FILTRULE_WILD2_PREFIX && *fname != '/') {
/* Allow "**"+"/" to match at the start of the string. */
@@ -1095,8 +999,8 @@ static void report_filter_result(enum logcode code, char const *name,
: "file";
rprintf(code, "[%s] %sing %s %s because of pattern %s%s%s\n",
w, actions[*w=='g'][!(ent->rflags & FILTRULE_INCLUDE)],
t, name, rule_text(ent, ent->pattern),
rule_detail(ent, ent->rflags & FILTRULE_DIRECTORY ? "/" : ""), type);
t, name, ent->pattern,
ent->rflags & FILTRULE_DIRECTORY ? "/" : "", type);
}
}
@@ -1129,56 +1033,6 @@ int check_server_filter(filter_rule_list *listp, enum logcode code, const char *
return ret;
}
/* Returns 1 if `name` matches an implied-parent rule (a directory component
* seeded by add_implied_include() with FILTRULE_DIRECTORY) but not a leaf
* rule -- i.e. the client asked for something under the dir, never the dir
* itself as content.
*
* The receiver uses this to refuse a malicious sender that sets XMIT_TOP_DIR
* without XMIT_NO_CONTENT_DIR on such a dir: the honest encoding is both flags
* (flist.c send path), so otherwise the receiver would set FLAG_CONTENT_DIR
* and delete_in_dir() could sweep pre-existing siblings under --delete. */
int is_implied_parent_dir(const char *name)
{
filter_rule *ent;
int parent_match = 0;
if (!implied_filter_list.head)
return 0;
/* The receiver exempts its synthetic transfer-root entry from the
* requested-name filter. Treat it as parent-only unless an empty/root
* source argument added the root-content rule. */
if ((name[0] == '.' && name[1] == '\0')
|| (name[0] == '/' && name[1] == '.' && name[2] == '\0')) {
for (ent = implied_filter_list.head; ent; ent = ent->next) {
if (!(ent->rflags & FILTRULE_INCLUDE))
continue;
if (strcmp(ent->pattern, "/**") == 0
|| strcmp(ent->pattern, "/*") == 0)
return 0;
}
return 1;
}
for (ent = implied_filter_list.head; ent; ent = ent->next) {
if (ent->rflags & (FILTRULE_PERDIR_MERGE | FILTRULE_CVS_IGNORE))
continue;
if (!rule_matches(name, ent, NAME_IS_DIR))
continue;
if (!(ent->rflags & FILTRULE_INCLUDE))
continue;
if (ent->rflags & FILTRULE_DIRECTORY) {
parent_match = 1;
continue;
}
/* A non-DIRECTORY include rule = a leaf the client asked for, so
* the dir is legitimately in the list, not parent-only. */
return 0;
}
return parent_match;
}
/* Return -1 if file "name" is defined to be excluded by the specified
* exclude list, 1 if it is included, and 0 if it was not matched. */
int check_filter(filter_rule_list *listp, enum logcode code,
@@ -1258,8 +1112,6 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
/* Inherit from the template. Don't inherit FILTRULES_SIDES; we check
* that later. */
rule->rflags = template->rflags & FILTRULES_FROM_CONTAINER;
if (rule_src_in_file)
rule->rflags |= FILTRULE_FROM_FILE; /* before parse_merge_name() */
/* Figure out what kind of a filter rule "s" is pointing at. Note
* that if FILTRULE_NO_PREFIXES is set, the rule is either an include
@@ -1357,7 +1209,8 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
rule->rflags |= FILTRULE_CLEAR_LIST;
break;
default:
filter_rule_err("Unknown filter rule", *rulestr_ptr);
rprintf(FERROR, "Unknown filter rule: `%s'\n", *rulestr_ptr);
exit_cleanup(RERR_SYNTAX);
}
while (ch != '!' && *++s && *s != ' ' && *s != '_') {
if (template->rflags & FILTRULE_WORD_SPLIT && isspace(*s)) {
@@ -1366,15 +1219,11 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
}
switch (*s) {
default:
invalid: {
char where[32];
snprintf(where, sizeof where, " '%c' at position %d",
*s, (int)(s - (const uchar *)*rulestr_ptr));
rprintf(FERROR, "invalid modifier%s in filter rule: %s\n",
rule_detail(NULL, where),
rule_text(NULL, *rulestr_ptr));
invalid:
rprintf(FERROR,
"invalid modifier '%c' at position %d in filter rule: %s\n",
*s, (int)(s - (const uchar *)*rulestr_ptr), *rulestr_ptr);
exit_cleanup(RERR_SYNTAX);
}
case '-':
if (!BITS_SETnUNSET(rule->rflags, FILTRULE_MERGE_FILE, FILTRULE_NO_PREFIXES))
goto invalid;
@@ -1446,8 +1295,10 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
/* The filter and template both specify side(s). This
* is dodgy (and won't work correctly if the template is
* a one-sided per-dir merge rule), so reject it. */
filter_rule_err("specified-side merge file contains specified-side filter",
*rulestr_ptr);
rprintf(FERROR,
"specified-side merge file contains specified-side filter: %s\n",
*rulestr_ptr);
exit_cleanup(RERR_SYNTAX);
}
rule->rflags |= template->rflags & FILTRULES_SIDES;
}
@@ -1462,14 +1313,17 @@ static filter_rule *parse_rule_tok(const char **rulestr_ptr,
len = strlen((char*)s);
if (rule->rflags & FILTRULE_CLEAR_LIST) {
if (!(template->rflags & FILTRULE_NO_PREFIXES)
if (!(rule->rflags & FILTRULE_NO_PREFIXES)
&& !(xflags & XFLG_OLD_PREFIXES) && len) {
filter_rule_err("'!' rule has trailing characters", *rulestr_ptr);
rprintf(FERROR,
"'!' rule has trailing characters: %s\n", *rulestr_ptr);
exit_cleanup(RERR_SYNTAX);
}
if (len > 1)
rule->rflags &= ~FILTRULE_CLEAR_LIST;
} else if (!len && !(rule->rflags & FILTRULE_CVS_IGNORE)) {
filter_rule_err("unexpected end of filter rule", *rulestr_ptr);
rprintf(FERROR, "unexpected end of filter rule: %s\n", *rulestr_ptr);
exit_cleanup(RERR_SYNTAX);
}
/* --delete-excluded turns an un-modified include/exclude into a sender-side rule. */
@@ -1528,8 +1382,8 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
break;
if (pat_len >= MAXPATHLEN) {
rprintf(FERROR, "discarding over-long filter: %s\n",
rule_text_len(NULL, pat, (int)pat_len));
rprintf(FERROR, "discarding over-long filter: %.*s\n",
(int)pat_len, pat);
free_continue:
free_filter(rule);
continue;
@@ -1557,11 +1411,6 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
filter_rule *excl_self;
excl_self = new0(filter_rule);
/* The pattern below is the merge rule's own text, so it
* inherits that rule's provenance. Built by hand, this
* rule looked argument-origin once parsing finished and
* the match trace echoed a merge file's contents at -vv. */
excl_self->rflags = rule->rflags & FILTRULE_FROM_FILE;
/* Find the beginning of the basename and add an exclude for it. */
for (name = pat + pat_len; name > pat && name[-1] != '/'; name--) {}
add_rule(listp, name, (pat + pat_len) - name, excl_self, 0);
@@ -1571,7 +1420,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
if (parent_dirscan) {
const char *p;
unsigned int len = pat_len;
if ((p = parse_merge_name(rule, pat, &len, module_dirlen)))
if ((p = parse_merge_name(pat, &len, module_dirlen)))
add_rule(listp, p, len, rule, 0);
else
free_filter(rule);
@@ -1580,7 +1429,7 @@ void parse_filter_str(filter_rule_list *listp, const char *rulestr,
} else {
const char *p;
unsigned int len = pat_len;
if ((p = parse_merge_name(rule, pat, &len, 0)))
if ((p = parse_merge_name(pat, &len, 0)))
parse_filter_file(listp, p, rule, XFLG_FATAL_ERRORS);
free_filter(rule);
continue;
@@ -1601,159 +1450,46 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
char line[BIGPATHBUFLEN];
char *eob = line + sizeof line - 1;
BOOL word_split = (template->rflags & FILTRULE_WORD_SPLIT) != 0;
const char *save_src_file, *save_src_named_at;
int save_src_line, save_src_in_file;
int named_by_file;
int pending = EOF;
char named_at[MAXPATHLEN + 32];
/* Our own copy: fname may point into parse_merge_name()'s static buffer,
* which a merge rule inside THIS file overwrites while we still need it. */
char src_name[MAXPATHLEN];
if (!fname || !*fname)
return;
if (merge_depth >= MAX_MERGE_DEPTH) {
rprintf(FERROR,
"[%s] merge-file include depth limit (%d) exceeded at %s\n",
who_am_i(), MAX_MERGE_DEPTH, rule_text(template, fname));
/* Match the failed-open path below: abort under a fatal
* (operator-supplied) merge, otherwise drop the rule. */
if (xflags & XFLG_FATAL_ERRORS)
exit_cleanup(RERR_FILEIO);
return;
}
merge_depth++;
if (*fname != '-' || fname[1] || am_server) {
/* This path is operator- and (via per-directory merge files like
* .cvsignore) sender-controlled: a planted symlink could leak a
* root-readable file through the filter parser, or redirect an
* --exclude-from open via a planted parent. Refuse symlinks not
* owned by uid 0 or our euid. */
const char *open_path;
int fd;
if (daemon_filter_list.head) {
char *dir;
strlcpy(line, fname, sizeof line);
/* parse_merge_name() prepends module_dir for absolute paths,
* so strip module_dirlen back off before the check or the
* anchored module-relative daemon rule won't match (as
* options.c does for --exclude-from/--include-from). The
* original absolute path is still used for the open below. */
dir = line + (*line == '/' ? module_dirlen : 0);
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0) {
/* Hidden by the daemon filter: treat the merge file as
* non-existent rather than tripping XFLG_FATAL_ERRORS
* below, so it neither errors out nor leaks a
* fatal-vs-silent oracle. */
if (DEBUG_GTE(FILTER, 2)) {
/* Same rule as everywhere else: the name is
* file content when a rule we read named it,
* and so is "the daemon filter hides it". */
rprintf(FINFO, "[%s] parse_filter_file(%s)%s\n",
who_am_i(), rule_text(template, fname),
rule_detail(template, " hidden by daemon filter"));
}
merge_depth--;
return;
}
open_path = line;
clean_fname(line, CFN_COLLAPSE_DOT_DOT_DIRS);
if (check_filter(&daemon_filter_list, FLOG, line, 0) < 0)
fp = NULL;
else
fp = fopen(line, "rb");
} else
open_path = fname;
/* Confine the open to the module root. The ownership walk on its own
* is not enough for a peer-driven merge file: a non-chrooted daemon
* writes --backup-dir entries as root, so a raced backup symlink is
* ROOT-owned -- exactly what the ownership walk treats as trusted --
* and naming it in a dir-merge rule would read an out-of-module file
* in as filter rules (their text comes back to the peer in "Unknown
* filter rule" errors).
*
* The daemon's own "filter"/"include from"/"exclude from" parameters
* are exempt: those are operator-configured and legitimately live
* outside the module (/etc/rsync/excludes and the like). */
fd = vfs_open_owner_walk(open_path, O_RDONLY, 0, !daemon_config_filter_file);
if (fd < 0)
fp = NULL;
else if (!(fp = fdopen(fd, "rb")))
close(fd);
fp = fopen(fname, "rb");
} else
fp = stdin;
if (DEBUG_GTE(FILTER, 2)) {
/* The name is file CONTENT when a rule we read named it, and a
* word-split per-dir merge turns every word of a file into one
* of these -- so the trace would echo what the syntax errors no
* longer do. Say where it came from instead. */
rprintf(FINFO, "[%s] parse_filter_file(%s,%x,%x)%s\n",
who_am_i(), rule_text(template, fname), template->rflags, xflags,
rule_detail(template, fp ? "" : " [not found]"));
who_am_i(), fname, template->rflags, xflags,
fp ? "" : " [not found]");
}
if (!fp) {
if (xflags & XFLG_FATAL_ERRORS) {
/* rule_src_file is still the PARENT's context here: when it
* is set, this name came out of a file we read, so neither
* the name nor errno (an existence oracle) may be shown. */
if (TEXT_FROM_FILE(template)) {
/* errno too: it answers "does this path exist". */
rprintf(FERROR, "failed to open %sclude file %s\n",
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
rule_text(template, fname));
} else {
rsyserr(FERROR, errno,
"failed to open %sclude file %s",
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
fname);
}
rsyserr(FERROR, errno,
"failed to open %sclude file %s",
template->rflags & FILTRULE_INCLUDE ? "in" : "ex",
fname);
exit_cleanup(RERR_FILEIO);
}
merge_depth--;
return;
}
/* Before dirbuf is cut back: a per-directory fname points INTO dirbuf,
* so truncating first leaves only the directory and the location we
* report loses the filename. */
strlcpy(src_name, fname, sizeof src_name);
dirbuf[dirbuf_len] = '\0';
/* Rule text from here on is this file's contents, not an argument, so
* a syntax error must not echo it. Saved and restored because a merge
* rule inside this file can bring us back in for another file. */
save_src_in_file = rule_src_in_file;
save_src_file = rule_src_file;
save_src_line = rule_src_line;
/* If a rule we read named THIS file, our own path is file content too:
* track the location for provenance but do not put it in a message. */
named_by_file = TEXT_FROM_FILE(template);
save_src_named_at = rule_src_named_at;
if (named_by_file) {
/* Snapshot where we were told to merge this, before that state
* is replaced below (rule_src_where returns a static buffer).
* A DEFERRED merge has no live location to point at -- the file
* that named it was read and finished long ago -- so leave the
* generic description rather than nesting two vague ones. */
if (rule_src_in_file) {
strlcpy(named_at, rule_src_where(), sizeof named_at);
rule_src_named_at = named_at;
} else
rule_src_named_at = NULL;
}
rule_src_in_file = 1;
rule_src_file = named_by_file ? NULL : src_name;
rule_src_line = word_split ? -1 : 0; /* -1: tokens, not lines */
while (1) {
char *s = line;
int ch, overflow = 0;
if (rule_src_line >= 0)
rule_src_line++;
while (1) {
if (pending != EOF) { /* a CR lookahead we could not push back */
ch = pending;
pending = EOF;
} else if ((ch = getc(fp)) == EOF) {
if ((ch = getc(fp)) == EOF) {
if (ferror(fp) && errno == EINTR) {
clearerr(fp);
continue;
@@ -1762,51 +1498,25 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
}
if (word_split && isspace(ch))
break;
if (eol_nulls? !ch : (ch == '\n' || ch == '\r')) {
if (ch == '\r') { /* CRLF is one line, not two */
int nxt;
while ((nxt = getc(fp)) == EOF
&& ferror(fp) && errno == EINTR)
clearerr(fp);
if (nxt == EOF) {
if (!ferror(fp))
ch = EOF; /* real end of file */
} else if (nxt != '\n' && ungetc(nxt, fp) == EOF) {
/* Pushback failed: hand it to the
* NEXT rule, where it belongs --
* appending it here would both
* corrupt this rule and skip the
* s < eob bound below. */
pending = nxt;
}
}
if (eol_nulls? !ch : (ch == '\n' || ch == '\r'))
break;
}
if (s < eob)
*s++ = ch;
else
overflow = 1;
}
if (overflow) {
rprintf(FERROR, "discarding over-long filter: %s\n",
rule_text_len(NULL, line, 0));
rprintf(FERROR, "discarding over-long filter: %s...\n", line);
s = line;
}
*s = '\0';
/* Skip an empty token and (when line parsing) comments. */
if (*line && (word_split || (*line != ';' && *line != '#'))) {
rule_src_file = named_by_file ? NULL : src_name;
if (*line && (word_split || (*line != ';' && *line != '#')))
parse_filter_str(listp, line, template, xflags);
}
if (ch == EOF)
break;
}
rule_src_in_file = save_src_in_file;
rule_src_file = save_src_file;
rule_src_line = save_src_line;
rule_src_named_at = save_src_named_at;
fclose(fp);
merge_depth--;
}
/* If the "for_xfer" flag is set, the prefix is made compatible with the
+35 -95
View File
@@ -45,17 +45,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
int ret = 0;
if (updating_basis_or_equiv) {
if (sparse_seek && vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0)
if (sparse_seek && do_punch_hole(f, sparse_past_write, sparse_seek) < 0)
ret = -1;
#ifdef HAVE_FTRUNCATE /* A compilation formality -- in-place requires ftruncate() */
else /* Just in case the original file was longer */
ret = vfs_ftruncate(f, size);
ret = do_ftruncate(f, size);
#endif
} else if (sparse_seek) {
#ifdef HAVE_FTRUNCATE
ret = vfs_ftruncate(f, size);
ret = do_ftruncate(f, size);
#else
if (vfs_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
if (do_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
ret = -1;
else {
do {
@@ -75,60 +75,11 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
/* Note that the offset is just the caller letting us know where
* the current file position is in the file. The use_seek arg tells
* us that we should seek over matching data instead of writing it. */
/* Flush any deferred run of zero bytes as a hole, advancing the file
* position past it (both vfs_lseek() and vfs_punch_hole() move the offset). */
static int flush_sparse_hole(int f)
{
if (!sparse_seek)
return 0;
if (sparse_past_write >= preallocated_len) {
if (vfs_lseek(f, sparse_seek, SEEK_CUR) < 0) {
sparse_seek = 0;
return -1;
}
} else if (vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
sparse_seek = 0;
return -1;
}
sparse_seek = 0;
return 0;
}
static int full_sparse_write(int f, const char *buf, int len)
{
while (len > 0) {
int ret = write(f, buf, len);
if (ret <= 0) {
if (ret < 0 && errno == EINTR)
continue;
sparse_seek = 0;
return -1;
}
buf += ret;
len -= ret;
}
return 0;
}
/* Emit one span of data that is not being turned into a hole. For an in-place
* update (use_seek) the bytes on disk already match, so we only need to move
* past them; otherwise we write them out. Either way a deferred hole is
* flushed first so that the span lands at the right offset. */
static int emit_sparse_span(int f, int use_seek, const char *buf, int len)
{
if (flush_sparse_hole(f) < 0)
return -1;
if (use_seek)
return vfs_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;
return full_sparse_write(f, buf, len);
}
static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int len)
{
int l1, l2, i, start, end;
int l1 = 0, l2 = 0;
int ret;
/* Always treat a leading and trailing run of zeros as a (deferred)
* hole, since they may merge with holes in the adjacent write calls. */
for (l1 = 0; l1 < len && buf[l1] == 0; l1++) {}
for (l2 = 0; l2 < len-l1 && buf[len-(l2+1)] == 0; l2++) {}
@@ -137,46 +88,37 @@ static int write_sparse(int f, int use_seek, OFF_T offset, const char *buf, int
if (l1 == len)
return len;
/* Scan the middle [l1, len-l2) for interior runs of zeros that are at
* least SPARSE_WRITE_SIZE long (the hole granularity rsync has always
* used) and defer those as holes. Everything in between -- which may
* include shorter zero runs not worth a hole -- is emitted in one go,
* rather than being chopped into SPARSE_WRITE_SIZE-byte pieces, which
* made copying a large non-sparse file cost ~one write() per KiB.
*
* The matched (use_seek) case runs through the same scan: its interior
* zero runs still have to be punched out, which is what --inplace
* --sparse relies on to keep a hole-y basis file sparse. */
start = l1;
end = len - l2;
for (i = l1; i < end; ) {
int z;
if (buf[i] != 0) {
i++;
continue;
}
for (z = 1; i + z < end && buf[i+z] == 0; z++) {}
if (z < SPARSE_WRITE_SIZE) {
i += z;
continue;
}
if (i > start) {
if (emit_sparse_span(f, use_seek, buf + start, i - start) < 0)
if (sparse_seek) {
if (sparse_past_write >= preallocated_len) {
if (do_lseek(f, sparse_seek, SEEK_CUR) < 0)
return -1;
sparse_past_write = offset + i;
}
sparse_seek += z;
i += z;
start = i;
}
if (end > start) {
if (emit_sparse_span(f, use_seek, buf + start, end - start) < 0)
} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
sparse_seek = 0;
return -1;
}
}
sparse_seek = l2;
sparse_past_write = offset + len - l2;
if (use_seek) {
/* The in-place data already matches. */
if (do_lseek(f, len - (l1+l2), SEEK_CUR) < 0)
return -1;
return len;
}
while ((ret = write(f, buf + l1, len - (l1+l2))) <= 0) {
if (ret < 0 && errno == EINTR)
continue;
sparse_seek = 0;
return ret;
}
if (ret != (int)(len - (l1+l2))) {
sparse_seek = 0;
return l1+ret;
}
return len;
}
@@ -211,10 +153,8 @@ int write_file(int f, int use_seek, OFF_T offset, const char *buf, int len)
while (len > 0) {
int r1;
if (sparse_files > 0) {
/* write_sparse() handles the whole span itself, scanning
* for holes and coalescing the non-zero data into large
* write()s instead of SPARSE_WRITE_SIZE-byte dribbles. */
r1 = write_sparse(f, use_seek, offset, buf, len);
int len1 = MIN(len, SPARSE_WRITE_SIZE);
r1 = write_sparse(f, use_seek, offset, buf, len1);
offset += r1;
} else {
if (!wf_writeBuf) {
@@ -262,7 +202,7 @@ int skip_matched(int fd, OFF_T offset, const char *buf, int len)
if (flush_write_file(fd) < 0)
return -1;
if ((pos = vfs_lseek(fd, len, SEEK_CUR)) != offset + len) {
if ((pos = do_lseek(fd, len, SEEK_CUR)) != offset + len) {
rsyserr(FERROR_XFER, errno, "lseek returned %s, not %s",
big_num(pos), big_num(offset));
return -1;
@@ -345,7 +285,7 @@ char *map_ptr(struct map_struct *map, OFF_T offset, int32 len)
}
if (map->p_fd_offset != read_start) {
OFF_T ret = vfs_lseek(map->fd, read_start, SEEK_SET);
OFF_T ret = do_lseek(map->fd, read_start, SEEK_SET);
if (ret != read_start) {
rsyserr(FERROR, errno, "lseek returned %s, not %s",
big_num(ret), big_num(read_start));
+51 -386
View File
@@ -29,10 +29,6 @@
extern int am_root;
extern int am_server;
extern int am_daemon;
extern int am_chrooted;
extern char *module_dir;
extern unsigned int module_dirlen;
extern int module_dirfd;
extern int am_sender;
extern int am_generator;
extern int inc_recurse;
@@ -68,7 +64,6 @@ extern int non_perishable_cnt;
extern int prune_empty_dirs;
extern int copy_links;
extern int copy_unsafe_links;
extern int insecure_links;
extern int protocol_version;
extern int sanitize_paths;
extern int munge_symlinks;
@@ -86,6 +81,7 @@ extern char *usermap, *groupmap;
extern struct name_num_item *file_sum_nni;
extern char curr_dir[MAXPATHLEN];
extern struct chmod_mode_struct *chmod_modes;
@@ -136,18 +132,6 @@ static int64 tmp_dev = -1, tmp_ino;
#endif
static char tmp_sum[MAX_DIGEST_LEN];
#ifdef ST_MTIME_NSEC
/* Return st_mtim nsec if it is in the wire-valid range, else 0. */
static inline uint32 wire_mtime_nsec_from_stat(const STRUCT_STAT *stp)
{
unsigned long nsec = (unsigned long)stp->ST_MTIME_NSEC;
if (nsec > MAX_WIRE_NSEC)
return 0;
return (uint32)nsec;
}
#endif
static char empty_sum[MAX_DIGEST_LEN];
static int flist_count_offset; /* for --delete --progress */
static int show_filelist_progress;
@@ -218,47 +202,13 @@ void show_flist_stats(void)
*
* The stat structure pointed to by stp will contain information about the
* link or the referent as appropriate, if they exist. */
/* Set by send_directory() to the fd of the directory it is currently scanning
* (and that dir's path prefix), so the per-entry stat can go through the
* already-open dir fd instead of re-resolving the full path for every entry.
* Pure performance and sender-side only -- the scanned dir is already open, so
* fstatat(scan_dirfd, basename) is identical to lstat(scandir/basename); no
* confinement is implied or needed. */
static int scan_dirfd = -1;
static const char *scan_dir_prefix;
static int scan_dir_prefix_len;
static int scan_link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)
{
/* Use the held scan fd only for a single component directly inside the
* scanned dir, and only when am_root >= 0 (link_stat_at folds in no
* fake-super %stat xattr; link_stat does so via get_stat_xattr, a no-op
* once am_root >= 0). */
if (scan_dirfd >= 0 && am_root >= 0
&& strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0
&& path[scan_dir_prefix_len] == '/'
&& strchr(path + scan_dir_prefix_len + 1, '/') == NULL)
return link_stat_at(scan_dirfd, path + scan_dir_prefix_len + 1, stp, follow_dirlinks);
return link_stat(path, stp, follow_dirlinks);
}
static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)
{
if (scan_dirfd >= 0 && am_root >= 0
&& strncmp(path, scan_dir_prefix, scan_dir_prefix_len) == 0
&& path[scan_dir_prefix_len] == '/'
&& strchr(path + scan_dir_prefix_len + 1, '/') == NULL)
return vfs_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);
return vfs_readlink(path, linkbuf, bufsiz);
}
static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
{
#ifdef SUPPORT_LINKS
if (scan_link_stat(path, stp, copy_dirlinks) < 0)
if (link_stat(path, stp, copy_dirlinks) < 0)
return -1;
if (S_ISLNK(stp->st_mode)) {
int llen = scan_readlink(path, linkbuf, MAXPATHLEN - 1);
int llen = do_readlink(path, linkbuf, MAXPATHLEN - 1);
if (llen < 0)
return -1;
linkbuf[llen] = '\0';
@@ -267,7 +217,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
rprintf(FINFO,"copying unsafe symlink \"%s\" -> \"%s\"\n",
path, linkbuf);
}
return x_stat(path, stp, NULL, 0);
return x_stat(path, stp, NULL);
}
if (munge_symlinks && am_sender && llen > SYMLINK_PREFIX_LEN
&& strncmp(linkbuf, SYMLINK_PREFIX, SYMLINK_PREFIX_LEN) == 0) {
@@ -277,7 +227,7 @@ static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
}
return 0;
#else
return x_stat(path, stp, NULL, 0);
return x_stat(path, stp, NULL);
#endif
}
@@ -285,41 +235,17 @@ int link_stat(const char *path, STRUCT_STAT *stp, int follow_dirlinks)
{
#ifdef SUPPORT_LINKS
if (copy_links)
return x_stat(path, stp, NULL, 0);
if (x_lstat(path, stp, NULL, 0) < 0)
return x_stat(path, stp, NULL);
if (x_lstat(path, stp, NULL) < 0)
return -1;
if (follow_dirlinks && S_ISLNK(stp->st_mode)) {
STRUCT_STAT st;
if (x_stat(path, &st, NULL, 0) == 0 && S_ISDIR(st.st_mode))
if (x_stat(path, &st, NULL) == 0 && S_ISDIR(st.st_mode))
*stp = st;
}
return 0;
#else
return x_stat(path, stp, NULL, 0);
#endif
}
/* Held-dirfd variant of link_stat(): stat single-component `name` relative to
* directory fd `dfd`, instead of re-resolving a full path. Equivalent to
* link_stat() only when NOT in --fake-super mode -- x_stat/x_lstat fold the
* fake-super %stat xattr into the result via get_stat_xattr(), which is a
* path-based no-op once am_root >= 0. Callers therefore use this only when
* am_root >= 0 (and a valid dfd), falling back to link_stat() otherwise. */
int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlinks)
{
#ifdef SUPPORT_LINKS
if (copy_links)
return vfs_stat(dfd, name, stp, 0);
if (vfs_lstat(dfd, name, stp, 0) < 0)
return -1;
if (follow_dirlinks && S_ISLNK(stp->st_mode)) {
STRUCT_STAT st;
if (vfs_stat(dfd, name, &st, 0) == 0 && S_ISDIR(st.st_mode))
*stp = st;
}
return 0;
#else
return vfs_stat(dfd, name, stp, 0);
return x_stat(path, stp, NULL);
#endif
}
@@ -365,31 +291,17 @@ static void flist_expand(struct file_list *flist, int extra)
{
struct file_struct **new_ptr;
/* Refuse BEFORE any int arithmetic below can overflow: used+extra (computed
* in the early-return and the cap below) and the malloced growth math. Only
* reachable past INT_MAX entries (my_alloc's --max-alloc cap normally stops
* the list growing anywhere near there). */
if (extra < 0 || flist->used < 0 || flist->used > INT_MAX - extra)
goto too_large;
if (flist->used + extra <= flist->malloced)
return;
if (flist->malloced < FLIST_START)
flist->malloced = FLIST_START;
else if (flist->malloced >= FLIST_LINEAR) {
if (flist->malloced > INT_MAX - FLIST_LINEAR)
goto too_large;
else if (flist->malloced >= FLIST_LINEAR)
flist->malloced += FLIST_LINEAR;
} else if (flist->malloced < FLIST_START_LARGE/16) {
if (flist->malloced > INT_MAX/4)
goto too_large;
else if (flist->malloced < FLIST_START_LARGE/16)
flist->malloced *= 4;
} else {
if (flist->malloced > INT_MAX/2)
goto too_large;
else
flist->malloced *= 2;
}
/* In case count jumped or we are starting the list
* with a known size just set it. */
@@ -406,11 +318,6 @@ static void flist_expand(struct file_list *flist, int extra)
}
flist->files = new_ptr;
return;
too_large:
rprintf(FERROR, "[%s] file list has grown too large to expand\n", who_am_i());
exit_cleanup(RERR_MALLOC);
}
static void flist_done_allocating(struct file_list *flist)
@@ -857,7 +764,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
if ((basename = strrchr(thisname, '/')) != NULL) {
int len = basename++ - thisname;
if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {
if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {
lastdir = new_array(char, len + 1);
memcpy(lastdir, thisname, len);
lastdir[len] = '\0';
@@ -906,17 +813,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
rdev_major = DEV_MAJOR(devp);
rdev = MAKEDEV(rdev_major, DEV_MINOR(devp));
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
} else if (IS_DEVICE(mode)) {
/* Abbrev-branch counterpart to the !preserve_devices
* stub-alloc below: zeroed F_RDEV_P slots. */
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
}
if (preserve_links && S_ISLNK(mode))
linkname_len = strlen(F_SYMLINK(first)) + 1;
else if (S_ISLNK(mode))
/* Abbrev-branch counterpart to the !preserve_links
* stub-alloc below: empty linkname. */
linkname_len = 1;
else
linkname_len = 0;
real_ISREG_entry = S_ISREG(mode) ? 1 : 0;
@@ -941,9 +840,9 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
}
if (xflags & XMIT_MOD_NSEC)
#ifndef CAN_SET_NSEC
(void)read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec");
(void)read_varint(f);
#else
modtime_nsec = read_varint_bounded(f, 0, MAX_WIRE_NSEC, "modtime_nsec");
modtime_nsec = read_varint(f);
else
modtime_nsec = 0;
#endif
@@ -962,24 +861,8 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
#endif
}
#endif
if (!(xflags & XMIT_SAME_MODE)) {
if (!(xflags & XMIT_SAME_MODE))
mode = from_wire_mode(read_int(f));
/* Reject modes whose type bits are not one of the standard
* file types; otherwise garbage mode values propagate through
* the file-type checks below unpredictably. mode 0 is the one
* legitimate exception: --delete-missing-args (missing_args==2)
* sends a missing arg as a mode-0 entry (IS_MISSING_FILE), the
* generator's delete signal (#910). */
if (mode != 0 || missing_args != 2) {
if (!S_ISREG(mode) && !S_ISDIR(mode) && !S_ISLNK(mode)
&& !S_ISCHR(mode) && !S_ISBLK(mode)
&& !S_ISFIFO(mode) && !S_ISSOCK(mode)) {
rprintf(FERROR, "invalid file mode 0%o for %s [%s]\n",
(unsigned)mode, lastname, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
}
}
if (atimes_ndx && !S_ISDIR(mode) && !(xflags & XMIT_SAME_ATIME)) {
atime = read_varlong(f, 4);
#if SIZEOF_TIME_T < SIZEOF_INT64
@@ -1038,15 +921,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
if (IS_DEVICE(mode))
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
file_length = 0;
} else if (IS_DEVICE(mode)) {
/* Peer/batch sent an S_IFCHR/S_IFBLK entry but we are not
* preserving devices. A cooperating sender wouldn't do this;
* a crafted batch can. Allocate (and zero, via the memset
* below) the DEV_EXTRA_CNT slots so F_RDEV_P() callers
* (set_stat_xattr under --fake-super, generator IS_DEVICE
* paths) read {0,0} instead of the previous pool slot. */
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
file_length = 0;
} else if (protocol_version < 28)
rdev = MAKEDEV(0, 0);
@@ -1067,14 +941,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
#endif
if (munge_symlinks)
linkname_len += SYMLINK_PREFIX_LEN;
} else if (S_ISLNK(mode)) {
/* Peer/batch sent an S_IFLNK entry but we are not preserving
* links (no -l, and the batch stream-flags didn't set it). A
* cooperating sender wouldn't do this; a crafted batch can.
* Allocate one byte for an empty linkname so F_SYMLINK()
* callers (log.c %L, generator.c) read a valid "" instead of
* the next pool slot's redzone. */
linkname_len = 1;
}
else
#endif
@@ -1122,15 +988,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
exit_cleanup(RERR_UNSUPPORTED);
}
/* "." is the synthetic transfer root. Reinterpreting it as a file lets
* --force recursively remove the real destination directory before the
* receiver creates that file. */
if ((!strcmp(thisname, ".") || !strcmp(thisname, "/.")) && !S_ISDIR(mode)) {
rprintf(FERROR, "ERROR: rejecting non-directory transfer-root entry: %s\n",
thisname);
exit_cleanup(RERR_PROTOCOL);
}
if (*thisname == '/' ? thisname[1] != '.' || thisname[2] != '\0' : *thisname != '.' || thisname[1] != '\0') {
int filt_flags = S_ISDIR(mode) ? NAME_IS_DIR : NAME_IS_FILE;
if (!trust_sender_filter /* a per-dir filter rule means we must trust the sender's filtering */
@@ -1170,8 +1027,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
memcpy(bp, basename, basename_len);
#ifdef SUPPORT_HARD_LINKS
if (preserve_hard_links && xflags & XMIT_HLINKED
&& !S_ISDIR(mode)
if (xflags & XMIT_HLINKED
#ifndef CAN_HARDLINK_SYMLINK
&& !S_ISLNK(mode)
#endif
@@ -1226,26 +1082,6 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
if (basename_len == 1+1 && *basename == '.') /* +1 for '\0' */
F_DEPTH(file)--;
if (protocol_version >= 30) {
/* Stop a malicious sender expanding --delete scope by flagging
* an implied parent as a content dir: if we only allowed this
* entry as a parent of the requested leaf, force the flags back
* to the honest implied-parent encoding (XMIT_TOP_DIR |
* XMIT_NO_CONTENT_DIR) so it lands in FLAG_IMPLIED_DIR, not
* FLAG_CONTENT_DIR, and delete_in_dir() can't sweep siblings.
* Not gated on trust_sender_filter: implied_filter_list is
* receiver-owned state, so a per-dir filter must not be able to
* downgrade this defense. */
if (implied_filter_list.head
&& is_implied_parent_dir(thisname)
&& (!(xflags & XMIT_NO_CONTENT_DIR) || !(xflags & XMIT_TOP_DIR))) {
if (DEBUG_GTE(FILTER, 1)) {
rprintf(FINFO,
"[%s] receiver downgraded implied-parent dir %s "
"to non-content (sender xflags=0x%x)\n",
who_am_i(), thisname, xflags);
}
xflags |= XMIT_NO_CONTENT_DIR | XMIT_TOP_DIR;
}
if (!(xflags & XMIT_NO_CONTENT_DIR)) {
if (xflags & XMIT_TOP_DIR)
file->flags |= FLAG_TOP_DIR;
@@ -1253,17 +1089,13 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
} else if (xflags & XMIT_TOP_DIR)
file->flags |= FLAG_IMPLIED_DIR;
} else if (xflags & XMIT_TOP_DIR) {
if (implied_filter_list.head && is_implied_parent_dir(thisname))
file->flags |= FLAG_IMPLIED_DIR;
else {
in_del_hier = recurse;
del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;
if (relative_paths && del_hier_name_len > 2
&& lastname[del_hier_name_len-1] == '.'
&& lastname[del_hier_name_len-2] == '/')
del_hier_name_len -= 2;
file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;
}
in_del_hier = recurse;
del_hier_name_len = F_DEPTH(file) == 0 ? 0 : l1 + l2;
if (relative_paths && del_hier_name_len > 2
&& lastname[del_hier_name_len-1] == '.'
&& lastname[del_hier_name_len-2] == '/')
del_hier_name_len -= 2;
file->flags |= FLAG_TOP_DIR | FLAG_CONTENT_DIR;
} else if (in_del_hier) {
if (!relative_paths || !del_hier_name_len
|| (l1 >= del_hier_name_len
@@ -1283,11 +1115,7 @@ static struct file_struct *recv_file_entry(int f, struct file_list *flist, int x
#ifdef SUPPORT_LINKS
if (linkname_len) {
bp += basename_len;
if (!preserve_links) {
/* The empty-linkname case allocated above; nothing on
* the wire to read. Just terminate it. */
*bp = '\0';
} else if (first_hlink_ndx >= flist->ndx_start) {
if (first_hlink_ndx >= flist->ndx_start) {
struct file_struct *first = flist->files[first_hlink_ndx - flist->ndx_start];
memcpy(bp, F_SYMLINK(first), linkname_len);
} else {
@@ -1411,7 +1239,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
int extra_len = file_extra_cnt * EXTRA_LEN;
const char *basename;
alloc_pool_t *pool;
STRUCT_STAT st = {0};
STRUCT_STAT st;
char *bp;
if (strlcpy(thisname, fname, sizeof thisname) >= sizeof thisname) {
@@ -1447,7 +1275,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
* options was specified, so there's no need for the
* extra lstat() if one of these options isn't on. */
if ((copy_links || copy_unsafe_links || copy_dirlinks)
&& x_lstat(thisname, &st, NULL, 0) == 0
&& x_lstat(thisname, &st, NULL) == 0
&& S_ISLNK(st.st_mode)) {
io_error |= IOERR_GENERAL;
rprintf(FERROR_XFER, "symlink has no referent: %s\n",
@@ -1544,7 +1372,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
if ((basename = strrchr(thisname, '/')) != NULL) {
int len = basename++ - thisname;
if (len != lastdir_len || !lastdir || memcmp(thisname, lastdir, len) != 0) {
if (len != lastdir_len || memcmp(thisname, lastdir, len) != 0) {
lastdir = new_array(char, len + 1);
memcpy(lastdir, thisname, len);
lastdir[len] = '\0';
@@ -1562,7 +1390,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
if (copy_devices && am_sender && IS_DEVICE(st.st_mode)) {
if (st.st_size == 0) {
int fd = vfs_open_checklinks(fname);
int fd = do_open_checklinks(fname);
if (fd >= 0) {
st.st_size = get_device_size(fd, fname);
close(fd);
@@ -1573,12 +1401,8 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
}
#ifdef ST_MTIME_NSEC
{
uint32 nsec = wire_mtime_nsec_from_stat(&st);
if (nsec && protocol_version >= 31)
extra_len += EXTRA_LEN;
}
if (st.ST_MTIME_NSEC && protocol_version >= 31)
extra_len += EXTRA_LEN;
#endif
#if SIZEOF_CAPITAL_OFF_T >= 8
if (st.st_size > 0xFFFFFFFFu && S_ISREG(st.st_mode))
@@ -1591,18 +1415,6 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
extra_len += SUM_EXTRA_CNT * EXTRA_LEN;
}
#ifdef HAVE_STRUCT_STAT_ST_RDEV
/* The sender path historically passes rdev via the tmp_rdev static
* (read by send_file_entry()), so make_file() never reserved
* DEV_EXTRA_CNT in the file_struct itself. But receiver-side callers
* (recv_generator's --inplace --backup back_file, backup.c make_backup)
* hand this struct to set_file_attrs() -> set_stat_xattr(), which reads
* F_RDEV_P(file) under --fake-super. Reserve and populate the slots
* so the struct is self-contained, matching recv_file_entry(). */
if (IS_DEVICE(st.st_mode))
extra_len += DEV_EXTRA_CNT * EXTRA_LEN;
#endif
#if EXTRA_ROUNDING > 0
if (extra_len & (EXTRA_ROUNDING * EXTRA_LEN))
extra_len = (extra_len | (EXTRA_ROUNDING * EXTRA_LEN)) + EXTRA_LEN;
@@ -1636,10 +1448,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
#ifdef HAVE_STRUCT_STAT_ST_RDEV
if (IS_DEVICE(st.st_mode)) {
uint32 *devp = F_RDEV_P(file);
tmp_rdev = st.st_rdev;
DEV_MAJOR(devp) = major(st.st_rdev);
DEV_MINOR(devp) = minor(st.st_rdev);
st.st_size = 0;
} else if (IS_SPECIAL(st.st_mode))
st.st_size = 0;
@@ -1648,13 +1457,9 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
file->flags = flags;
file->modtime = st.st_mtime;
#ifdef ST_MTIME_NSEC
{
uint32 nsec = wire_mtime_nsec_from_stat(&st);
if (nsec && protocol_version >= 31) {
file->flags |= FLAG_MOD_NSEC;
F_MOD_NSEC(file) = nsec;
}
if (st.ST_MTIME_NSEC && protocol_version >= 31) {
file->flags |= FLAG_MOD_NSEC;
F_MOD_NSEC(file) = st.ST_MTIME_NSEC;
}
#endif
file->len32 = (uint32)st.st_size;
@@ -1675,7 +1480,7 @@ struct file_struct *make_file(const char *fname, struct file_list *flist,
F_ATIME(file) = st.st_atime;
#ifdef SUPPORT_CRTIMES
if (crtimes_ndx)
F_CRTIME(file) = vfs_get_create_time(fname, &st);
F_CRTIME(file) = get_create_time(fname, &st);
#endif
if (basename != thisname)
@@ -1824,7 +1629,6 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
sx.st.st_mode = file->mode;
if (get_acl(fname, &sx) < 0) {
io_error |= IOERR_GENERAL;
free_acl(&sx);
return NULL;
}
}
@@ -1832,11 +1636,8 @@ static struct file_struct *send_file_name(int f, struct file_list *flist,
#ifdef SUPPORT_XATTRS
if (preserve_xattrs) {
sx.st.st_mode = file->mode;
if (get_xattr(fname, -1, &sx) < 0) {
if (get_xattr(fname, &sx) < 0) {
io_error |= IOERR_GENERAL;
#ifdef SUPPORT_ACLS
free_acl(&sx); /* get_acl() above may have loaded one */
#endif
return NULL;
}
}
@@ -2011,77 +1812,6 @@ static void interpret_stat_error(const char *fname, int is_dir)
}
}
#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD
/* Open a source directory for scanning confined beneath the transfer root.
* vfs_resolve_open() does a per-component O_NOFOLLOW walk that refuses a
* parent component raced into a symlink pointing out of the tree; fdopendir()
* then turns the held fd into the DIR* the scan reads. This mirrors the
* sender's confined content open (sender.c): the directory enumeration must be
* confined the same way, or a parent-symlink race (or, for a daemon following
* mode, an in-module symlink to outside) lets the scan enumerate an out-of-tree
* directory and leak its names/metadata/symlink targets. O_DIRECTORY without
* O_NOFOLLOW makes vfs_resolve_open() follow in-tree directory symlinks
* beneath the anchor and refuse escapes, so this serves both the default
* no-follow scan and a daemon's symlink-following scan (see the caller).
* Returns NULL with errno set on failure, like opendir(). */
static DIR *secure_opendir(const char *fbuf)
{
int dfd, fl;
DIR *d;
if (am_daemon && (!am_chrooted || module_dirlen)
&& module_dir && module_dir[0] == '/' && *fbuf != '/' && module_dirfd >= 0
&& vfs.curr_dir_len >= module_dirlen
&& strncmp(vfs.curr_dir, module_dir, module_dirlen) == 0
&& (vfs.curr_dir[module_dirlen] == '\0' || vfs.curr_dir[module_dirlen] == '/')) {
/* Daemon: anchor the confined scan at the module root pinned by identity
* at module setup (module_dirfd, opened while the daemon was positioned
* there and still privileged), and walk the module-relative path of the
* scan target beneath it. This re-follows the same in-module path -- so a
* legitimate in-module ".." climb (sub/climb -> ../sibling) or an in-module
* directory symlink is followed, and an escape refused -- without
* re-walking the absolute module path as the dropped uid (the privilege-
* drop EACCES), and without assuming the lexical vfs.curr_dir depth matches the
* real cwd (a followed in-module symlink can desync them; anchoring at the
* pinned module root and walking down the logical path is correct either
* way). */
const char *p = vfs.curr_dir + module_dirlen;
char modrel[MAXPATHLEN];
while (*p == '/')
p++;
if ((size_t)snprintf(modrel, sizeof modrel, "%s%s%s",
p, *p ? "/" : "", fbuf) >= sizeof modrel) {
errno = ENAMETOOLONG;
return NULL;
}
dfd = vfs_resolve_open_at(module_dirfd, *modrel ? modrel : ".",
O_RDONLY | O_DIRECTORY, 0);
} else if (*fbuf == '/') {
/* An absolute scan path (an absolute --relative / --files-from name, or a
* "/" transfer root): anchor at "/" -- operator-named, trusted. */
const char *relp = fbuf;
while (*relp == '/')
relp++;
dfd = vfs_resolve_open("/", relp, O_RDONLY | O_DIRECTORY, 0);
} else {
/* Non-daemon (or chrooted) sender: confine beneath the cwd the sender
* chdir'd into (the transfer root). */
dfd = vfs_resolve_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0);
}
if (dfd < 0)
return NULL;
if ((fl = fcntl(dfd, F_GETFD)) >= 0)
fcntl(dfd, F_SETFD, fl | FD_CLOEXEC);
if (!(d = fdopendir(dfd))) {
int save = errno;
close(dfd);
errno = save;
}
return d;
}
#endif
/* This function is normally called by the sender, but the receiving side also
* calls it from get_dirlist() with f set to -1 so that we just construct the
* file list in memory without sending it over the wire. Also, get_dirlist()
@@ -2100,31 +1830,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
assert(flist != NULL);
#if defined HAVE_FDOPENDIR && defined HAVE_DIRFD
/* Confine the enumeration beneath the transfer root. secure_opendir()
* follows in-tree directory symlinks (RESOLVE_BENEATH) and refuses one that
* escapes, so it serves both modes:
* - a daemon/hardened sender (vfs_relpath_active()) is confined to the
* module in EVERY mode -- including -L/--copy-dirlinks/--copy-unsafe-
* links, matching the content open (sender_open_copylinks_confined) --
* so a following mode cannot be lured to enumerate outside the module;
* - a non-daemon sender is confined in the default no-follow mode; its
* symlink-following modes intentionally dereference out of the
* operator's own tree, so they keep the legacy opendir().
* f >= 0 is the sender's outgoing scan; get_dirlist() passes f < 0 and keeps
* the legacy opendir(). A module opted out of confinement ("insecure links =
* yes", admin-only) -- or a non-daemon --insecure-links -- uses the legacy
* opendir() too, restoring the pre-hardening enumeration (re-opening the
* escape; documented). */
if (f >= 0 && !vfs_symlink_optout_allowed() && (vfs_relpath_active()
|| !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)))
d = secure_opendir(fbuf);
else
d = opendir(fbuf);
#else
d = opendir(fbuf);
#endif
if (!d) {
if (!(d = opendir(fbuf))) {
if (errno == ENOENT) {
if (am_sender) /* Can abuse this for vanished error w/ENOENT: */
interpret_stat_error(fbuf, True);
@@ -2147,14 +1853,6 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
} else
remainder = 0;
#ifdef HAVE_DIRFD
/* Let the per-entry stat (readlink_stat -> scan_link_stat) go through the
* already-open directory fd instead of re-resolving fbuf for each name. */
scan_dirfd = dirfd(d);
scan_dir_prefix = fbuf;
scan_dir_prefix_len = len;
#endif
for (errno = 0, di = readdir(d); di; errno = 0, di = readdir(d)) {
unsigned name_len;
char *dname = d_name(di);
@@ -2183,9 +1881,6 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
send_file_name(f, flist, fbuf, NULL, flags, filter_level);
}
scan_dirfd = -1; /* fbuf is about to be reused / d closed */
scan_dir_prefix = NULL; /* and don't leave the global pointing into fbuf */
scan_dir_prefix_len = 0;
fbuf[len] = '\0';
if (errno) {
@@ -2319,8 +2014,7 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)
int len, dlen, flags = FLAG_DIVERT_DIRS | FLAG_CONTENT_DIR;
size_t j;
if (!f_name(file, fbuf))
return;
f_name(file, fbuf);
dlen = strlen(fbuf);
if (!change_pathname(file, NULL, 0))
@@ -2365,9 +2059,10 @@ static void send1extra(int f, struct file_struct *file, struct file_list *flist)
}
if (name_type != NORMAL_NAME) {
STRUCT_STAT st = {0};
if (name_type != MISSING_NAME && link_stat(fbuf, &st, 1) != 0) {
STRUCT_STAT st;
if (name_type == MISSING_NAME)
memset(&st, 0, sizeof st);
else if (link_stat(fbuf, &st, 1) != 0) {
interpret_stat_error(fbuf, True);
continue;
}
@@ -2499,7 +2194,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
static const char *lastdir;
static int lastdir_len = -1;
int len, dirlen;
STRUCT_STAT st = {0};
STRUCT_STAT st;
char *p, *dir;
struct file_list *flist;
struct timeval start_tv, end_tv;
@@ -2558,7 +2253,7 @@ struct file_list *send_file_list(int f, int argc, char *argv[])
}
if (!orig_dir)
orig_dir = strdup(vfs.curr_dir);
orig_dir = strdup(curr_dir);
while (1) {
char fbuf[MAXPATHLEN], *fn, name_type;
@@ -2890,33 +2585,11 @@ struct file_list *recv_file_list(int f, int dir_ndx)
#endif
if (inc_recurse && dir_ndx >= 0) {
if (!first_flist) {
/* All flists have already been freed via the NDX_DONE
* chain, so dir_flist is stale: its files[] entries
* point into a destroyed pool. A sub-flist marker now
* is a protocol violation (and would otherwise UAF the
* stale dir entry below, then deref an uninitialised
* slot in the freshly reset dir_flist further down). */
rprintf(FERROR_XFER,
"rsync: refusing sub-flist after final flist was freed\n");
exit_cleanup(RERR_PROTOCOL);
}
if (dir_ndx >= dir_flist->used) {
rprintf(FERROR_XFER, "rsync: refusing invalid dir_ndx %u >= %u\n", dir_ndx, dir_flist->used);
exit_cleanup(RERR_PROTOCOL);
}
struct file_struct *file = dir_flist->files[dir_ndx];
if (!F_IS_ACTIVE(file)) {
/* flist_sort_and_clean() can clear_file() a directory
* entry that was a duplicate or otherwise pruned, but
* the cleared file_struct stays in dir_flist. A peer
* that then sends a sub-flist for that slot would make
* f_name() return NULL into the dirname strcmp() below. */
rprintf(FERROR_XFER,
"rsync: refusing flist for cleared dir_ndx %d\n",
dir_ndx);
exit_cleanup(RERR_PROTOCOL);
}
if (file->flags & FLAG_GOT_DIR_FLIST) {
rprintf(FERROR_XFER, "rsync: refusing malicious duplicate flist for dir %d\n", dir_ndx);
exit_cleanup(RERR_PROTOCOL);
@@ -2945,7 +2618,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
if ((flags = read_varint(f)) == 0) {
int err = read_varint(f);
if (!ignore_errors)
io_error |= err & IOERR_VALID_MASK;
io_error |= err;
break;
}
} else {
@@ -2963,7 +2636,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
}
err = read_varint(f);
if (!ignore_errors)
io_error |= err & IOERR_VALID_MASK;
io_error |= err;
break;
}
}
@@ -2978,7 +2651,7 @@ struct file_list *recv_file_list(int f, int dir_ndx)
cur_dir++;
if (cur_dir != good_dirname) {
const char *d = dir_ndx >= 0 ? f_name(dir_flist->files[dir_ndx], NULL) : empty_dir;
if (!d || strcmp(cur_dir, d) != 0) {
if (strcmp(cur_dir, d) != 0) {
rprintf(FERROR,
"ABORTING due to invalid path from sender: %s/%s\n",
cur_dir, file->basename);
@@ -3066,17 +2739,9 @@ struct file_list *recv_file_list(int f, int dir_ndx)
/* Recv the io_error flag */
int err = read_int(f);
if (!ignore_errors)
io_error |= err & IOERR_VALID_MASK;
io_error |= err;
} else if (inc_recurse && flist->ndx_start == 1) {
/* The first inc_recurse flist has no parent in dir_flist; a
* malicious peer can send a "." entry whose mode is not a
* directory, so it never lands in dir_flist (used stays 0) yet
* the basename test below still passes. That left parent_ndx at
* its default 0 and the consumers dereferenced dir_flist->files[0]
* = uninitialised heap. Require dir_flist to actually hold an
* entry before trusting index 0. */
if (!file_total || !dir_flist->used
|| strcmp(flist->sorted[flist->low]->basename, ".") != 0)
if (!file_total || strcmp(flist->sorted[flist->low]->basename, ".") != 0)
flist->parent_ndx = -1;
}
@@ -3502,8 +3167,8 @@ static void output_flist(struct file_list *flist)
} else
*uidbuf = '\0';
if (gid_ndx) {
static const char parens[] = "(\0)\0\0\0";
const char *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3);
static char parens[] = "(\0)\0\0\0";
char *pp = parens + (file->flags & FLAG_SKIP_GROUP ? 0 : 3);
snprintf(gidbuf, sizeof gidbuf, " gid=%s%u%s",
pp, F_GROUP(file), pp + 2);
} else
+76 -562
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -57,6 +57,5 @@
printf("%lu", (unsigned long)gid);
printf("\n");
free(list);
return 0;
}
+10 -25
View File
@@ -19,7 +19,7 @@
#include "rsync.h"
#define HASH_LOAD_LIMIT(size) ((size)/4*3) /* /4 first: never overflows int */
#define HASH_LOAD_LIMIT(size) ((size)*3/4)
struct hashtable *hashtable_create(int size, int key64)
{
@@ -28,25 +28,15 @@ struct hashtable *hashtable_create(int size, int key64)
int node_size = key64 ? sizeof (struct ht_int64_node)
: sizeof (struct ht_int32_node);
/* Pick a power of 2 that can hold the requested size. Test size < 16 first
* so a negative/zero req short-circuits before the size-1 (INT_MIN is UB). */
if (size < 16 || (size & (size-1))) {
/* Pick a power of 2 that can hold the requested size. */
if (size & (size-1) || size < 16) {
size = 16;
while (size < req) {
if (size > INT_MAX/2) { /* the next doubling would overflow int */
rprintf(FERROR, "[%s] hashtable_create: requested size %d is too large\n",
who_am_i(), req);
exit_cleanup(RERR_MALLOC);
}
while (size < req)
size *= 2;
}
}
tbl = new(struct hashtable);
/* Pass size and node_size as SEPARATE factors so my_alloc's overflow /
* --max-alloc guard sees both; computing size*node_size as int would wrap to
* a tiny count and under-allocate (heap overflow on later node access). */
tbl->nodes = my_alloc(do_calloc, size, node_size, __FILE__, __LINE__);
tbl->nodes = new_array0(char, size * node_size);
tbl->size = size;
tbl->entries = 0;
tbl->node_size = node_size;
@@ -100,15 +90,10 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
if (data_when_new && tbl->entries > HASH_LOAD_LIMIT(tbl->size)) {
void *old_nodes = tbl->nodes;
int size, i;
int size = tbl->size * 2;
int i;
if (tbl->size > INT_MAX/2) { /* doubling would overflow int */
rprintf(FERROR, "[%s] hashtable grow: size overflow\n", who_am_i());
exit_cleanup(RERR_MALLOC);
}
size = tbl->size * 2;
/* Separate factors so my_alloc's guard sees both (see hashtable_create). */
tbl->nodes = my_alloc(do_calloc, size, tbl->node_size, __FILE__, __LINE__);
tbl->nodes = new_array0(char, size * tbl->node_size);
tbl->size = size;
tbl->entries = 0;
@@ -135,7 +120,7 @@ void *hashtable_find(struct hashtable *tbl, int64 key, void *data_when_new)
if (!key64) {
/* Based on Jenkins One-at-a-time hash. */
uchar buf[4] = {0}, *keyp = buf; /* {0} only to satisfy the analyzer (SIVALu fills buf) */
uchar buf[4], *keyp = buf;
int i;
SIVALu(buf, 0, key);
@@ -366,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)
{
+5 -29
View File
@@ -125,22 +125,8 @@ static void match_gnums(int32 *ndx_list, int ndx_count)
if (inc_recurse) {
node = hashtable_find(prior_hlinks, gnum, data_when_new);
if (node->data == data_when_new) {
if (gnum < hlink_flist->ndx_start) {
/* A non-first hard-link entry whose
* gnum points before this flist's
* ndx_start should already have been
* recorded in prior_hlinks by an
* earlier flist. A peer that sends
* such a back-reference on the first
* flist (or to a gnum that was never
* declared XMIT_HLINK_FIRST) is
* misbehaving. */
rprintf(FERROR,
"hard-link gnum %d precedes flist start %d\n",
(int)gnum, (int)hlink_flist->ndx_start);
exit_cleanup(RERR_PROTOCOL);
}
node->data = new_array0(char, 5);
assert(gnum >= hlink_flist->ndx_start);
file->flags |= FLAG_HLINK_FIRST;
prev = -1;
} else if (CVAL(node->data, 0) == 0) {
@@ -420,14 +406,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
}
break;
}
/* Content-based basis match only applies to regular
* files: for a hard-linked symlink/device/special the
* exact-inode check above is the only meaningful test,
* and quick_check_ok(FT_REG, ...) would read F_SUM()
* on a file_struct that has no SUM_EXTRA_CNT space
* (recv_file_entry only allocates it for S_ISREG). */
if (!S_ISREG(file->mode)
|| !quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))
if (!quick_check_ok(FT_REG, cmpbuf, file, &alt_sx.st))
continue;
statret = 1;
if (unchanged_attrs(cmpbuf, file, &alt_sx))
@@ -451,7 +430,7 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
if (preserve_xattrs) {
free_xattr(sxp);
if (!XATTR_READY(alt_sx))
get_xattr(cmpbuf, -1, sxp);
get_xattr(cmpbuf, sxp);
else {
sxp->xattr = alt_sx.xattr;
alt_sx.xattr = NULL;
@@ -473,12 +452,9 @@ int hard_link_check(struct file_struct *file, int ndx, char *fname,
}
int hard_link_one(struct file_struct *file, const char *fname,
const char *oldname, int terse, int vfs_flags)
const char *oldname, int terse)
{
/* oldname is the link source (vfs_flags carries its policy -- VFS_OPERATOR_PATH
* for an alt-dest basis on a non-daemon receiver, else 0); fname is the
* transfer destination, always under the secure receiver resolve. */
if (vfs_link_at(oldname, fname, vfs_flags, 0) < 0) {
if (do_link(oldname, fname) < 0) {
enum logcode code;
if (terse) {
if (!INFO_GTE(NAME, 1))
+69 -339
View File
@@ -31,15 +31,7 @@
#include "ifuncs.h"
#include "inums.h"
#include <poll.h>
/* Readiness bits we act on. poll() can report POLLERR/POLLHUP/POLLNVAL even
* when they were not requested, and POLLPRI stands in for select()'s old
* exception set. */
#define POLL_RD_BITS (POLLIN | POLLPRI | POLLERR | POLLHUP)
#define POLL_WR_BITS (POLLOUT | POLLERR | POLLHUP)
/** If no timeout is specified then use a 60 second I/O timeout */
/** If no timeout is specified then use a 60 second select timeout */
#define SELECT_TIMEOUT 60
extern int bwlimit;
@@ -67,7 +59,6 @@ extern int xfer_sum_len;
extern int daemon_connection;
extern int protocol_version;
extern int remove_source_files;
extern int write_batch;
extern int preserve_hard_links;
extern BOOL extra_flist_sending_enabled;
extern BOOL flush_ok_after_signal;
@@ -88,7 +79,6 @@ BOOL flist_receiving_enabled = False;
/* Ignore an EOF error if non-zero. See whine_about_eof(). */
int kluge_around_eof = 0;
int got_kill_signal = -1; /* is set to 0 only after multiplexed I/O starts */
volatile sig_atomic_t got_sigusr2 = 0; /* set by the async-signal-safe SIGUSR2 handler */
int sock_f_in = -1;
int sock_f_out = -1;
@@ -112,11 +102,6 @@ static struct {
static time_t last_io_in;
static time_t last_io_out;
/* Absolute wall-clock bound for peer-controlled daemon handshake reads.
* This is deliberately separate from io_timeout: the latter is an idle
* transfer timeout and may be supplied by the module or client. */
static time_t daemon_handshake_deadline;
static int write_batch_monitor_in = -1;
static int write_batch_monitor_out = -1;
@@ -128,43 +113,11 @@ static xbuf ff_xb = EMPTY_XBUF;
static xbuf iconv_buf = EMPTY_XBUF;
#endif
static int select_timeout = SELECT_TIMEOUT;
/* Turn select_timeout (in seconds) into a poll() millisecond count, keeping it
* positive and bounded. A negative count means "wait forever" to poll(), which
* would bypass our keepalives and timeout enforcement entirely. */
static int poll_timeout_ms(void)
{
int secs = select_timeout;
if (secs <= 0 || secs > SELECT_TIMEOUT)
secs = SELECT_TIMEOUT;
return secs * 1000;
}
static int handshake_poll_timeout_ms(void)
{
time_t now, left;
int timeout = poll_timeout_ms();
if (!daemon_handshake_deadline)
return timeout;
now = time(NULL);
left = daemon_handshake_deadline - now;
if (left <= 0) {
rprintf(FERROR, "[%s] daemon handshake timeout -- exiting\n", who_am_i());
exit_cleanup(RERR_TIMEOUT);
}
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
timeout = (int)left * 1000;
return timeout;
}
static int active_filecnt = 0;
static OFF_T active_bytecnt = 0;
static int first_message = 1;
static const char int_byte_extra[64] = {
static char int_byte_extra[64] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (00 - 3F)/4 */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* (40 - 7F)/4 */
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* (80 - BF)/4 */
@@ -267,15 +220,9 @@ static NORETURN void whine_about_eof(BOOL allow_kluge)
int i;
if (kluge_around_eof > 0)
exit_cleanup(0);
/* The receiver is waiting here for the generator's SIGUSR2; act on it
* (exit cleanly) the moment it arrives rather than sleeping the full
* 10s and then erroring. The async-signal-safe handler only sets the
* flag, so this loop must poll it. */
for (i = 10*1000/20; i--; ) {
if (got_sigusr2)
receive_sigusr2();
/* If we're still here after 10 seconds, exit with an error. */
for (i = 10*1000/20; i--; )
msleep(20);
}
}
rprintf(FERROR, RSYNC_NAME ": connection unexpectedly closed "
@@ -296,35 +243,31 @@ static size_t safe_read(int fd, char *buf, size_t len)
assert(fd != iobuf.in_fd);
while (1) {
struct pollfd pfd;
struct timeval tv;
fd_set r_fds, e_fds;
int cnt;
if (got_sigusr2) /* receiver told to wrap up (e.g. a --read-batch fd) */
receive_sigusr2();
FD_ZERO(&r_fds);
FD_SET(fd, &r_fds);
FD_ZERO(&e_fds);
FD_SET(fd, &e_fds);
tv.tv_sec = select_timeout;
tv.tv_usec = 0;
/* We use poll() rather than select() so that a high-numbered fd
* (>= FD_SETSIZE) cannot overflow an fd_set bitmap. */
pfd.fd = fd;
pfd.events = POLLIN | POLLPRI;
pfd.revents = 0;
cnt = poll(&pfd, 1, handshake_poll_timeout_ms());
cnt = select(fd+1, &r_fds, NULL, &e_fds, &tv);
if (cnt <= 0) {
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
rsyserr(FERROR, errno, "safe_read poll failed");
if (cnt < 0 && errno == EBADF) {
rsyserr(FERROR, errno, "safe_read select failed");
exit_cleanup(RERR_FILEIO);
}
check_timeout(1, MSK_ALLOW_FLUSH);
continue;
}
/* An invalid fd is reported here rather than via poll()'s return. */
if (pfd.revents & POLLNVAL) {
rsyserr(FERROR, EBADF, "safe_read poll failed");
exit_cleanup(RERR_FILEIO);
}
/*if (FD_ISSET(fd, &e_fds))
rprintf(FINFO, "select exception on fd %d\n", fd); */
if (pfd.revents & POLL_RD_BITS) {
if (FD_ISSET(fd, &r_fds)) {
ssize_t n = read(fd, buf + got, len - got);
if (DEBUG_GTE(IO, 2)) {
rprintf(FINFO, "[%s] safe_read(%d)=%" SIZE_T_FMT_MOD "d\n",
@@ -372,9 +315,6 @@ static void safe_write(int fd, const char *buf, size_t len)
assert(fd != iobuf.out_fd);
if (got_sigusr2) /* receiver told to wrap up before this (batch) write */
receive_sigusr2();
n = write(fd, buf, len);
if ((size_t)n == len)
return;
@@ -392,21 +332,19 @@ static void safe_write(int fd, const char *buf, size_t len)
}
while (len) {
struct pollfd pfd;
struct timeval tv;
fd_set w_fds;
int cnt;
if (got_sigusr2) /* receiver told to wrap up (e.g. a --write-batch fd) */
receive_sigusr2();
FD_ZERO(&w_fds);
FD_SET(fd, &w_fds);
tv.tv_sec = select_timeout;
tv.tv_usec = 0;
/* poll() avoids the FD_SETSIZE limit that select() imposes. */
pfd.fd = fd;
pfd.events = POLLOUT;
pfd.revents = 0;
cnt = poll(&pfd, 1, poll_timeout_ms());
cnt = select(fd + 1, NULL, &w_fds, NULL, &tv);
if (cnt <= 0) {
if (cnt < 0 && errno != EINTR && errno != EAGAIN) {
rsyserr(FERROR, errno, "safe_write poll failed on %s", what_fd_is(fd));
if (cnt < 0 && errno == EBADF) {
rsyserr(FERROR, errno, "safe_write select failed on %s", what_fd_is(fd));
exit_cleanup(RERR_FILEIO);
}
if (io_timeout)
@@ -414,12 +352,7 @@ static void safe_write(int fd, const char *buf, size_t len)
continue;
}
if (pfd.revents & POLLNVAL) {
rsyserr(FERROR, EBADF, "safe_write poll failed on %s", what_fd_is(fd));
exit_cleanup(RERR_FILEIO);
}
if (pfd.revents & POLL_WR_BITS) {
if (FD_ISSET(fd, &w_fds)) {
n = write(fd, buf, len);
if (n < 0) {
if (errno == EINTR)
@@ -628,8 +561,9 @@ static void handle_kill_signal(BOOL flush_ok)
* unused raw data in the buf would prevent the reading of socket data. */
static char *perform_io(size_t needed, int flags)
{
struct pollfd pfds[3];
int cnt, max_fd, npfds, poll_timeout, in_pollpos, out_pollpos, ff_pollpos;
fd_set r_fds, e_fds, w_fds;
struct timeval tv;
int cnt, max_fd;
size_t empty_buf_len = 0;
xbuf *out;
char *data;
@@ -722,15 +656,13 @@ static char *perform_io(size_t needed, int flags)
}
max_fd = -1;
npfds = 0;
in_pollpos = out_pollpos = ff_pollpos = -1;
FD_ZERO(&r_fds);
FD_ZERO(&e_fds);
if (iobuf.in_fd >= 0 && iobuf.in.size - iobuf.in.len) {
if (!read_batch || batch_fd >= 0) {
pfds[npfds].fd = iobuf.in_fd;
pfds[npfds].events = POLLIN | POLLPRI;
pfds[npfds].revents = 0;
in_pollpos = npfds++;
FD_SET(iobuf.in_fd, &r_fds);
FD_SET(iobuf.in_fd, &e_fds);
}
if (iobuf.in_fd > max_fd)
max_fd = iobuf.in_fd;
@@ -738,14 +670,12 @@ static char *perform_io(size_t needed, int flags)
/* Only do more filesfrom processing if there is enough room in the out buffer. */
if (ff_forward_fd >= 0 && iobuf.out.size - iobuf.out.len > FILESFROM_BUFLEN*2) {
pfds[npfds].fd = ff_forward_fd;
pfds[npfds].events = POLLIN;
pfds[npfds].revents = 0;
ff_pollpos = npfds++;
FD_SET(ff_forward_fd, &r_fds);
if (ff_forward_fd > max_fd)
max_fd = ff_forward_fd;
}
FD_ZERO(&w_fds);
if (iobuf.out_fd >= 0) {
if (iobuf.raw_flushing_ends_before
|| (!iobuf.msg.len && iobuf.out.len > iobuf.out_empty_len && !(flags & PIO_NEED_MSGROOM))) {
@@ -785,18 +715,7 @@ static char *perform_io(size_t needed, int flags)
} else
out = NULL;
if (out) {
/* A direct daemon connection uses one fd for both
* directions; give it a single row with both events
* rather than two rows carrying different masks. */
if (in_pollpos >= 0 && iobuf.out_fd == iobuf.in_fd) {
pfds[in_pollpos].events |= POLLOUT;
out_pollpos = in_pollpos;
} else {
pfds[npfds].fd = iobuf.out_fd;
pfds[npfds].events = POLLOUT;
pfds[npfds].revents = 0;
out_pollpos = npfds++;
}
FD_SET(iobuf.out_fd, &w_fds);
if (iobuf.out_fd > max_fd)
max_fd = iobuf.out_fd;
}
@@ -830,20 +749,19 @@ static char *perform_io(size_t needed, int flags)
if (got_kill_signal > 0)
handle_kill_signal(True);
if (got_sigusr2)
receive_sigusr2();
if (extra_flist_sending_enabled) {
if (file_total - file_old_total < MAX_FILECNT_LOOKAHEAD && IN_MULTIPLEXED_AND_READY)
poll_timeout = 0;
tv.tv_sec = 0;
else {
extra_flist_sending_enabled = False;
poll_timeout = poll_timeout_ms();
tv.tv_sec = select_timeout;
}
} else
poll_timeout = poll_timeout_ms();
tv.tv_sec = select_timeout;
tv.tv_usec = 0;
cnt = poll(pfds, npfds, poll_timeout);
cnt = select(max_fd + 1, &r_fds, &w_fds, &e_fds, &tv);
if (cnt <= 0) {
if (cnt < 0 && errno == EBADF) {
@@ -856,29 +774,11 @@ static char *perform_io(size_t needed, int flags)
extra_flist_sending_enabled = !flist_eof;
} else
check_timeout((flags & PIO_NEED_INPUT) != 0, 0);
/* Just in case... */
if (in_pollpos >= 0)
pfds[in_pollpos].revents = 0;
if (ff_pollpos >= 0)
pfds[ff_pollpos].revents = 0;
if (out_pollpos >= 0)
pfds[out_pollpos].revents = 0;
FD_ZERO(&r_fds); /* Just in case... */
FD_ZERO(&w_fds);
}
if (cnt > 0) {
/* poll() reports a bad fd here, not via its return value. */
int p;
for (p = 0; p < npfds; p++) {
if (pfds[p].revents & POLLNVAL) {
msgs2stderr = 1;
rsyserr(FERROR, EBADF, "perform_io: poll reported an invalid fd");
exit_cleanup(RERR_SOCKETIO);
}
}
}
if (iobuf.in_fd >= 0 && in_pollpos >= 0
&& pfds[in_pollpos].revents & POLL_RD_BITS) {
if (iobuf.in_fd >= 0 && FD_ISSET(iobuf.in_fd, &r_fds)) {
size_t len, pos = iobuf.in.pos + iobuf.in.len;
ssize_t n;
if (pos >= iobuf.in.size) {
@@ -927,7 +827,7 @@ static char *perform_io(size_t needed, int flags)
exit_cleanup(RERR_TIMEOUT);
}
if (out && out_pollpos >= 0 && pfds[out_pollpos].revents & POLL_WR_BITS) {
if (out && FD_ISSET(iobuf.out_fd, &w_fds)) {
size_t len = iobuf.raw_flushing_ends_before ? iobuf.raw_flushing_ends_before - out->pos : out->len;
ssize_t n;
@@ -978,8 +878,6 @@ static char *perform_io(size_t needed, int flags)
if (got_kill_signal > 0)
handle_kill_signal(True);
if (got_sigusr2)
receive_sigusr2();
/* We need to help prevent deadlock by doing what reading
* we can whenever we are here trying to write. */
@@ -990,8 +888,7 @@ static char *perform_io(size_t needed, int flags)
wait_for_receiver(); /* generator only */
}
if (ff_forward_fd >= 0 && ff_pollpos >= 0
&& pfds[ff_pollpos].revents & POLL_RD_BITS) {
if (ff_forward_fd >= 0 && FD_ISSET(ff_forward_fd, &r_fds)) {
/* This can potentially flush all output and enable
* multiplexed output, so keep this last in the loop
* and be sure to not cache anything that would break
@@ -1003,8 +900,6 @@ static char *perform_io(size_t needed, int flags)
if (got_kill_signal > 0)
handle_kill_signal(True);
if (got_sigusr2)
receive_sigusr2();
data = iobuf.in.buf + iobuf.in.pos;
@@ -1175,30 +1070,17 @@ void send_msg_int(enum msgcode code, int num)
void send_msg_success(const char *fname, int num)
{
/* Batch-only mode has not duplicated anything on the receiving side yet.
* The receiver still reports success to the generator for file-list and
* hard-link bookkeeping, but the generator must not turn that status into
* sender-side removal. */
if (am_generator && write_batch < 0 && remove_source_files)
return;
if (local_server) {
STRUCT_STAT st;
if (DEBUG_GTE(IO, 1))
rprintf(FINFO, "[%s] send_msg_success(%d)\n", who_am_i(), num);
/* The dev/ino is consumed only by the sender's --remove-source-files
* same-file safety check (successful_send), so skip the per-file
* stat entirely otherwise -- it's sent but never read. */
if (remove_source_files && stat(fname, &st) == 0) {
SIVAL64(num_dev_ino_buf, 4, st.st_dev);
SIVAL64(num_dev_ino_buf, 4+8, st.st_ino);
} else {
SIVAL64(num_dev_ino_buf, 4, 0);
SIVAL64(num_dev_ino_buf, 4+8, 0);
}
if (stat(fname, &st) < 0)
memset(&st, 0, sizeof (STRUCT_STAT));
SIVAL(num_dev_ino_buf, 0, num);
SIVAL64(num_dev_ino_buf, 4, st.st_dev);
SIVAL64(num_dev_ino_buf, 4+8, st.st_ino);
send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);
} else
send_msg_int(MSG_SUCCESS, num);
@@ -1208,9 +1090,6 @@ static void got_flist_entry_status(enum festatus status, int ndx)
{
struct file_list *flist = flist_for_ndx(ndx, "got_flist_entry_status");
if (ndx < flist->ndx_start)
exit_cleanup(RERR_PROTOCOL);
if (remove_source_files) {
active_filecnt--;
active_bytecnt -= F_LENGTH(flist->files[ndx - flist->ndx_start]);
@@ -1221,7 +1100,7 @@ static void got_flist_entry_status(enum festatus status, int ndx)
switch (status) {
case FES_SUCCESS:
if (remove_source_files && write_batch >= 0) {
if (remove_source_files) {
if (local_server)
send_msg(MSG_SUCCESS, num_dev_ino_buf, sizeof num_dev_ino_buf, -1);
else
@@ -1265,26 +1144,8 @@ void io_set_sock_fds(int f_in, int f_out)
void set_io_timeout(int secs)
{
/* A negative timeout is meaningless; treat it as "no timeout" rather than
* letting it drive allowed_lull / select_timeout negative (a tight loop).
* (--timeout is parsed by options.c as a plain int, so it can be negative.) */
if (secs < 0)
secs = 0;
io_timeout = secs;
/* Compute ceil(io_timeout/2) in a wider type: io_timeout can be INT_MAX
* (a peer's MSG_IO_TIMEOUT -- now capped in read_a_msg() -- or an operator
* --timeout, which options.c parses unbounded), and a plain "io_timeout + 1"
* would overflow to a negative allowed_lull / select_timeout. poll() now
* takes a millisecond count where negative means "wait forever", so this
* would hang the process rather than spin it -- and it still fires a
* keepalive flood. poll_timeout_ms() clamps as well; keep both. */
allowed_lull = (int)(((int64)io_timeout + 1) / 2);
/* The generator and sender derive an int loop-check limit as
* allowed_lull * 5; keep allowed_lull small enough that that product can't
* overflow either. The cap is invisible to real use -- allowed_lull is the
* keep-alive half-interval and INT_MAX/5 seconds is over 13 years. */
if (allowed_lull > INT_MAX / 5)
allowed_lull = INT_MAX / 5;
allowed_lull = (io_timeout + 1) / 2;
if (!io_timeout || allowed_lull > SELECT_TIMEOUT)
select_timeout = SELECT_TIMEOUT;
@@ -1295,18 +1156,10 @@ void set_io_timeout(int secs)
allowed_lull = 0;
}
void set_daemon_handshake_timeout(int secs)
{
if (secs > 0)
daemon_handshake_deadline = time(NULL) + secs;
else
daemon_handshake_deadline = 0;
}
static void check_for_d_option_error(const char *msg)
{
static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz";
const char *colon;
static char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz";
char *colon;
int saw_d = 0;
if (*msg != 'r'
@@ -1436,23 +1289,8 @@ int read_line(int fd, char *buf, size_t bufsiz, int flags)
return s - buf;
}
/* Reverse safe_arg()'s backslash escaping of a daemon option arg, the way a
* remote shell un-escapes args for the ssh transport. In place; \X -> X. */
static void unbackslash_arg(char *s)
{
char *f = s, *t = s;
while (*f) {
if (*f == '\\' && f[1])
f++;
*t++ = *f++;
}
*t = '\0';
}
#define MAX_DAEMON_ARGS (MAX_ARGS * 16)
void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
int unescape, char ***argv_p, int *argc_p, char **request_p)
char ***argv_p, int *argc_p, char **request_p)
{
int maxargs = MAX_ARGS;
int dot_pos = 0, argc = 0, request_len = 0;
@@ -1474,11 +1312,6 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
if (read_line(f_in, buf, bufsiz, rl_flags) == 0)
break;
if (mod_name && argc >= MAX_DAEMON_ARGS - 1) {
rprintf(FERROR, "too many daemon arguments\n");
exit_cleanup(RERR_PROTOCOL);
}
if (argc == maxargs-1) {
maxargs += MAX_ARGS;
argv = realloc_array(argv, char *, maxargs);
@@ -1499,23 +1332,11 @@ void read_args(int f_in, char *mod_name, char *buf, size_t bufsiz, int rl_nulls,
glob_expand(buf, &argv, &argc, &maxargs);
} else {
p = strdup(buf);
/* An option arg the client escaped with safe_arg() (no
* remote shell un-escapes it for a daemon). File args
* after the dot are handled by glob_expand() below. */
if (unescape)
unbackslash_arg(p);
argv[argc++] = p;
if (*p == '.' && p[1] == '\0')
dot_pos = argc;
}
}
/* glob_expand()/glob_match() reserve glob.argc+1 slots -- room for the
* entry being added but not for this trailing NULL. A post-dot line
* whose " mod/" splits land argc on exactly maxargs (or any later
* ENSURE_MEMSPACE doubling boundary) would otherwise make the next
* store an 8-byte NULL write one slot past the argv allocation. */
if (argc >= maxargs)
argv = realloc_array(argv, char *, maxargs = argc + 1);
argv[argc] = NULL;
glob_expand(NULL, NULL, NULL, NULL);
@@ -1532,9 +1353,8 @@ BOOL io_start_buffering_out(int f_out)
if (iobuf.out.buf) {
if (iobuf.out_fd == -1)
iobuf.out_fd = f_out;
else if (iobuf.out_fd >= 0)
else
assert(f_out == iobuf.out_fd);
/* else out_fd == -2: peer already gone; leave it dead. */
return False;
}
@@ -1552,9 +1372,8 @@ BOOL io_start_buffering_in(int f_in)
if (iobuf.in.buf) {
if (iobuf.in_fd == -1)
iobuf.in_fd = f_in;
else if (iobuf.in_fd >= 0)
else
assert(f_in == iobuf.in_fd);
/* else in_fd == -2: peer already EOF'd; leave it dead. */
return False;
}
@@ -1703,26 +1522,16 @@ static void read_a_msg(void)
if (msg_bytes != 4)
goto invalid_msg;
val = raw_read_int();
val &= IOERR_VALID_MASK;
iobuf.in_multiplexed = 1;
io_error |= val;
if (am_receiver)
send_msg_int(MSG_IO_ERROR, val);
iobuf.in_multiplexed = 1;
break;
case MSG_IO_TIMEOUT:
if (msg_bytes != 4 || am_server || am_generator)
goto invalid_msg;
val = raw_read_int();
iobuf.in_multiplexed = 1;
/* The peer may only ask us to use a SHORTER timeout (a stricter cap); a
* non-positive value would disable our --timeout entirely, letting a
* malicious server hang the client indefinitely, so ignore it. A very
* large value (near INT_MAX) would overflow the (io_timeout + 1) / 2
* computation in set_io_timeout(), wrapping allowed_lull and
* select_timeout negative -- which poll() reads as "wait forever",
* hanging the client. Cap at 24 hours. */
if (val <= 0 || val > 86400)
break;
if (!io_timeout || io_timeout > val) {
if (INFO_GTE(MISC, 2))
rprintf(FINFO, "Setting --timeout=%d to match server\n", val);
@@ -1733,17 +1542,17 @@ static void read_a_msg(void)
/* Support protocol-30 keep-alive method. */
if (msg_bytes != 0)
goto invalid_msg;
iobuf.in_multiplexed = 1;
if (am_sender)
maybe_send_keepalive(time(NULL), MSK_ALLOW_FLUSH);
iobuf.in_multiplexed = 1;
break;
case MSG_DELETED:
if (msg_bytes >= sizeof data)
goto overflow;
if (am_generator) {
raw_read_buf(data, msg_bytes);
send_msg(MSG_DELETED, data, msg_bytes, 1);
iobuf.in_multiplexed = 1;
send_msg(MSG_DELETED, data, msg_bytes, 1);
break;
}
#ifdef ICONV_OPTION
@@ -1781,6 +1590,7 @@ static void read_a_msg(void)
} else
#endif
raw_read_buf(data, msg_bytes);
iobuf.in_multiplexed = 1;
/* A directory name was sent with the trailing null */
if (msg_bytes > 0 && !data[msg_bytes-1])
log_delete(data, S_IFDIR);
@@ -1788,7 +1598,6 @@ static void read_a_msg(void)
data[msg_bytes] = '\0';
log_delete(data, S_IFREG);
}
iobuf.in_multiplexed = 1;
break;
case MSG_SUCCESS:
if (msg_bytes != (local_server ? 4+8+8 : 4)) {
@@ -1810,11 +1619,11 @@ static void read_a_msg(void)
if (msg_bytes != 4)
goto invalid_msg;
val = raw_read_int();
iobuf.in_multiplexed = 1;
if (am_generator)
got_flist_entry_status(FES_NO_SEND, val);
else
send_msg_int(MSG_NO_SEND, val);
iobuf.in_multiplexed = 1;
break;
case MSG_ERROR_SOCKET:
case MSG_ERROR_UTF8:
@@ -2056,45 +1865,6 @@ int64 read_varlong(int f, uchar min_bytes)
return u.x;
}
/* Read an int32 and verify lo <= v <= hi. On out-of-range, abort with a
* protocol error naming "what". The bound is co-located with the read so it
* cannot be forgotten by a downstream user. */
int32 read_int_bounded(int f, int32 lo, int32 hi, const char *what)
{
int32 v = read_int(f);
if (v < lo || v > hi) {
rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n",
what, (long)v, (long)lo, (long)hi, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
return v;
}
/* As read_int_bounded but for varint-encoded values. */
int32 read_varint_bounded(int f, int32 lo, int32 hi, const char *what)
{
int32 v = read_varint(f);
if (v < lo || v > hi) {
rprintf(FERROR, "wire value %s out of range: %ld not in [%ld,%ld] [%s]\n",
what, (long)v, (long)lo, (long)hi, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
return v;
}
/* Read a varint that will be used as a size_t. Rejects negative values
* (which would wrap to ~SIZE_MAX) and values exceeding the supplied max. */
size_t read_varint_size(int f, size_t max, const char *what)
{
int32 v = read_varint(f);
if (v < 0 || (size_t)v > max) {
rprintf(FERROR, "wire size %s out of range: %ld > %lu [%s]\n",
what, (long)v, (unsigned long)max, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
return (size_t)v;
}
int64 read_longint(int f)
{
#if SIZEOF_INT64 >= 8
@@ -2201,42 +1971,12 @@ void read_sum_head(int f, struct sum_struct *sum)
(long)sum->count, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
/* Guard against integer overflow in downstream allocations sized by
* count*element_size. my_alloc uses divide-not-multiply so it is
* already wraparound-safe, but checking here gives a clearer error
* and also covers the (size_t)count * xfer_sum_len arithmetic that
* is performed *before* reaching my_alloc. */
if (xfer_sum_len > 0 && (size_t)sum->count > SIZE_MAX / (size_t)xfer_sum_len) {
rprintf(FERROR, "Invalid checksum count %ld (too large) [%s]\n",
(long)sum->count, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
if ((size_t)sum->count > SIZE_MAX / sizeof(struct sum_buf)) {
rprintf(FERROR, "Invalid checksum count %ld (sum_buf overflow) [%s]\n",
(long)sum->count, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
sum->blength = read_int(f);
if (sum->blength < 0 || sum->blength > max_blength) {
rprintf(FERROR, "Invalid block length %ld [%s]\n",
(long)sum->blength, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
if (sum->count && sum->blength == 0) {
rprintf(FERROR, "Invalid zero block length [%s]\n",
who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
#if SIZEOF_CAPITAL_OFF_T < 8
/* The append-mode callers compute (OFF_T)count * blength; on a 32-bit
* OFF_T that product can wrap even though both factors are individually
* in range, corrupting the lseek/loop bounds. Reject it early. */
if (sum->blength > 0 && sum->count > MAX_INT32 / sum->blength) {
rprintf(FERROR, "checksum count*blength overflows OFF_T [%s]\n",
who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
#endif
sum->s2length = protocol_version < 27 ? csum_length : (int)read_int(f);
if (sum->s2length < 0 || sum->s2length > xfer_sum_len) {
rprintf(FERROR, "Invalid checksum length %d [%s]\n",
@@ -2348,7 +2088,7 @@ void write_int(int f, int32 x)
void write_varint(int f, int32 x)
{
char b[5] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL initialising b[1..4] */
char b[5];
uchar bit;
int cnt;
@@ -2370,7 +2110,7 @@ void write_varint(int f, int32 x)
void write_varlong(int f, int64 x, uchar min_bytes)
{
char b[9] = {0}; /* {0} only to satisfy the analyzer: it doesn't model SIVAL64 initialising b[1..8] */
char b[9];
uchar bit;
int cnt = 8;
@@ -2551,7 +2291,6 @@ int32 read_ndx(int f)
{
static int32 prev_positive = -1, prev_negative = 1;
int32 *prev_ptr, num;
uint32 unum;
char b[4];
if (protocol_version < 30)
@@ -2571,20 +2310,11 @@ int32 read_ndx(int f)
b[3] = CVAL(b, 0) & ~0x80;
b[0] = b[1];
read_buf(f, b+1, 2);
unum = IVAL(b, 0);
num = IVAL(b, 0);
} else
unum = (UVAL(b,0)<<8) + UVAL(b,1) + (uint32)*prev_ptr;
num = (UVAL(b,0)<<8) + UVAL(b,1) + *prev_ptr;
} else
unum = UVAL(b, 0) + (uint32)*prev_ptr;
/* A peer-supplied index that overflows a signed int32 (used unchecked as a
* file-list index) is a protocol violation -- reject it here rather than
* relying on every downstream consumer to bounds-check. */
if (unum > (uint32)MAX_INT32) {
rprintf(FERROR, "Invalid file index: %lu [%s]\n",
(unsigned long)unum, who_am_i());
exit_cleanup(RERR_PROTOCOL);
}
num = (int32)unum;
num = UVAL(b, 0) + *prev_ptr;
*prev_ptr = num;
if (prev_ptr == &prev_negative)
num = -num;
+1 -1
View File
@@ -1 +1 @@
#define LATEST_YEAR "2026"
#define LATEST_YEAR "2025"
-451
View File
@@ -1,451 +0,0 @@
/*
* POSIX ACL get/set/delete via the generic xattr syscalls.
*
* POSIX ACLs are stored by the kernel as the "system.posix_acl_access" and
* "system.posix_acl_default" extended attributes, in a fixed little-endian
* wire format (see include/acl_ea.h in the acl package). By serializing that
* format ourselves and using fgetxattr/fsetxattr on a held O_NOFOLLOW fd -- or
* getxattrat/setxattrat(AT_SYMLINK_NOFOLLOW) on a dirfd+leaf -- we get a
* symlink-race-safe ACL primitive that also covers the *default* ACL, which
* libacl's fd API (acl_get_fd/acl_set_fd, access-only) cannot.
*
* This file knows nothing about rsync's globals or its internal ACL form: it
* speaks a neutral (tag, perm, id) entry array, which makes it directly
* comparable against the system libacl in the t_acl unit test.
*
* Copyright (C) 2026 Wayne Davison & the rsync project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, visit the http://fsf.org website.
*/
#include "rsync.h"
#include "acl.h"
#ifdef SUPPORT_ACL_FD
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h> /* AT_SYMLINK_NOFOLLOW */
#if defined HAVE_SYS_XATTR_H
#include <sys/xattr.h>
#elif defined HAVE_ATTR_XATTR_H
#include <attr/xattr.h>
#endif
#ifdef HAVE_XATTRAT_SYSCALLS
#include <sys/syscall.h>
/* Self-contained copy of the kernel's struct xattr_args (stable ABI: an
* 8-byte-aligned u64 pointer, then two u32s). Defined locally to avoid
* pulling <linux/xattr.h>, whose XATTR_* macros clash with <sys/xattr.h>. */
struct rsync_xattr_args {
uint64_t value __attribute__((aligned(8)));
uint32_t size;
uint32_t flags;
};
#endif
/* Linux 2.4 didn't have a distinct ENOATTR. */
#ifndef ENOATTR
#define ENOATTR ENODATA
#endif
#define ACL_XATTR_ACCESS "system.posix_acl_access"
#define ACL_XATTR_DEFAULT "system.posix_acl_default"
/* On-disk layout: a 4-byte LE version header followed by 8-byte LE entries. */
#define ACL_EA_VERSION 0x0002
#define ACL_EA_HDR_LEN 4
#define ACL_EA_ENT_LEN 8
/* === little-endian (de)serialization (host-endianness independent) === */
static void put_le16(unsigned char *p, uint16_t v)
{
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
}
static void put_le32(unsigned char *p, uint32_t v)
{
p[0] = (unsigned char)(v & 0xff);
p[1] = (unsigned char)((v >> 8) & 0xff);
p[2] = (unsigned char)((v >> 16) & 0xff);
p[3] = (unsigned char)((v >> 24) & 0xff);
}
static uint16_t get_le16(const unsigned char *p)
{
return (uint16_t)(p[0] | (p[1] << 8));
}
static uint32_t get_le32(const unsigned char *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static int is_named_tag(uint16_t tag)
{
return tag == RACL_USER || tag == RACL_GROUP;
}
/* Canonical order: tag ascending, then id ascending within a tag. This is
* the order libacl's __acl_reorder_obj_p() produces and what the kernel's
* validator expects (USER_OBJ, USER*, GROUP_OBJ, GROUP*, MASK, OTHER). */
static int ent_compare(const void *a, const void *b)
{
const rsync_acl_ent *x = a, *y = b;
if (x->tag != y->tag)
return x->tag < y->tag ? -1 : 1;
if (x->id != y->id)
return x->id < y->id ? -1 : 1;
return 0;
}
/* Serialize entries into a freshly-malloc'd xattr buffer (canonical order). */
static unsigned char *acl_to_xattr(const rsync_acl_ent *ents, int count, size_t *len_out)
{
size_t len = ACL_EA_HDR_LEN + (size_t)count * ACL_EA_ENT_LEN;
unsigned char *buf = malloc(len);
rsync_acl_ent *sorted = NULL;
unsigned char *p;
int i;
if (!buf)
return NULL;
if (count > 1) {
sorted = malloc((size_t)count * sizeof sorted[0]);
if (!sorted) {
free(buf);
return NULL;
}
memcpy(sorted, ents, (size_t)count * sizeof sorted[0]);
qsort(sorted, count, sizeof sorted[0], ent_compare);
ents = sorted;
}
put_le32(buf, ACL_EA_VERSION);
p = buf + ACL_EA_HDR_LEN;
for (i = 0; i < count; i++, p += ACL_EA_ENT_LEN) {
put_le16(p, ents[i].tag);
put_le16(p + 2, ents[i].perm);
put_le32(p + 4, is_named_tag(ents[i].tag) ? ents[i].id : RACL_UNDEFINED_ID);
}
if (sorted)
free(sorted);
*len_out = len;
return buf;
}
/* Parse an xattr buffer into a malloc'd entry array (canonical order). */
static int xattr_to_acl(const unsigned char *buf, size_t len,
rsync_acl_ent **out, int *count_out)
{
rsync_acl_ent *ents;
const unsigned char *p;
int n, i;
if (len < ACL_EA_HDR_LEN || (len - ACL_EA_HDR_LEN) % ACL_EA_ENT_LEN != 0
|| get_le32(buf) != ACL_EA_VERSION) {
errno = EINVAL;
return -1;
}
n = (int)((len - ACL_EA_HDR_LEN) / ACL_EA_ENT_LEN);
ents = n ? malloc((size_t)n * sizeof ents[0]) : NULL;
if (n && !ents)
return -1;
p = buf + ACL_EA_HDR_LEN;
for (i = 0; i < n; i++, p += ACL_EA_ENT_LEN) {
ents[i].tag = get_le16(p);
ents[i].perm = get_le16(p + 2);
ents[i].id = is_named_tag(ents[i].tag) ? get_le32(p + 4) : RACL_UNDEFINED_ID;
}
if (n > 1)
qsort(ents, n, sizeof ents[0], ent_compare);
*out = ents;
*count_out = n;
return 0;
}
/* === syscall dispatchers (fd-variant vs at-variant) === */
/* Pre-6.13 fallback for the dirfd+leaf at-variants: address the leaf as
* /proc/self/fd/<dirfd>/<leaf> and use the l*xattr (no-follow-leaf) calls. The
* /proc/self/fd/<dirfd> magic symlink resolves to the pinned parent inode -- a
* raced parent symlink cannot redirect it -- and l*xattr does not follow a raced
* leaf symlink, so this is race-safe without the Linux 6.13 *xattrat syscalls, as
* long as procfs is mounted. (`leaf` is a single component, <= NAME_MAX.)
* Returns 0 and fills `buf`, or -1 with ENAMETOOLONG. */
static int proc_fd_leaf_path(char *buf, size_t buflen, int dirfd, const char *leaf)
{
int n = snprintf(buf, buflen, "/proc/self/fd/%d/%s", dirfd, leaf);
if (n < 0 || (size_t)n >= buflen) {
errno = ENAMETOOLONG;
return -1;
}
return 0;
}
static ssize_t do_getxattr(int fd, int dirfd, const char *leaf,
const char *name, void *val, size_t size)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fgetxattr(fd, name, val, size);
#ifdef HAVE_XATTRAT_SYSCALLS
{
struct rsync_xattr_args args;
ssize_t ret;
args.value = (uint64_t)(uintptr_t)val;
args.size = (uint32_t)size;
args.flags = 0;
ret = syscall(SYS_getxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
if (ret != -1 || errno != ENOSYS)
return ret;
/* ENOSYS: kernel < 6.13 -- fall through to the /proc compat. */
}
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lgetxattr(p, name, val, size);
}
static int do_setxattr(int fd, int dirfd, const char *leaf,
const char *name, const void *val, size_t size)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fsetxattr(fd, name, val, size, 0);
#ifdef HAVE_XATTRAT_SYSCALLS
{
struct rsync_xattr_args args;
int ret;
args.value = (uint64_t)(uintptr_t)val;
args.size = (uint32_t)size;
args.flags = 0; /* replace */
ret = syscall(SYS_setxattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW,
name, &args, sizeof args);
if (ret != -1 || errno != ENOSYS)
return ret;
}
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lsetxattr(p, name, val, size, 0);
}
static int do_removexattr(int fd, int dirfd, const char *leaf, const char *name)
{
char p[MAXPATHLEN];
if (fd >= 0)
return fremovexattr(fd, name);
#ifdef HAVE_XATTRAT_SYSCALLS
{
int ret = syscall(SYS_removexattrat, dirfd, leaf, AT_SYMLINK_NOFOLLOW, name);
if (ret != -1 || errno != ENOSYS)
return ret;
}
#endif
if (proc_fd_leaf_path(p, sizeof p, dirfd, leaf) < 0)
return -1;
return lremovexattr(p, name);
}
/* Read the whole named xattr into a malloc'd buffer, growing as needed. */
static int read_full_xattr(int fd, int dirfd, const char *leaf,
const char *name, unsigned char **buf_out, size_t *len_out)
{
unsigned char *buf = NULL;
size_t size = 0;
int tries;
for (tries = 0; tries < 8; tries++) {
ssize_t n = do_getxattr(fd, dirfd, leaf, name, size ? buf : NULL, size);
if (n >= 0) {
if (size == 0) {
/* First call just learned the length. */
size = n ? (size_t)n : 1;
buf = malloc(size);
if (!buf)
return -1;
continue;
}
*buf_out = buf;
*len_out = (size_t)n;
return 0;
}
if (errno == ERANGE) { /* grew under us: re-probe the size */
if (buf)
free(buf);
buf = NULL;
size = 0;
continue;
}
if (buf)
free(buf);
return -1; /* ENODATA / EOPNOTSUPP / ENOSYS / ... in errno */
}
if (buf)
free(buf);
errno = ERANGE;
return -1;
}
/* === public API === */
static int acl_get_common(int fd, int dirfd, const char *leaf,
int want_default, rsync_acl_ent **entries, int *count)
{
const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;
unsigned char *buf;
size_t len;
int rc;
*entries = NULL;
*count = 0;
if (read_full_xattr(fd, dirfd, leaf, name, &buf, &len) < 0) {
if (errno == ENODATA || errno == ENOATTR)
return 0; /* no explicit ACL present */
return -1; /* EOPNOTSUPP / ENOSYS / real error */
}
rc = xattr_to_acl(buf, len, entries, count);
free(buf);
return rc;
}
int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count)
{
return acl_get_common(fd, -1, NULL, want_default, entries, count);
}
int xacl_get_at(int dirfd, const char *leaf, int want_default,
rsync_acl_ent **entries, int *count)
{
return acl_get_common(-1, dirfd, leaf, want_default, entries, count);
}
static int acl_set_common(int fd, int dirfd, const char *leaf,
int want_default, const rsync_acl_ent *ents, int count)
{
const char *name = want_default ? ACL_XATTR_DEFAULT : ACL_XATTR_ACCESS;
unsigned char *buf;
size_t len;
int rc, save_errno;
buf = acl_to_xattr(ents, count, &len);
if (!buf) {
errno = ENOMEM;
return -1;
}
rc = do_setxattr(fd, dirfd, leaf, name, buf, len);
save_errno = errno;
free(buf);
errno = save_errno;
return rc < 0 ? -1 : 0;
}
int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *ents, int count)
{
return acl_set_common(fd, -1, NULL, want_default, ents, count);
}
int xacl_set_at(int dirfd, const char *leaf, int want_default,
const rsync_acl_ent *ents, int count)
{
return acl_set_common(-1, dirfd, leaf, want_default, ents, count);
}
static int acl_del_default_common(int fd, int dirfd, const char *leaf)
{
if (do_removexattr(fd, dirfd, leaf, ACL_XATTR_DEFAULT) < 0) {
if (errno == ENODATA || errno == ENOATTR)
return 0; /* already absent: success, like acl_delete_def_file */
return -1;
}
return 0;
}
int xacl_del_default_fd(int fd)
{
return acl_del_default_common(fd, -1, NULL);
}
int xacl_del_default_at(int dirfd, const char *leaf)
{
return acl_del_default_common(-1, dirfd, leaf);
}
/* True iff /proc/self/fd magic symlinks are usable, so the dirfd+leaf at-variants
* work race-safely via the /proc compat on a pre-6.13 kernel. */
static int proc_self_fd_usable(void)
{
int dfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
char p[64];
int usable = 0;
if (dfd < 0)
return 0;
if (snprintf(p, sizeof p, "/proc/self/fd/%d/.", dfd) < (int)sizeof p) {
/* The probe attr is absent; the path resolving (any errno but
* ENOENT/ENOTDIR -- e.g. ENODATA/ENOTSUP/EACCES) means procfs gives us
* the magic fd-symlink we need. */
errno = 0;
lgetxattr(p, "user.rsync_acl_probe", NULL, 0);
usable = !(errno == ENOENT || errno == ENOTDIR);
}
close(dfd);
return usable;
}
int xacl_at_available(void)
{
static int avail = -1;
if (avail < 0) {
#ifdef HAVE_XATTRAT_SYSCALLS
/* Probe the *xattrat syscall directly (not via do_getxattr's /proc
* fallback): any errno other than ENOSYS means it is present (6.13+). */
struct rsync_xattr_args args;
args.value = 0;
args.size = 0;
args.flags = 0;
errno = 0;
syscall(SYS_getxattrat, AT_FDCWD, ".", AT_SYMLINK_NOFOLLOW,
"user.rsync_acl_probe", &args, sizeof args);
if (errno != ENOSYS) {
avail = 1;
return avail;
}
#endif
/* No *xattrat syscalls (pre-6.13, or a kernel built without them): the dirfd+leaf ACL ops
* are still race-safe via /proc/self/fd if procfs is mounted, closing
* the parent-symlink-race gap that otherwise forces the path-based set. */
avail = proc_self_fd_usable();
}
return avail;
}
#endif /* SUPPORT_ACL_FD */
-74
View File
@@ -1,74 +0,0 @@
/*
* POSIX ACL get/set/delete via the generic xattr syscalls, addressing the
* kernel "system.posix_acl_{access,default}" attributes directly so that the
* operation can be confined to a held O_NOFOLLOW fd (fsetxattr) or a
* dirfd+leaf with AT_SYMLINK_NOFOLLOW (setxattrat). This replaces the path-
* based libacl acl_*_file() calls on Linux, where those would re-resolve the
* path and could be redirected by a parent-component symlink race.
*
* Copyright (C) 2026 Wayne Davison & the rsync project
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, visit the http://fsf.org website.
*/
#ifdef SUPPORT_ACL_FD
#include <stdint.h>
/* A single logical POSIX ACL entry in host-native form. The tag values are
* the stable kernel ABI numbers (== the libacl ACL_* constants), so they map
* straight onto the on-disk e_tag without translation. */
typedef struct {
uint16_t tag; /* RACL_USER_OBJ / USER / GROUP_OBJ / GROUP / MASK / OTHER */
uint16_t perm; /* permission bits: read=4, write=2, execute=1 */
uint32_t id; /* uid/gid for USER/GROUP entries; RACL_UNDEFINED_ID otherwise */
} rsync_acl_ent;
#define RACL_USER_OBJ 0x01
#define RACL_USER 0x02
#define RACL_GROUP_OBJ 0x04
#define RACL_GROUP 0x08
#define RACL_MASK 0x10
#define RACL_OTHER 0x20
#define RACL_UNDEFINED_ID ((uint32_t)-1)
/* Read the access (want_default==0) or default (want_default!=0) ACL.
*
* On success returns 0 and sets *entries to a malloc()ed array of *count
* entries (the caller frees it with free(); *entries may be NULL when
* *count==0, which means "no explicit ACL present" -- e.g. ENODATA).
*
* On failure returns -1 with errno set. Callers distinguish:
* ENOTSUP/EOPNOTSUPP - this filesystem has no ACL support (may differ per fs)
* ENOSYS - the at-variant syscalls are unavailable on this kernel
* The fd-variant operates on a held, already-NOFOLLOW-opened descriptor. The
* at-variant resolves leaf relative to dirfd and never follows a leaf symlink. */
int xacl_get_fd(int fd, int want_default, rsync_acl_ent **entries, int *count);
int xacl_get_at(int dirfd, const char *leaf, int want_default, rsync_acl_ent **entries, int *count);
/* Write the given entries as the access/default ACL. The entries are emitted
* in canonical order; the kernel validates them (a malformed set -> EINVAL). */
int xacl_set_fd(int fd, int want_default, const rsync_acl_ent *entries, int count);
int xacl_set_at(int dirfd, const char *leaf, int want_default, const rsync_acl_ent *entries, int count);
/* Delete a directory's default ACL. A missing default ACL is success. */
int xacl_del_default_fd(int fd);
int xacl_del_default_at(int dirfd, const char *leaf);
/* Cached runtime probe: are the *xattrat syscalls usable on this kernel?
* Returns 0 when they are absent (so callers can fall back) or unbuilt. */
int xacl_at_available(void);
#endif /* SUPPORT_ACL_FD */
+1 -3
View File
@@ -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:
+1 -1
View File
@@ -197,7 +197,7 @@ void md5_update(md_context *ctx, const uchar *input, uint32 length)
memcpy(ctx->buffer + left, input, length);
}
static const uchar md5_padding[CSUM_CHUNK] = { 0x80 };
static uchar md5_padding[CSUM_CHUNK] = { 0x80 };
void md5_result(md_context *ctx, uchar digest[MD5_DIGEST_LEN])
{
+2 -2
View File
@@ -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 -44
View File
@@ -44,32 +44,6 @@ struct align_test {
#define PTR_ADD(b,o) ( (void*) ((char*)(b) + (o)) )
#define PTR_SUB(b,o) ( (void*) ((char*)(b) - (o)) )
/* Under AddressSanitizer, fence each pool_alloc() chunk with a poisoned
* redzone just below it (allocations grow downward from the top of an extent).
* A bump allocator hands out chunks from one big malloc, so ASan cannot see a
* write that underflows one chunk into its neighbour -- e.g. a miscomputed
* F_SUM() reaching before a file_struct's extras. The redzone turns that into
* a hard ASan report. We unpoison a whole extent whenever its space is reused
* (reset/reclaim), so legitimate later allocations never trip over old
* redzones; ASan unpoisons freed extents itself via free(). */
#if defined(__SANITIZE_ADDRESS__)
# define POOL_ASAN 1
#elif defined(__has_feature)
# if __has_feature(address_sanitizer)
# define POOL_ASAN 1
# endif
#endif
#ifdef POOL_ASAN
# include <sanitizer/asan_interface.h>
# define POOL_REDZONE 16 /* >= the largest pool-relative underflow we guard */
# define POOL_POISON(p,n) ASAN_POISON_MEMORY_REGION((p), (n))
# define POOL_UNPOISON(p,n) ASAN_UNPOISON_MEMORY_REGION((p), (n))
#else
# define POOL_POISON(p,n) ((void)0)
# define POOL_UNPOISON(p,n) ((void)0)
#endif
alloc_pool_t
pool_create(size_t size, size_t quantum, void (*bomb)(const char*, const char*, int), int flags)
{
@@ -191,18 +165,7 @@ pool_alloc(alloc_pool_t p, size_t len, const char *bomb_msg)
pool->extents->free -= len;
{
void *ret = PTR_ADD(pool->extents->start, pool->extents->free);
#ifdef POOL_ASAN
size_t rz = pool->extents->free < POOL_REDZONE
? pool->extents->free : POOL_REDZONE;
if (rz) {
pool->extents->free -= rz;
POOL_POISON(PTR_ADD(pool->extents->start, pool->extents->free), rz);
}
#endif
return ret;
}
return PTR_ADD(pool->extents->start, pool->extents->free);
bomb_out:
if (pool->bomb)
@@ -252,10 +215,6 @@ pool_free(alloc_pool_t p, size_t len, void *addr)
if (!cur)
return;
/* This extent's space may be reused (and POOL_CLEAR may memset it)
* below, so drop any redzones in it first. */
POOL_UNPOISON(cur->start, pool->size);
if (!prev) {
/* The "live" extent is kept ready for more allocations. */
if (cur->free + cur->bound + len >= pool->size) {
@@ -313,8 +272,6 @@ pool_free_old(alloc_pool_t p, void *addr)
if (!cur)
return;
POOL_UNPOISON(cur->start, pool->size);
if (addr == PTR_ADD(cur->start, cur->free)) {
if (prev) {
prev->next = NULL;
+7 -151
View File
@@ -180,26 +180,6 @@ int sys_acl_free_acl(SMB_ACL_T the_acl)
return acl_free(the_acl);
}
#ifdef HAVE_LIBACL_AT
/* Dirfd/AT-flag ACL ops via the new libacl,
* race-safe on every Linux kernel. at_flags is AT_SYMLINK_NOFOLLOW (dirfd+leaf)
* or AT_EMPTY_PATH (operate on an open fd passed as dirfd, path ""). */
SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type)
{
return acl_get_file_at(dirfd, path_p, at_flags, type);
}
int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl)
{
return acl_set_file_at(dirfd, path_p, at_flags, type, theacl);
}
int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags)
{
return acl_delete_def_file_at(dirfd, path_p, at_flags);
}
#endif /* HAVE_LIBACL_AT */
#elif defined(HAVE_TRU64_ACLS) /*--------------------------------------------*/
/*
* The interface to DEC/Compaq Tru64 UNIX ACLs
@@ -499,20 +479,12 @@ SMB_ACL_T sys_acl_get_file(const char *path_p, SMB_ACL_TYPE_T type)
return acl_d;
}
#ifdef HAVE_SOLARIS_ACLS
/* facl(2)-based ACL read on a held fd (no path re-resolution). Solaris stores
* the access and default ACLs as one combined ACL; split out the requested half. */
SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type)
#if 0
SMB_ACL_T sys_acl_get_fd(int fd)
{
SMB_ACL_T acl_d;
int count; /* # of ACL entries allocated */
int naccess; /* # of access ACL entries */
int ndefault; /* # of default ACL entries */
if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {
errno = EINVAL;
return NULL;
}
count = INITIAL_ACL_SIZE;
if ((acl_d = sys_acl_init(count)) == NULL) {
@@ -539,39 +511,17 @@ SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type)
}
/*
* calculate the number of access and default ACL entries
* calculate the number of access ACL entries
*/
for (naccess = 0; naccess < count; naccess++) {
if (acl_d->acl[naccess].a_type & ACL_DEFAULT)
break;
}
ndefault = count - naccess;
if (type == SMB_ACL_TYPE_DEFAULT) {
int i, j;
/*
* Default ACL entries follow the access entries in the combined
* Solaris ACL; move them to the front of the wrapper and clear
* ACL_DEFAULT so the caller sees a plain default ACL.
*/
for (i = 0, j = naccess; i < ndefault; i++, j++) {
acl_d->acl[i] = acl_d->acl[j];
acl_d->acl[i].a_type &= ~ACL_DEFAULT;
}
acl_d->count = ndefault;
} else {
acl_d->count = naccess;
}
acl_d->count = naccess;
return acl_d;
}
SMB_ACL_T sys_acl_get_fd(int fd)
{
return sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);
}
#endif
int sys_acl_get_info(SMB_ACL_ENTRY_T entry, SMB_ACL_TAG_T *tag_type_p, uint32 *bits_p, id_t *u_g_id_p)
@@ -778,108 +728,14 @@ int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)
return ret;
}
#ifdef HAVE_SOLARIS_ACLS
/* facl(2)-based ACL write on a held fd (no path re-resolution). Setting an ACL
* on a directory replaces the combined access+default set, so for a dir read the
* other half through the fd, merge, and write the combined ACL back. Mirrors the
* path-based sys_acl_set_file() below. */
int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T acl_d)
#if 0
int sys_acl_set_fd(int fd, SMB_ACL_T acl_d)
{
struct stat s;
struct acl *acl_p;
int acl_count;
struct acl *acl_buf = NULL;
int ret;
if (type != SMB_ACL_TYPE_ACCESS && type != SMB_ACL_TYPE_DEFAULT) {
errno = EINVAL;
return -1;
}
if (acl_sort(acl_d) != 0) {
return -1;
}
acl_p = &acl_d->acl[0];
acl_count = acl_d->count;
if (fstat(fd, &s) != 0) {
return -1;
}
if (S_ISDIR(s.st_mode)) {
SMB_ACL_T acc_acl;
SMB_ACL_T def_acl;
SMB_ACL_T tmp_acl;
int i;
if (type == SMB_ACL_TYPE_ACCESS) {
acc_acl = acl_d;
def_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_DEFAULT);
} else {
def_acl = acl_d;
acc_acl = tmp_acl = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS);
}
if (tmp_acl == NULL) {
return -1;
}
acl_count = acc_acl->count + def_acl->count;
acl_p = acl_buf = SMB_MALLOC_ARRAY(struct acl, acl_count);
if (acl_buf == NULL) {
sys_acl_free_acl(tmp_acl);
errno = ENOMEM;
return -1;
}
/* Concatenate access + default, then mark the default half. */
memcpy(&acl_buf[0], &acc_acl->acl[0],
acc_acl->count * sizeof acl_buf[0]);
memcpy(&acl_buf[acc_acl->count], &def_acl->acl[0],
def_acl->count * sizeof acl_buf[0]);
for (i = acc_acl->count; i < acl_count; i++) {
acl_buf[i].a_type |= ACL_DEFAULT;
}
sys_acl_free_acl(tmp_acl);
} else if (type != SMB_ACL_TYPE_ACCESS) {
errno = EINVAL;
return -1;
}
ret = facl(fd, SETACL, acl_count, acl_p);
SAFE_FREE(acl_buf);
return ret;
}
int sys_acl_set_fd(int fd, SMB_ACL_T acl_d)
{
return sys_acl_set_fd_type(fd, SMB_ACL_TYPE_ACCESS, acl_d);
}
int sys_acl_delete_def_fd(int fd)
{
SMB_ACL_T acl_d;
int ret;
/*
* Fetching the access ACL through the fd and rewriting it deletes the
* default ACL, without re-resolving the path.
*/
if ((acl_d = sys_acl_get_fd_type(fd, SMB_ACL_TYPE_ACCESS)) == NULL) {
return -1;
}
ret = facl(fd, SETACL, acl_d->count, acl_d->acl);
sys_acl_free_acl(acl_d);
return ret;
return facl(fd, SETACL, acl_d->count, &acl_d->acl[0]);
}
#endif
-11
View File
@@ -301,18 +301,7 @@ int sys_acl_valid(SMB_ACL_T theacl);
int sys_acl_set_file(const char *name, SMB_ACL_TYPE_T acltype, SMB_ACL_T theacl);
int sys_acl_set_fd(int fd, SMB_ACL_T theacl);
int sys_acl_delete_def_file(const char *name);
#ifdef HAVE_SOLARIS_ACLS
SMB_ACL_T sys_acl_get_fd_type(int fd, SMB_ACL_TYPE_T type);
int sys_acl_set_fd_type(int fd, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);
int sys_acl_delete_def_fd(int fd);
#endif
int sys_acl_free_acl(SMB_ACL_T the_acl);
int no_acl_syscall_error(int err);
#ifdef HAVE_LIBACL_AT
SMB_ACL_T sys_acl_get_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type);
int sys_acl_set_file_at(int dirfd, const char *path_p, int at_flags, SMB_ACL_TYPE_T type, SMB_ACL_T theacl);
int sys_acl_delete_def_file_at(int dirfd, const char *path_p, int at_flags);
#endif
#endif /* SUPPORT_ACLS */
+28 -153
View File
@@ -45,31 +45,16 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
return lsetxattr(path, name, value, size, 0);
}
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
{
return fsetxattr(filedes, name, value, size, 0);
}
int sys_lremovexattr(const char *path, const char *name)
{
return lremovexattr(path, name);
}
int sys_fremovexattr(int filedes, const char *name)
{
return fremovexattr(filedes, name);
}
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
return llistxattr(path, list, size);
}
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
{
return flistxattr(filedes, list, size);
}
#elif HAVE_OSX_XATTRS
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)
@@ -104,31 +89,16 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
return setxattr(path, name, value, size, 0, XATTR_NOFOLLOW);
}
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
{
return fsetxattr(filedes, name, value, size, 0, 0);
}
int sys_lremovexattr(const char *path, const char *name)
{
return removexattr(path, name, XATTR_NOFOLLOW);
}
int sys_fremovexattr(int filedes, const char *name)
{
return fremovexattr(filedes, name, 0);
}
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
return listxattr(path, list, size, XATTR_NOFOLLOW);
}
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
{
return flistxattr(filedes, list, size, 0);
}
#elif HAVE_FREEBSD_XATTRS
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size)
@@ -146,46 +116,27 @@ int sys_lsetxattr(const char *path, const char *name, const void *value, size_t
return extattr_set_link(path, EXTATTR_NAMESPACE_USER, name, value, size);
}
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
{
return extattr_set_fd(filedes, EXTATTR_NAMESPACE_USER, name, value, size);
}
int sys_lremovexattr(const char *path, const char *name)
{
return extattr_delete_link(path, EXTATTR_NAMESPACE_USER, name);
}
int sys_fremovexattr(int filedes, const char *name)
{
return extattr_delete_fd(filedes, EXTATTR_NAMESPACE_USER, name);
}
/* Turn the FreeBSD extattr_list_xx() output (a single length byte before each
* name, no '\0' terminator) into the series of null-terminated strings that the
* rest of rsync expects. Since the size is unchanged, transform in place.
* Shared by the path and fd list variants. */
static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len)
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
unsigned char keylen;
ssize_t off;
ssize_t off, len = extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size);
if (len <= 0 || size == 0)
if (len <= 0 || (size_t)len > size)
return len;
if ((size_t)len >= size) {
/* FreeBSD extattr_list_xx() returns 'size' as 'len' in case there are
more data available, truncating the output, we solve this by signalling
ERANGE in case len == size so that the code in xattrs.c will retry with
a bigger buffer */
errno = ERANGE;
return -1;
}
/* FreeBSD puts a single-byte length before each string, with no '\0'
* terminator. We need to change this into a series of null-terminted
* strings. Since the size is the same, we can simply transform the
* output in place. */
for (off = 0; off < len; off += keylen + 1) {
keylen = ((unsigned char*)list)[off];
if (off + keylen >= len) {
/* Should be impossible, but bugs happen! */
/* Should be impossible, but kernel bugs happen! */
errno = EINVAL;
return -1;
}
@@ -196,18 +147,6 @@ static ssize_t freebsd_list_finish(char *list, size_t size, ssize_t len)
return len;
}
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
return freebsd_list_finish(list, size,
extattr_list_link(path, EXTATTR_NAMESPACE_USER, list, size));
}
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
{
return freebsd_list_finish(list, size,
extattr_list_fd(filedes, EXTATTR_NAMESPACE_USER, list, size));
}
#elif HAVE_SOLARIS_XATTRS
static ssize_t read_xattr(int attrfd, void *buf, size_t buflen)
@@ -269,59 +208,29 @@ ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size)
return read_xattr(attrfd, value, size);
}
/* Write a datum to the already-opened attribute fd, closing it. Shared by the
* path- and fd-keyed setters below. */
static int write_xattr(int attrfd, const void *value, size_t size)
{
size_t bufpos;
int ret = 0, saved_errno = 0;
for (bufpos = 0; bufpos < size; ) {
ssize_t cnt = write(attrfd, (const char *)value + bufpos, size - bufpos);
if (cnt < 0) {
if (errno == EINTR)
continue;
ret = -1;
saved_errno = errno;
break;
}
if (cnt == 0) {
ret = -1;
saved_errno = EIO;
break;
}
bufpos += cnt;
}
/* Don't let close() clobber the write error; do report a close() failure. */
if (close(attrfd) < 0 && ret == 0)
return -1;
if (ret < 0 && saved_errno)
errno = saved_errno;
return ret;
}
int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size)
{
int attrfd;
size_t bufpos;
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
if ((attrfd = attropen(path, name, O_CREAT|O_TRUNC|O_WRONLY, mode)) < 0)
return -1;
return write_xattr(attrfd, value, size);
}
for (bufpos = 0; bufpos < size; ) {
ssize_t cnt = write(attrfd, (char*)value + bufpos, size);
if (cnt <= 0) {
if (cnt < 0 && errno == EINTR)
continue;
bufpos = -1;
break;
}
bufpos += cnt;
}
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size)
{
int attrfd;
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
close(attrfd);
if ((attrfd = openat(filedes, name, O_CREAT|O_TRUNC|O_WRONLY|O_XATTR, mode)) < 0)
return -1;
return write_xattr(attrfd, value, size);
return bufpos > 0 ? 0 : -1;
}
int sys_lremovexattr(const char *path, const char *name)
@@ -339,29 +248,18 @@ int sys_lremovexattr(const char *path, const char *name)
return ret;
}
int sys_fremovexattr(int filedes, const char *name)
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
int attrdirfd;
int ret;
if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0)
return -1;
ret = unlinkat(attrdirfd, name, 0);
close(attrdirfd);
return ret;
}
/* List the names in an already-opened attribute-dir fd, consuming it. Shared
* by the path- and fd-keyed listers below. */
static ssize_t list_xattr(int attrdirfd, char *list, size_t size)
{
DIR *dirp;
struct dirent *dp;
ssize_t ret = 0;
if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) {
errno = ENOTSUP;
return -1;
}
if ((dirp = fdopendir(attrdirfd)) == NULL) {
close(attrdirfd);
return -1;
@@ -389,34 +287,11 @@ static ssize_t list_xattr(int attrdirfd, char *list, size_t size)
}
closedir(dirp);
close(attrdirfd);
return ret;
}
ssize_t sys_llistxattr(const char *path, char *list, size_t size)
{
int attrdirfd;
if ((attrdirfd = attropen(path, ".", O_RDONLY)) < 0) {
errno = ENOTSUP;
return -1;
}
return list_xattr(attrdirfd, list, size);
}
ssize_t sys_flistxattr(int filedes, char *list, size_t size)
{
int attrdirfd;
if ((attrdirfd = openat(filedes, ".", O_RDONLY|O_XATTR, 0)) < 0) {
errno = ENOTSUP;
return -1;
}
return list_xattr(attrdirfd, list, size);
}
#else
#error You need to create xattr compatibility functions.
-3
View File
@@ -16,11 +16,8 @@
ssize_t sys_lgetxattr(const char *path, const char *name, void *value, size_t size);
ssize_t sys_fgetxattr(int filedes, const char *name, void *value, size_t size);
int sys_lsetxattr(const char *path, const char *name, const void *value, size_t size);
int sys_fsetxattr(int filedes, const char *name, const void *value, size_t size);
int sys_lremovexattr(const char *path, const char *name);
int sys_fremovexattr(int filedes, const char *name);
ssize_t sys_llistxattr(const char *path, char *list, size_t size);
ssize_t sys_flistxattr(int filedes, char *list, size_t size);
#else
+2 -15
View File
@@ -89,11 +89,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
p_ch = *++p;
/* FALLTHROUGH */
default:
/* iwildmatch() folds the text to lower case above; fold the pattern
* char too so matching is truly case-insensitive (not just text-side).
* Without this an upper-case "hosts deny" token fails OPEN. */
if (force_lower_case && ISUPPER(p_ch))
p_ch = tolower(p_ch);
if (t_ch != p_ch)
return FALSE;
continue;
@@ -155,8 +150,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
p_ch = *++p;
if (!p_ch)
return ABORT_ALL;
if (force_lower_case && ISUPPER(p_ch))
p_ch = tolower(p_ch);
if (t_ch == p_ch)
matched = TRUE;
} else if (p_ch == '-' && prev_ch && p[1] && p[1] != ']') {
@@ -166,8 +159,6 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
if (!p_ch)
return ABORT_ALL;
}
if (force_lower_case && ISUPPER(p_ch))
p_ch = tolower(p_ch);
if (t_ch <= p_ch && t_ch >= prev_ch)
matched = TRUE;
p_ch = 0; /* This makes "prev_ch" get set to 0. */
@@ -225,12 +216,8 @@ static int dowild(const uchar *p, const uchar *text, const uchar*const *a)
} else /* malformed [:class:] string */
return ABORT_ALL;
p_ch = 0; /* This makes "prev_ch" get set to 0. */
} else {
if (force_lower_case && ISUPPER(p_ch))
p_ch = tolower(p_ch);
if (t_ch == p_ch)
matched = TRUE;
}
} else if (t_ch == p_ch)
matched = TRUE;
} while (prev_ch = p_ch, (p_ch = *++p) != ']');
if (matched == special || t_ch == '/')
return FALSE;
+9 -128
View File
@@ -65,7 +65,7 @@ typedef enum {
struct enum_list {
int value;
const char *name;
char *name;
};
struct parm_struct {
@@ -73,7 +73,7 @@ struct parm_struct {
parm_type type;
parm_class class;
void *ptr;
const struct enum_list *enum_list;
struct enum_list *enum_list;
unsigned flags;
};
@@ -95,7 +95,7 @@ static item_list section_list = EMPTY_ITEM_LIST;
static int iSectionIndex = -1;
static BOOL bInGlobalSection = True;
static const struct enum_list enum_syslog_facility[] = {
static struct enum_list enum_syslog_facility[] = {
#ifdef LOG_AUTH
{ LOG_AUTH, "auth" },
#endif
@@ -164,81 +164,11 @@ static const struct enum_list enum_syslog_facility[] = {
/* Expand %VAR% references. Any unknown vars or unrecognized
* syntax leaves the raw chars unchanged. */
enum shell_quote_context {
SHELL_UNQUOTED,
SHELL_SINGLE_QUOTED,
SHELL_DOUBLE_QUOTED
};
/* Characters that can turn a substituted value into shell syntax rather than
* data, in any quoting context. Quoting alone cannot be relied on here:
* context-aware escaping is correct for exactly one level of shell parsing,
* and a hook such as `sh -c '... %RSYNC_USER_NAME% ...'` re-parses the word in
* a second shell that sees the value bare. Peer-supplied values carrying any
* of these are refused instead. */
static int shell_unsafe_value(const char *val)
{
const char *s;
for (s = val; *s; s++) {
/* '!' negates in command position (a hook `sh -c '%VAR% false'`
* becomes `! false` and reports success, inverting an access
* check); '~' is tilde-expanded; '{' and '}' brace-expand in
* bash and zsh. None of them execute anything on their own,
* which is why a set built from the obvious metacharacters
* missed them. */
if (strchr("'\"`$\\;&|<>()*?[]# !~{}", *s)
|| (unsigned char)*s < 0x20 || (unsigned char)*s == 0x7f)
return 1;
}
return 0;
}
static char *expand_vars_shell_escape(const char *val, int quote_context)
{
const char *s;
char *ret, *t;
/* A double-quoted value is deliberately BOTH backslash-escaped and
* wrapped in single quotes. The wrap is redundant for one level of
* shell parsing (and shows up as literal quotes in the value), but a
* hook such as `sh -c "... %RSYNC_USER_NAME% ..."` re-parses the word
* in a second shell, where the backslashes are already gone and only
* the quotes still protect it. */
size_t len = quote_context == SHELL_SINGLE_QUOTED ? 0 : 2;
for (s = val; *s; s++) {
if (quote_context == SHELL_DOUBLE_QUOTED
&& strchr("\\\"`$", *s))
len += 2;
else
len += *s == '\'' ? 4 : 1;
}
ret = new_array(char, len + 1);
t = ret;
if (quote_context != SHELL_SINGLE_QUOTED)
*t++ = '\'';
for (s = val; *s; s++) {
if (quote_context == SHELL_DOUBLE_QUOTED
&& strchr("\\\"`$", *s)) {
*t++ = '\\';
*t++ = *s;
} else if (*s == '\'') {
memcpy(t, "'\\''", 4);
t += 4;
} else
*t++ = *s;
}
if (quote_context != SHELL_SINGLE_QUOTED)
*t++ = '\'';
*t = '\0';
return ret;
}
static char *expand_vars(const char *str, int shell_escape)
static char *expand_vars(const char *str)
{
char *buf, *t;
const char *f;
int bufsize, quote_context = SHELL_UNQUOTED, escaped_char = 0;
int bufsize;
if (!str || !strchr(str, '%'))
return (char *)str; /* TODO change return value to const char* at some point. */
@@ -248,35 +178,13 @@ static char *expand_vars(const char *str, int shell_escape)
for (t = buf, f = str; bufsize && *f; ) {
if (*f == '%' && isUpper(f+1)) {
const char *percent = strchr(f+1, '%');
char *percent = strchr(f+1, '%');
if (percent && percent - f < bufsize) {
char *val;
strlcpy(t, f+1, percent - f);
val = getenv(t);
if (val) {
char *escaped = NULL;
int len;
/* %RSYNC_*% values originate from the peer request/args.
* When the result is fed to a shell-executed hook, escape it
* for the template's current shell quote context so a value
* containing shell metacharacters can't inject. For ordinary string
* params (path, uid, gid, ...) leave them verbatim --
* quoting there would corrupt the value (e.g. a documented
* `path = /home/%RSYNC_USER_NAME%` would become /home/'x'). */
if (shell_escape && strncmp(t, "RSYNC_", 6) == 0) {
if (shell_unsafe_value(val)) {
/* Fail closed: the hook may be an access
* check, so skipping it is not an option. */
rprintf(FLOG,
"refusing to run shell hook: %%%s%% holds a shell metacharacter\n",
t);
exit_cleanup(RERR_UNSUPPORTED);
}
val = escaped = expand_vars_shell_escape(val, quote_context);
}
len = strlcpy(t, val, bufsize+1);
if (escaped)
free(escaped);
int len = strlcpy(t, val, bufsize+1);
if (len > bufsize)
break;
bufsize -= len;
@@ -286,28 +194,6 @@ static char *expand_vars(const char *str, int shell_escape)
}
}
}
if (shell_escape) {
if (quote_context == SHELL_SINGLE_QUOTED) {
/* Nothing is special inside '...', not even a backslash;
* only the closing quote ends it. */
if (*f == '\'')
quote_context = SHELL_UNQUOTED;
} else if (escaped_char)
escaped_char = 0;
else if (*f == '\\')
escaped_char = 1;
else if (quote_context == SHELL_DOUBLE_QUOTED) {
/* A single quote inside "..." is literal and must not be
* taken as opening a single-quoted run -- doing so would
* de-sync the tracker and escape a later value for the
* wrong context. */
if (*f == '"')
quote_context = SHELL_UNQUOTED;
} else if (*f == '\'')
quote_context = SHELL_SINGLE_QUOTED;
else if (*f == '"')
quote_context = SHELL_DOUBLE_QUOTED;
}
*t++ = *f++;
bufsize--;
}
@@ -327,10 +213,7 @@ static char *expand_vars(const char *str, int shell_escape)
/* Each "char* foo" has an associated "BOOL foo_EXP" that tracks if the string has been expanded yet or not. */
/* NOTE: use this function and all the FN_{GLOBAL,LOCAL} ones WITHOUT a trailing semicolon! */
#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val, 0); val ## _EXP = True;} return val ? val : "";}
/* Variant for params whose expansion is fed to a shell-executed hook: quote
* %RSYNC_*% peer-controlled values to prevent shell injection. */
#define RETURN_EXPANDED_SHELL(val) {if (!val ## _EXP) {val = expand_vars(val, 1); val ## _EXP = True;} return val ? val : "";}
#define RETURN_EXPANDED(val) {if (!val ## _EXP) {val = expand_vars(val); val ## _EXP = True;} return val ? val : "";}
/* In this section all the functions that are used to access the
* parameters from the rest of the program are defined. */
@@ -346,8 +229,6 @@ static char *expand_vars(const char *str, int shell_escape)
#define FN_LOCAL_STRING(fn_name, val) \
char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED(iSECTION(i).val) else RETURN_EXPANDED(Vars.l.val)}
#define FN_LOCAL_STRING_SHELL(fn_name, val) \
char *fn_name(int i) {if (LP_SNUM_OK(i) && iSECTION(i).val) RETURN_EXPANDED_SHELL(iSECTION(i).val) else RETURN_EXPANDED_SHELL(Vars.l.val)}
#define FN_LOCAL_BOOL(fn_name, val) \
BOOL fn_name(int i) {return LP_SNUM_OK(i)? iSECTION(i).val : Vars.l.val;}
#define FN_LOCAL_CHAR(fn_name, val) \
@@ -529,7 +410,7 @@ static BOOL do_parameter(char *parmname, char *parmvalue)
break;
default:
/* expand any %VAR% strings now */
parmvalue = expand_vars(parmvalue, 0);
parmvalue = expand_vars(parmvalue);
break;
}
+18 -70
View File
@@ -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;
@@ -55,6 +54,7 @@ extern iconv_t ic_chck;
#ifdef ICONV_OPTION
extern iconv_t ic_recv;
#endif
extern char curr_dir[MAXPATHLEN];
extern char *full_module_path;
extern unsigned int module_dirlen;
extern char sender_file_sum[MAX_DIGEST_LEN];
@@ -119,20 +119,12 @@ static char const *rerr_name(int code)
return NULL;
}
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char);
static void logit(int priority, const char *buf)
{
if (logfile_was_closed)
logfile_reopen();
if (logfile_fp) {
/* Escape control chars in the message so an attacker-controlled
* filename can't inject terminal escapes into the log an admin later
* cat's (CWE-117); keep the trailing newline raw via end_char. */
int len = strlen(buf);
char trailing = len && (buf[len-1] == '\n' || buf[len-1] == '\r') ? buf[--len] : '\0';
fprintf(logfile_fp, "%s [%d] ", timestring(time(NULL)), (int)getpid());
filtered_fwrite(logfile_fp, buf, len, 0, 1, trailing);
fprintf(logfile_fp, "%s [%d] %s", timestring(time(NULL)), (int)getpid(), buf);
fflush(logfile_fp);
} else {
syslog(priority, "%s", buf);
@@ -161,15 +153,7 @@ static void syslog_init()
static void logfile_open(void)
{
mode_t old_umask = umask(022 | orig_umask);
/* --log-file/`log file =` are operator-supplied paths that may transit
* attacker-writable dirs; a planted symlink could redirect root's log
* into e.g. /root/.ssh/authorized_keys. Refuse symlinks not owned by
* uid 0 or our euid. */
int fd = vfs_open_owner_walk(logfile_name,
O_WRONLY | O_APPEND | O_CREAT, 0644, 0);
logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL;
if (!logfile_fp && fd >= 0)
close(fd);
logfile_fp = fopen(logfile_name, "a");
umask(old_umask);
if (!logfile_fp) {
int fopen_errno = errno;
@@ -238,7 +222,7 @@ void logfile_reopen(void)
}
}
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, int escape_c1, char end_char)
static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isprint, char end_char)
{
char outbuf[1024], *ob = outbuf;
const char *end = in_buf + in_len;
@@ -250,8 +234,7 @@ static void filtered_fwrite(FILE *f, const char *in_buf, int in_len, int use_isp
}
if ((in_buf < end - 4 && *in_buf == '\\' && in_buf[1] == '#'
&& isDigit(in_buf + 2) && isDigit(in_buf + 3) && isDigit(in_buf + 4))
|| (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' '
|| (escape_c1 && *(uchar*)in_buf >= 0x80 && *(uchar*)in_buf <= 0x9f))))
|| (*in_buf != '\t' && ((use_isprint && !isPrint(in_buf)) || *(uchar*)in_buf < ' ')))
ob += snprintf(ob, 6, "\\#%03o", *(uchar*)in_buf++);
else
*ob++ = *in_buf++;
@@ -289,12 +272,8 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
if (am_daemon > 0 && code != FCLIENT)
code = FLOG;
} else if (send_msgs_to_gen) {
/* Pass the message to our sibling in native charset. is_utf8
* may be set here if a malicious peer sends MSG_INFO/MSG_ERROR
* to a daemon receiver (read_a_msg passes !am_generator); the
* old assert(!is_utf8) made that a remotely-reachable abort.
* Forwarding the bytes raw is safe -- the generator's rwrite()
* gets is_utf8=0 and filtered_fwrite escapes non-printables. */
assert(!is_utf8);
/* Pass the message to our sibling in native charset. */
send_msg((enum msgcode)code, buf, len, 0);
return;
}
@@ -318,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;
@@ -398,7 +372,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
ierrno = errno;
if (outbuf.len) {
char trailing = inbuf.len ? '\0' : trailing_CR_or_NL;
filtered_fwrite(f, convbuf, outbuf.len, 0, 0, trailing);
filtered_fwrite(f, convbuf, outbuf.len, 0, trailing);
if (trailing) {
trailing_CR_or_NL = '\0';
fflush(f);
@@ -421,7 +395,7 @@ void rwrite(enum logcode code, const char *buf, int len, int is_utf8)
} else
#endif
{
filtered_fwrite(f, buf, len, !allow_8bit_chars, 0, trailing_CR_or_NL);
filtered_fwrite(f, buf, len, !allow_8bit_chars, trailing_CR_or_NL);
if (trailing_CR_or_NL)
fflush(f);
}
@@ -482,17 +456,11 @@ void rsyserr(enum logcode code, int errcode, const char *format, ...)
char buf[BIGPATHBUFLEN];
size_t len;
/* snprintf returns the would-have-been length on truncation, so
* each cumulative call must be guarded; if not, sizeof buf - len
* can underflow when promoted to size_t and the next call writes
* past the buffer. */
len = snprintf(buf, sizeof buf, RSYNC_NAME ": [%s] ", who_am_i());
if (len < sizeof buf) {
va_start(ap, format);
len += vsnprintf(buf + len, sizeof buf - len, format, ap);
va_end(ap);
}
va_start(ap, format);
len += vsnprintf(buf + len, sizeof buf - len, format, ap);
va_end(ap);
if (len < sizeof buf) {
len += snprintf(buf + len, sizeof buf - len,
@@ -525,17 +493,12 @@ void remember_initial_stats(void)
initial_data_written = total_data_written;
}
/* Size of log_formatted()'s per-escape "fmt" scratch buffer. log_format_has()
* must bound its width-digit scan to the same limit so the two parsers agree on
* where an escape letter falls (see the digit loop in each). */
#define LOG_FMT_SIZE 32
/* A generic logging routine for send/recv, with parameter substitiution. */
static void log_formatted(enum logcode code, const char *format, const char *op,
struct file_struct *file, const char *fname, int iflags,
const char *hlink)
{
char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[LOG_FMT_SIZE];
char buf[MAXPATHLEN+1024], buf2[MAXPATHLEN], fmt[32];
char *p, *s, *c;
const char *n;
size_t len, total;
@@ -647,7 +610,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
n = buf2;
} else if (am_daemon && *c != '/') {
pathjoin(buf2, sizeof buf2,
vfs.curr_dir + module_dirlen, c);
curr_dir + module_dirlen, c);
clean_fname(buf2, 0);
if (fmt[1]) {
strlcpy(c, buf2, MAXPATHLEN);
@@ -716,7 +679,7 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
case 'C':
n = NULL;
if (S_ISREG(file->mode)) {
if (always_checksum && !(iflags & ITEM_DELETED))
if (always_checksum)
n = sum_as_hex(file_sum_nni->num, F_SUM(file), 1);
else if (iflags & ITEM_TRANSFER)
n = sum_as_hex(xfer_sum_nni->num, sender_file_sum, 0);
@@ -781,9 +744,6 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
}
}
break;
case '%':
n = "%";
break;
}
/* "n" is the string to be inserted in place of this % code. */
@@ -827,33 +787,21 @@ static void log_formatted(enum logcode code, const char *format, const char *op,
int log_format_has(const char *format, char esc)
{
const char *p;
int width;
if (!format)
return 0;
for (p = format; (p = strchr(p, '%')) != NULL; ) {
for (p++; *p == '\''; p++) {} /*SHARED ITERATOR*/
/* Mirror log_formatted()'s width-digit scan exactly (c starts at
* fmt+1, so width starts at 1): both must stop at the same digit
* or they disagree on where the escape letter is, which for %C
* can leave sender_keeps_checksum unset and over-read F_SUM. */
width = 1;
if (*p == '-') {
if (*p == '-')
p++;
width++;
}
while (isDigit(p) && width < LOG_FMT_SIZE - 8) {
while (isDigit(p))
p++;
width++;
}
while (*p == '\'') p++;
if (!*p)
break;
if (*p == esc)
return 1;
if (*p == '%') /* %% is a literal '%', not the start of an escape */
p++;
}
return 0;
}
+45 -149
View File
@@ -31,9 +31,6 @@
#ifdef __TANDEM
#include <floss.h(floss_execlp)>
#endif
#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif
extern int dry_run;
extern int list_only;
@@ -51,7 +48,6 @@ extern int called_from_signal_handler;
extern int need_messages_from_generator;
extern int kluge_around_eof;
extern int got_xfer_error;
extern volatile sig_atomic_t got_sigusr2;
extern int old_style_args;
extern int msgs2stderr;
extern int module_id;
@@ -70,6 +66,7 @@ extern int protect_args;
extern int relative_paths;
extern int sanitize_paths;
extern int curr_dir_depth;
extern unsigned int curr_dir_len;
extern int module_id;
extern int rsync_port;
extern int whole_file;
@@ -105,6 +102,7 @@ extern char *password_file;
extern char *backup_dir;
extern char *copy_as;
extern char *tmpdir;
extern char curr_dir[MAXPATHLEN];
extern char backup_dir_buf[MAXPATHLEN];
extern char *basis_dir[MAX_BASIS_DIRS+1];
extern struct file_list *first_flist;
@@ -241,11 +239,11 @@ void write_del_stats(int f)
void read_del_stats(int f)
{
stats.deleted_files = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_files");
stats.deleted_files += stats.deleted_dirs = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_dirs");
stats.deleted_files += stats.deleted_symlinks = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_symlinks");
stats.deleted_files += stats.deleted_devices = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_devices");
stats.deleted_files += stats.deleted_specials = read_varint_bounded(f, 0, MAX_WIRE_DEL_STAT, "deleted_specials");
stats.deleted_files = read_varint(f);
stats.deleted_files += stats.deleted_dirs = read_varint(f);
stats.deleted_files += stats.deleted_symlinks = read_varint(f);
stats.deleted_files += stats.deleted_devices = read_varint(f);
stats.deleted_files += stats.deleted_specials = read_varint(f);
}
static void become_copy_as_user()
@@ -388,7 +386,7 @@ static void handle_stats(int f)
static void output_itemized_counts(const char *prefix, int *counts)
{
static char *const labels[] = { "reg", "dir", "link", "dev", "special" };
static char *labels[] = { "reg", "dir", "link", "dev", "special" };
char buf[1024], *pre = " (";
int j, len = 0;
int total = counts[0];
@@ -396,18 +394,9 @@ static void output_itemized_counts(const char *prefix, int *counts)
counts[0] -= counts[1] + counts[2] + counts[3] + counts[4];
for (j = 0; j < 5; j++) {
if (counts[j]) {
/* snprintf can return more than its size arg
* on truncation; keep len <= sizeof buf - 2 so
* the closing ')' and trailing NUL always
* have room and the next iteration's
* sizeof buf - len - 2 cannot underflow. */
if (len >= (int)sizeof buf - 2)
break;
len += snprintf(buf+len, sizeof buf - len - 2,
"%s%s: %s",
pre, labels[j], comma_num(counts[j]));
if (len > (int)sizeof buf - 2)
len = (int)sizeof buf - 2;
pre = ", ";
}
}
@@ -714,45 +703,41 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
dest_path = dot_dir_or_error();
if (daemon_filter_list.head) {
/* Collapse ".." for the NAME-based daemon filter check so a "../excluded"
* destination is matched by name, as stock rsync does on its sanitized
* arg. Done on a copy: the daemon exclude/filter is name-based (a symlink
* whose own name is not excluded is still followed -- see rsyncd.conf(5)
* "munge symlinks"), and the real dest_path is left for the resolver. */
char cleaned[MAXPATHLEN], *slash;
if (!sanitize_path(cleaned, dest_path, NULL, 0, SP_KEEP_DOT_DIRS))
strlcpy(cleaned, dest_path, sizeof cleaned);
slash = strrchr(cleaned, '/');
char *slash = strrchr(dest_path, '/');
if (slash && (slash[1] == '\0' || (slash[1] == '.' && slash[2] == '\0')))
*slash = '\0';
if ((*cleaned != '.' || cleaned[1] != '\0')
&& (check_filter(&daemon_filter_list, FLOG, cleaned, 0) < 0
|| check_filter(&daemon_filter_list, FLOG, cleaned, 1) < 0)) {
else
slash = NULL;
if ((*dest_path != '.' || dest_path[1] != '\0')
&& (check_filter(&daemon_filter_list, FLOG, dest_path, 0) < 0
|| check_filter(&daemon_filter_list, FLOG, dest_path, 1) < 0)) {
rprintf(FERROR, "ERROR: daemon has excluded destination \"%s\"\n",
dest_path);
exit_cleanup(RERR_FILESELECT);
}
if (slash)
*slash = '/';
}
/* See what currently exists at the destination. */
statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK);
statret = do_stat(dest_path, &st);
cp = strrchr(dest_path, '/');
trailing_slash = cp && !cp[1];
if (mkpath_dest_arg && statret < 0 && (cp || file_total > 1)) {
int save_errno = errno;
int ret = vfs_make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME, 0);
int ret = make_path(dest_path, file_total > 1 && !trailing_slash ? 0 : MKP_DROP_NAME);
if (ret < 0)
goto mkdir_error;
if (ret && (INFO_GTE(NAME, 1) || stdout_format_has_i)) {
if (cp && (file_total == 1 || trailing_slash))
if (file_total == 1 || trailing_slash)
*cp = '\0';
rprintf(FINFO, "created %d director%s for %s\n", ret, ret == 1 ? "y" : "ies", dest_path);
if (cp && (file_total == 1 || trailing_slash))
if (file_total == 1 || trailing_slash)
*cp = '/';
}
if (ret)
statret = vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK);
statret = do_stat(dest_path, &st);
else
errno = save_errno;
}
@@ -799,7 +784,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
exit_cleanup(RERR_SYNTAX);
}
if (vfs_mkdir(VFS_AT_FDCWD, dest_path, ACCESSPERMS, VFS_ALLOW_SYMLINK) != 0) {
if (do_mkdir(dest_path, ACCESSPERMS) != 0) {
mkdir_error:
rsyserr(FERROR, errno, "mkdir %s failed",
full_fname(dest_path));
@@ -838,16 +823,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
dest_path = "/";
*cp = '\0';
if (dry_run && mkpath_dest_arg && vfs_stat(VFS_AT_FDCWD, dest_path, &st, VFS_ALLOW_SYMLINK) < 0) {
/* --mkpath would have created this parent dir, but a dry run did
* not, so don't chdir into it; flag the destination as not yet
* present (as the dir-creation path above does) so the generator
* doesn't try to compare against the missing tree (#880). Only
* the missing-parent case is touched, so an ordinary file-to-file
* dry run still itemizes against an existing destination. */
dry_run++;
change_dir(dest_path, CD_SKIP_CHDIR);
} else if (!change_dir(dest_path, CD_NORMAL)) {
if (!change_dir(dest_path, CD_NORMAL)) {
rsyserr(FERROR, errno, "change_dir#3 %s failed",
full_fname(dest_path));
exit_cleanup(RERR_FILESELECT);
@@ -860,42 +836,35 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
/* This function checks on our alternate-basis directories. If we're in
* dry-run mode and the destination dir does not yet exist, we'll try to
* tweak any dest-relative paths to make them work for a dry-run (the
* destination dir must be in vfs.curr_dir[] when this function is called).
* destination dir must be in curr_dir[] when this function is called).
* We also warn about any arg that is non-existent or not a directory. */
static void check_alt_basis_dirs(void)
{
STRUCT_STAT st;
char *slash = strrchr(vfs.curr_dir, '/');
char *slash = strrchr(curr_dir, '/');
int j;
for (j = 0; j < basis_dir_cnt; j++) {
char *bdir = basis_dir[j];
assert(bdir != NULL); /* option-supplied root; never NULL */
int bd_len = strlen(bdir);
if (bd_len > 1 && bdir[bd_len-1] == '/')
bdir[--bd_len] = '\0';
/* Make a relative --link-dest/--copy-dest/--compare-dest absolute
* (vs the destination vfs.curr_dir). These are operator-trusted roots, so
* an absolute path makes the do_*_at() wrappers use plain resolution
* rather than reject an operator '..' outside the dest tree (e.g.
* --copy-dest=../to). Skipped when sanitize_paths already confined
* them; the dry_run>1 case keeps its leading-"../"-strip. */
if (*bdir != '/' && (dry_run > 1 || !sanitize_paths)) {
int len = vfs.curr_dir_len + 1 + bd_len + 1;
if (dry_run > 1 && *bdir != '/') {
int len = curr_dir_len + 1 + bd_len + 1;
char *new = new_array(char, len);
if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) {
if (slash && strncmp(bdir, "../", 3) == 0) {
/* We want to remove only one leading "../" prefix for
* the directory we couldn't create in dry-run mode:
* this ensures that any other ".." references get
* evaluated the same as they would for a live copy. */
*slash = '\0';
pathjoin(new, len, vfs.curr_dir, bdir + 3);
pathjoin(new, len, curr_dir, bdir + 3);
*slash = '/';
} else
pathjoin(new, len, vfs.curr_dir, bdir);
pathjoin(new, len, curr_dir, bdir);
basis_dir[j] = bdir = new;
}
if (vfs_stat(VFS_AT_FDCWD, bdir, &st, VFS_ALLOW_SYMLINK) < 0)
if (do_stat(bdir, &st) < 0)
rprintf(FWARNING, "%s arg does not exist: %s\n", alt_dest_opt(0), bdir);
else if (!S_ISDIR(st.st_mode))
rprintf(FWARNING, "%s arg is not a dir: %s\n", alt_dest_opt(0), bdir);
@@ -1023,7 +992,7 @@ static int do_recv(int f_in, int f_out, char *local_name)
int ret;
if (backup_dir_len > 1)
backup_dir_buf[backup_dir_len-1] = '\0';
ret = vfs_stat(VFS_AT_FDCWD, backup_dir_buf, &st, VFS_ALLOW_SYMLINK);
ret = do_stat(backup_dir_buf, &st);
if (ret != 0 || !S_ISDIR(st.st_mode)) {
if (ret == 0) {
rprintf(FERROR, "The backup-dir is not a directory: %s\n", backup_dir_buf);
@@ -1043,7 +1012,7 @@ static int do_recv(int f_in, int f_out, char *local_name)
if (tmpdir) {
STRUCT_STAT st;
int ret = vfs_stat(VFS_AT_FDCWD, tmpdir, &st, VFS_ALLOW_SYMLINK);
int ret = do_stat(tmpdir, &st);
if (ret < 0 || !S_ISDIR(st.st_mode)) {
if (ret == 0) {
rprintf(FERROR, "The temp-dir is not a directory: %s\n", tmpdir);
@@ -1110,13 +1079,11 @@ static int do_recv(int f_in, int f_out, char *local_name)
exit_cleanup(RERR_PROTOCOL);
}
/* Finally, we go to sleep until our parent tells us to wrap up
* with a USR2 signal. We sleep for a short time, as on some OSes
* a signal won't interrupt a sleep, then act on the flag the
* (async-signal-safe) handler set. */
while (!got_sigusr2)
/* Finally, we go to sleep until our parent kills us with a
* USR2 signal. We sleep for a short time, as on some OSes
* a signal won't interrupt a sleep! */
while (1)
msleep(20);
receive_sigusr2();
}
am_generator = 1;
@@ -1242,25 +1209,15 @@ static void do_server_recv(int f_in, int f_out, int argc, char *argv[])
char **dir_p;
filter_rule_list *elp = &daemon_filter_list;
/* Collapse ".." and strip the module-dir prefix to get the module-relative
* name, but keep a leading "/" for a "path = /" module (module_dirlen <= 1)
* so an absolute (module-rooted) filter rule still matches. */
char clean[MAXPATHLEN], *dir;
for (dir_p = basis_dir; *dir_p; dir_p++) {
if (!sanitize_path(clean, *dir_p, "/", 0, SP_DEFAULT))
strlcpy(clean, *dir_p, sizeof clean);
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
char *dir = *dir_p;
if (*dir == '/')
dir += module_dirlen;
if (check_filter(elp, FLOG, dir, 1) < 0)
goto options_rejected;
}
if (partial_dir && *partial_dir == '/') {
if (!sanitize_path(clean, partial_dir, "/", 0, SP_DEFAULT))
strlcpy(clean, partial_dir, sizeof clean);
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
if (check_filter(elp, FLOG, dir, 1) < 0)
goto options_rejected;
}
if (0) {
if (partial_dir && *partial_dir == '/'
&& check_filter(elp, FLOG, partial_dir + module_dirlen, 1) < 0) {
options_rejected:
rprintf(FERROR, "Your options have been rejected by the server.\n");
exit_cleanup(RERR_SYNTAX);
@@ -1294,17 +1251,6 @@ void start_server(int f_in, int f_out, int argc, char *argv[])
if (am_sender) {
keep_dirlinks = 0; /* Must be disabled on the sender. */
/* Mirror client_run()'s sender_keeps_checksum check: a daemon-
* as-sender with -c and a `log format` containing %C will read
* F_SUM(file) in log_formatted(), so make_file() must allocate
* SUM_EXTRA_CNT. Without this, F_SUM() reads past the pool slot
* and hex-encodes adjacent heap into the transfer log. */
if (always_checksum
&& (log_format_has(stdout_format, 'C')
|| log_format_has(logfile_format, 'C')))
sender_keeps_checksum = 1;
if (need_messages_from_generator)
io_start_multiplex_in(f_in);
else
@@ -1368,7 +1314,7 @@ int client_run(int f_in, int f_out, pid_t pid, int argc, char *argv[])
become_copy_as_user();
send_file_list(f_out, argc, argv);
flist = send_file_list(f_out, argc, argv);
if (DEBUG_GTE(FLIST, 3))
rprintf(FINFO,"file list sent\n");
@@ -1613,10 +1559,6 @@ static int start_client(int argc, char *argv[])
shell_user = shell_machine;
shell_machine = p+1;
}
if (*shell_machine == '-') {
rprintf(FERROR, "Invalid remote host: hostnames may not start with '-'.\n");
exit_cleanup(RERR_SYNTAX);
}
}
if (DEBUG_GTE(CMD, 2)) {
@@ -1658,26 +1600,11 @@ static void sigusr1_handler(UNUSED(int val))
exit_cleanup(RERR_SIGNAL1);
}
/* SIGUSR2 tells the receiver child to wrap up. A signal handler must be
* async-signal-safe, so it only sets a flag here; receive_sigusr2() does the
* actual summary + shutdown (which use stdio/malloc/close) at a safe point in
* the receiver's post-transfer wait loops (read_final_goodbye via perform_io,
* and the trailing sleep). */
static void sigusr2_handler(UNUSED(int val))
{
got_sigusr2 = 1;
}
void receive_sigusr2(void)
{
if (!am_server)
output_summary();
close_all();
#ifdef GCOV_COVERAGE
/* The receiver child exits with _exit() here, bypassing the gcov atexit
* flush; without this it writes no .gcda. */
{ extern void __gcov_dump(void); __gcov_dump(); }
#endif
if (got_xfer_error)
_exit(RERR_PARTIAL);
_exit(0);
@@ -1780,31 +1707,6 @@ static void unset_env_var(const char *var)
}
/* The symlink-race-safe path resolver (vfs_resolve_open) holds one open
* dirfd per path component while it walks a path, plus an ancestor-dirfd cache
* -- far more descriptors than legacy rsync's single open(). On a host with a
* low default soft limit (e.g. OpenBSD's 128) a deep tree can hit EMFILE.
* Raise the soft RLIMIT_NOFILE toward the hard limit (unprivileged, per
* process; inherited by the sender/generator/receiver forks and daemon
* children), but cap it: some systems set an enormous hard limit (2^20+) that
* we don't want to adopt wholesale. */
static void raise_fd_limit(void)
{
#if defined HAVE_GETRLIMIT && defined HAVE_SETRLIMIT && defined RLIMIT_NOFILE
struct rlimit rl;
rlim_t want = 4096; /* covers a MAXPATHLEN-deep walk + cache + headroom */
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
return;
if (want > rl.rlim_max)
want = rl.rlim_max; /* never exceed the (admin-set) hard limit */
if (rl.rlim_cur < want) { /* only ever raise, never lower an inherited limit */
rl.rlim_cur = want;
(void)setrlimit(RLIMIT_NOFILE, &rl); /* best-effort */
}
#endif
}
int main(int argc,char *argv[])
{
int ret;
@@ -1812,10 +1714,6 @@ int main(int argc,char *argv[])
raw_argc = argc;
raw_argv = argv;
vfs_init();
raise_fd_limit();
#ifdef HAVE_SIGACTION
# ifdef HAVE_SIGPROCMASK
sigset_t sigmask;
@@ -1845,9 +1743,7 @@ int main(int argc,char *argv[])
our_gid = MY_GID();
am_root = our_uid == ROOT_UID;
// DISPLAY should not be emptied unconditionally
if (!getenv("SSH_ASKPASS"))
unset_env_var("DISPLAY");
unset_env_var("DISPLAY");
#if defined USE_OPENSSL && defined SET_OPENSSL_CONF
#define TO_STR2(x) #x
@@ -1929,7 +1825,7 @@ int main(int argc,char *argv[])
if (am_server && protect_args) {
char buf[MAXPATHLEN];
protect_args = 2;
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, 0, &argv, &argc, NULL);
read_args(STDIN_FILENO, NULL, buf, sizeof buf, 1, &argv, &argc, NULL);
if (!parse_arguments(&argc, (const char ***) &argv)) {
option_error();
exit_cleanup(RERR_SYNTAX);
-41
View File
@@ -44,29 +44,6 @@ extern struct stats stats;
#define TRADITIONAL_TABLESIZE (1<<16)
/* The maximum number of same-weak-checksum candidates we will compare
* against at a single file offset before giving up and rolling forward a
* byte. A weak checksum that collides thousands of times (very common in
* disk/VM images, which contain large runs of identical blocks) would
* otherwise turn hash_search()'s inner loop into an O(file_size *
* chain_length) scan, pegging a CPU at 100% for hours with no apparent
* progress (issue #217).
*
* Concretely, a synthetic 40000-block basis whose blocks all share one weak
* checksum took ~18.4s to sync a 60KB source on a modern x86_64 box before
* this cap and ~0.7s after it -- and the unbounded cost grows with the
* square of the file size, which is what produced the multi-hour "hangs"
* reported against real multi-GB images.
*
* Capping the per-offset work keeps the search bounded; any block we skip
* over is simply sent as literal data, so the result is always correct --
* only the transfer size is (slightly) affected. This is purely a
* sender-side search limit: it changes no checksum, emitted byte, or
* protocol field, so a capped sender interoperates with any receiver. */
#ifndef MAX_CHAIN_LEN
#define MAX_CHAIN_LEN 1024
#endif
static uint32 tablesize;
static int32 *hash_table;
@@ -205,7 +182,6 @@ static void hash_search(int f,struct sum_struct *s,
int done_csum2 = 0;
uint32 hash_entry;
int32 i, *prev;
int32 chain_len = 0;
if (DEBUG_GTE(DELTASUM, 4)) {
rprintf(FINFO, "offset=%s sum=%04x%04x\n",
@@ -242,14 +218,6 @@ static void hash_search(int f,struct sum_struct *s,
if (sum != s->sums[i].sum1)
continue;
/* Bound the work spent on a single pathological hash
* bucket. If this weak checksum matches more than
* MAX_CHAIN_LEN records, stop scanning and treat this
* offset as a non-match (issue #217). The skipped data
* is sent literally, never corrupted. */
if (++chain_len > MAX_CHAIN_LEN)
break;
/* also make sure the two blocks are the same length */
l = (int32)MIN((OFF_T)s->blength, len-offset);
if (l != s->sums[i].len)
@@ -325,7 +293,6 @@ static void hash_search(int f,struct sum_struct *s,
&& (!updating_basis_file || s->sums[want_i].offset >= offset
|| s->sums[want_i].flags & SUMFLG_SAME_OFFSET)
&& sum == s->sums[want_i].sum1
&& l == s->sums[want_i].len
&& memcmp(sum2, sum2_at(s, want_i), s->s2length) == 0) {
/* we've found an adjacent match - the RLL coder
* will be happy */
@@ -403,14 +370,6 @@ void match_sums(int f, struct sum_struct *s, struct map_struct *buf, OFF_T len)
sum_init(xfer_sum_nni, checksum_seed);
if (append_mode > 0) {
if (s->flength > len) {
/* A hostile or confused peer can claim a verified-prefix
* length that exceeds what we have on disk -- including
* for an empty local file, where buf is NULL and the
* map_ptr() calls below would dereference it. Clamp to
* what we can actually read. */
s->flength = len;
}
if (append_mode == 2) {
OFF_T j = 0;
for (j = CHUNK_SIZE; j < s->flength; j += CHUNK_SIZE) {
+1 -1
View File
@@ -15,7 +15,7 @@ if [ ! -f "$flagfile" ]; then
if "$srcdir/md-convert" --test "$srcdir/rsync-ssl.1.md" >/dev/null 2>&1; then
touch $flagfile
else
outname=`basename "$inname" .md`
outname=`echo "$inname" | sed 's/\.md$//'`
if [ -f "$outname" ]; then
exit 0
elif [ -f "$srcdir/$outname" ]; then
-5
View File
@@ -120,7 +120,6 @@ TZ_RE = re.compile(r'^#define\s+MAINTAINER_TZ_OFFSET\s+(-?\d+(\.\d+)?)', re.M)
VAR_REF_RE = re.compile(r'\$\{(\w+)\}')
VERSION_RE = re.compile(r' (\d[.\d]+)[, ]')
BIN_CHARS_RE = re.compile(r'[\1-\7]+')
LONG_OPT_DASH_RE = re.compile(r'(--\w[-\w]+)')
SPACE_DOUBLE_DASH_RE = re.compile(r'\s--(\s)')
NON_SPACE_SINGLE_DASH_RE = re.compile(r'(^|\W)-')
WHITESPACE_RE = re.compile(r'\s')
@@ -248,9 +247,6 @@ def find_man_substitutions():
env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(mtime + tz_offset)).lstrip('0')
if 'SOURCE_DATE_EPOCH' in os.environ:
env_subs['date'] = time.strftime('%d %b %Y', time.gmtime(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))))
def html_via_commonmark(txt):
return commonmark.HtmlRenderer().render(commonmark.Parser().parse(txt))
@@ -544,7 +540,6 @@ class TransformHtml(HTMLParser):
if st.in_pre:
html = htmlify(txt)
else:
txt = LONG_OPT_DASH_RE.sub(lambda x: x.group(1).replace('-', NBR_DASH[0]), txt)
txt = SPACE_DOUBLE_DASH_RE.sub(NBR_SPACE[0] + r'--\1', txt).replace('--', NBR_DASH[0]*2)
txt = NON_SPACE_SINGLE_DASH_RE.sub(r'\1' + NBR_DASH[0], txt)
html = htmlify(txt)
+4 -14
View File
@@ -7,20 +7,10 @@ if [ ! -f git-version.h ]; then
fi
if test -d "$srcdir/.git" || test -f "$srcdir/.git"; then
# Identify a git build by the development version from version.h plus the
# exact commit (e.g. "3.5.0dev-g1234abcd"), rather than the nearest release
# tag that `git describe` would pick: that tag can sit far behind a rebased
# development branch and then misnames the line you are actually on (showing,
# say, 3.4.3 for a 3.5.0dev tree). This also works in a shallow/tag-less
# clone. A release tarball has no .git, so git-version.h stays empty and
# rsync prints the plain RSYNC_VERSION.
# cd into the subshell rather than "git -C" (avoids needing a newer git).
gitsha=`(cd "$srcdir" && git rev-parse --short=8 HEAD) 2>/dev/null`
# Tolerate any preprocessor spacing and a trailing comment; capture only the
# quoted value. Empty (define missing/unmatched) -> leave RSYNC_GITVER unset.
rsyncver=`sed -n 's/^[[:space:]]*#[[:space:]]*define[[:space:]][[:space:]]*RSYNC_VERSION[[:space:]][[:space:]]*"\([^"]*\)".*/\1/p' "$srcdir/version.h"`
if [ -n "$gitsha" ] && [ -n "$rsyncver" ]; then
gitver="$rsyncver-g$gitsha"
gitver=`git describe --abbrev=8 2>/dev/null`
# NOTE: I'm avoiding "|" in sed since I'm not sure if sed -r is portable and "\|" fails on some OSes.
verchk=`echo "$gitver-" | sed -n '/^v3\.[0-9][0-9]*\.[0-9][0-9]*\(pre[0-9]*\)*-/p'`
if [ -n "$verchk" ]; then
echo "#define RSYNC_GITVER \"$gitver\"" >git-version.h.new
if ! diff git-version.h.new git-version.h >/dev/null; then
echo "Updating git-version.h"
-1
View File
@@ -18,7 +18,6 @@ inheader {
sub(/^CHAR\(/, "char ")
sub(/^INTEGER\(/, "int ")
sub(/^STRING\(/, "char *")
sub(/^STRING_SHELL\(/, "char *")
protos = protos "\n" $0 (local ? "(int module_id);" : "(void);")
next
}
-87
View File
@@ -1,87 +0,0 @@
# Old rsync version archive
Static rsync binaries built from historical release tags. Two uses:
1. **Cross-version behaviour checks** — confirming whether a behaviour a user
reported on an old release is version-specific or option-driven.
2. **The version-mixing test suite**`runtests.py --rsync-bin2=...` runs the
current code against one of these as the daemon / remote-shell peer; CI
(`.github/workflows/ubuntu-version-mix.yml`) does this for every binary
here against the per-version manifests in `testsuite/expect/`.
Binaries are **statically linked** so they run regardless of the host's
shared libraries, and named `rsync_<version>`:
| Binary | Version | Protocol | Notes |
|----------------|---------|----------|-----------------------------------------|
| `rsync_2.6.0` | 2.6.0 | 27 | 2004; needs autoconf regen (see below) |
| `rsync_3.0.0` | 3.0.0 | 30 | 2008 |
| `rsync_3.1.0` | 3.1.0 | 31 | 2013 |
| `rsync_3.1.3` | 3.1.3 | 31 | Ubuntu 18.04 / Debian buster era (2018) |
| `rsync_3.2.0` | 3.2.0 | 31 | 2020 (zstd/lz4/xxhash negotiation added)|
| `rsync_3.2.7` | 3.2.7 | 31 | 2022 |
| `rsync_3.3.0` | 3.3.0 | 31 | 2024 |
| `rsync_3.4.0` | 3.4.0 | 32 | 2025 |
| `rsync_3.4.1` | 3.4.1 | 32 | 2025 |
These are every `x.y.0` release from 2.6.0 (2004) onward plus a few point
releases. 2.6.0 is the practical floor: older tags need progressively more
porting to build on a current toolchain.
All built `--disable-openssl` and with `_FORTIFY_SOURCE` disabled (see below);
xxhash/zstd/lz4 are compiled in where the version supports them.
## Adding a version
```bash
./build_static.sh 3.2.7 # uses git tag v3.2.7
./build_static.sh 3.0.9 v3.0.9 # explicit tag if naming differs
```
The script checks out the tag into a throwaway `git worktree`, applies the
minimal patches needed to compile old sources on a modern toolchain, links
statically, verifies the result is static and reports the requested version,
then installs `rsync_<version>` here and removes the worktree.
Override the source repo with `RSYNC_REPO=/path/to/rsync ./build_static.sh ...`
(defaults to `../rsync.4`).
## Why the patches?
Modern GCC (>= 14, C23 default) and glibc reject things old rsync relied on.
`build_static.sh` handles these, each guarded so it's a no-op when not needed:
1. **K&R `lseek64()` redeclaration** in `syscall.c` clashes with glibc's real
prototype — removed.
2. **`gettimeofday()`** — glibc only has the 2-arg form; configure misdetects
the 1-arg form, so `HAVE_GETTIMEOFDAY_TZ` is forced on in `config.h`.
3. **C23 `()` == `(void)`** breaks K&R prototypes called with arguments
(`qsort` comparator, `pool->bomb`, etc.) — built with `-std=gnu11`.
4. Assorted modern `-Werror` promotions (incompatible pointer types, implicit
declarations) downgraded to warnings; bundled zlib/popt used to keep the
static link self-contained.
5. **OpenSSL (3.2+)** is disabled with `--disable-openssl`: linking
`libcrypto.a` statically drags in jitterentropy (`jent_*`) and zlib's
`uncompress` (OpenSSL's COMP module), which don't resolve here. OpenSSL only
provided optional MD4/MD5, which rsync implements natively, so checksum
behaviour is unaffected.
6. **`_FORTIFY_SOURCE` disabled** (`-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0`):
modern Ubuntu defaults it to `=3`, whose stricter object-size checks turn
latent (historically benign) over-reads in OLD rsync into hard
`*** buffer overflow detected ***` aborts when the binary runs as a
server/daemon — which made e.g. 3.1.3 and 3.2.7 unusable as peers. Disabling
it makes the archival binaries behave as the released versions did.
7. **Pre-3.0 tags (e.g. 2.6.0)** ship `configure.in`, not a generated
`configure`. The script runs `autoheader`/`autoconf` to generate it, after
neutralizing the `AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...))` fallbacks for
`inet_ntop`/`inet_pton`/`getaddrinfo`/`getnameinfo` — modern autoconf emits
broken shell for those never-taken branches (the funcs exist in glibc). It
also generates `proto.h` (no make rule in that era) and stubs the vendored
`lib/addrinfo.h` the tag dropped (modern glibc supplies `struct addrinfo`).
All guarded so they no-op on 3.x.
Newer versions may need fewer or different tweaks; if a build fails, the
script prints the first compiler errors from its log.
-128
View File
@@ -1,128 +0,0 @@
#!/bin/bash
# Build a static rsync binary from a historical git tag, for cross-version
# behaviour testing. Produces ./rsync_<version> in this directory.
#
# Usage: ./build_static.sh <version> [git-tag]
# Example: ./build_static.sh 3.1.3 # uses tag v3.1.3
# ./build_static.sh 3.2.7 v3.2.7
#
# Old rsync releases don't compile cleanly on a modern toolchain (GCC >= 14
# defaults to C23, where an empty () prototype means (void); glibc dropped the
# 1-arg gettimeofday; lseek64 K&R redeclarations clash). This script applies
# the minimal, best-effort workarounds and links statically so the result is
# self-contained and reproducible regardless of the host's shared libraries.
#
# Each workaround is guarded so it's a no-op on versions that don't need it.
set -euo pipefail
VERSION="${1:?usage: build_static.sh <version> [git-tag]}"
TAG="${2:-v$VERSION}"
ARCHIVE_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO="${RSYNC_REPO:-/home/tridge/project/rsync/rsync.4}" # any rsync worktree
WORKTREE="$(mktemp -d /tmp/rsync-build-XXXXXX)"
OUT="$ARCHIVE_DIR/rsync_$VERSION"
# C standard restores K&R () semantics; permissive flags downgrade the pile of
# modern -Werror promotions (incompatible pointers, implicit decls) to warnings.
# _FORTIFY_SOURCE is forced OFF: modern Ubuntu defaults it to =3, whose stricter
# object-size checks turn latent (historically benign) over-reads in OLD rsync
# into hard "*** buffer overflow detected ***" aborts when the binary acts as a
# server/daemon. Disabling it makes these archival binaries behave the way the
# released versions did, which is the whole point of the archive.
CFLAGS_OLD="-I. -I./zlib -O2 -g -std=gnu11 -fcommon -DHAVE_CONFIG_H -Wno-error \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-Wno-incompatible-pointer-types -Wno-implicit-function-declaration -Wno-int-conversion"
cleanup() {
cd "$REPO"
git worktree remove --force "$WORKTREE" 2>/dev/null || true
git worktree prune 2>/dev/null || true
}
trap cleanup EXIT
echo ">>> checking out $TAG into $WORKTREE"
# prefer an exact tag to avoid ambiguity with similarly-named branches
REF="$TAG"
if git -C "$REPO" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
REF="refs/tags/$TAG"
fi
git -C "$REPO" worktree add --detach "$WORKTREE" "$REF"
cd "$WORKTREE"
# --- workaround 1: K&R lseek64 redeclaration clashes with glibc's prototype ---
if grep -q 'off64_t lseek64();' syscall.c 2>/dev/null; then
echo ">>> patching syscall.c lseek64 redeclaration"
perl -0pi -e 's/#ifdef HAVE_LSEEK64\n#if !SIZEOF_OFF64_T\n\tOFF_T lseek64\(\);\n#else\n\toff64_t lseek64\(\);\n#endif\n\treturn lseek64/#ifdef HAVE_LSEEK64\n\treturn lseek64/' syscall.c
fi
# --- workaround 0: pre-3.0 tags ship configure.in, not a generated configure.
# Generate it. Modern autoconf emits broken shell for their
# AC_CHECK_FUNCS(fn,,AC_LIBOBJ(lib/...)) fallbacks -- but those branches are
# dead on a modern host (glibc has inet_ntop/inet_pton/getaddrinfo/getnameinfo),
# so neutralize the AC_LIBOBJ replacements before regenerating.
OLD_TREE=0
if [ ! -f ./configure ] && { [ -f configure.in ] || [ -f configure.ac ]; }; then
OLD_TREE=1
acsrc=configure.ac; [ -f configure.in ] && acsrc=configure.in
echo ">>> generating configure for an old tag (autoheader/autoconf)"
sed -i 's#AC_LIBOBJ(lib/[a-zA-Z_]*)#:#g' "$acsrc"
autoheader 2>/dev/null || true
autoconf 2>/dev/null || { echo "autoconf failed"; exit 1; }
fi
CONF_ARGS=(--disable-md2man --with-included-zlib=yes --with-included-popt=yes)
# OpenSSL (3.2+) only adds optional MD4/MD5 that rsync already implements, but
# linking libcrypto.a statically drags in jitterentropy + zlib's uncompress,
# which aren't resolvable here. Drop it when the flag exists.
if ./configure --help 2>/dev/null | grep -q -- '--disable-openssl'; then
echo ">>> disabling openssl for self-contained static link"
CONF_ARGS+=(--disable-openssl)
fi
echo ">>> configure (bundled zlib + popt, static-friendly)"
./configure "${CONF_ARGS[@]}" \
>"$WORKTREE/conf.log" 2>&1 || { tail -20 "$WORKTREE/conf.log"; exit 1; }
# --- workaround 2: modern glibc only has the 2-arg gettimeofday ---------------
if grep -q '/\* #undef HAVE_GETTIMEOFDAY_TZ \*/' config.h; then
echo ">>> forcing HAVE_GETTIMEOFDAY_TZ (configure misdetects it)"
sed -i 's|/\* #undef HAVE_GETTIMEOFDAY_TZ \*/|#define HAVE_GETTIMEOFDAY_TZ 1|' config.h
fi
# --- workaround 4 (old trees only): generate proto.h if the tree has no make
# rule for it, and stub a vendored lib/addrinfo.h that the git tag dropped
# (modern glibc supplies struct addrinfo / sockaddr_storage, so empty is right).
if [ "$OLD_TREE" = 1 ]; then
if [ ! -f proto.h ] && [ -f mkproto.awk ]; then
echo ">>> generating proto.h"
cat ./*.c ./lib/compat.c 2>/dev/null | awk -f ./mkproto.awk > proto.h
fi
if grep -q 'include "lib/addrinfo.h"' rsync.h 2>/dev/null && [ ! -f lib/addrinfo.h ]; then
echo ">>> stubbing lib/addrinfo.h"
echo '/* emptied: modern glibc provides struct addrinfo */' > lib/addrinfo.h
fi
fi
echo ">>> building (static)"
make -j"$(nproc)" CFLAGS="$CFLAGS_OLD" LDFLAGS="-static" \
>"$WORKTREE/make.log" 2>&1 || { grep -E 'error:|\*\*\*' "$WORKTREE/make.log" | head; exit 1; }
# verify it's actually static before we keep it
if ldd ./rsync 2>&1 | grep -qv 'not a dynamic executable'; then
echo "ERROR: binary is not statically linked:" >&2
ldd ./rsync >&2
exit 1
fi
GOT="$(./rsync --version | head -1 | awk '{print $3}')"
if [ "$GOT" != "$VERSION" ]; then
echo "ERROR: built version '$GOT' != requested '$VERSION'" >&2
exit 1
fi
cp ./rsync "$OUT"
strip "$OUT"
echo ">>> installed $OUT"
"$OUT" --version | head -1
file "$OUT"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+30 -215
View File
@@ -59,9 +59,6 @@ int preserve_perms = 0;
int preserve_executability = 0;
int preserve_devices = 0;
int preserve_specials = 0;
int drop_devices = 0;
char *confine_root = NULL; /* --confine-root: see vfs/dirstack.c */
unsigned int confine_rootlen = 0;
int preserve_uid = 0;
int preserve_gid = 0;
int preserve_mtimes = 0;
@@ -89,8 +86,6 @@ int sparse_files = 0;
int preallocate_files = 0;
int do_compression = 0;
int do_compression_level = CLVL_NOT_SPECIFIED;
int do_compression_threads = 0; /*n = 0 use rsync thread, n >= 1 spawn n threads for compression */
#define MAX_DAEMON_COMPRESSION_THREADS 8
int am_root = 0; /* 0 = normal, 1 = root, 2 = --super, -1 = --fake-super */
int am_server = 0;
int am_sender = 0;
@@ -118,21 +113,11 @@ int mkpath_dest_arg = 0;
int allow_inc_recurse = 1;
int xfer_dirs = -1;
int am_daemon = 0;
/* Set after a successful per-module chroot ("use chroot = yes") in
* clientserver.c. NOT set for the daemon-level "daemon chroot = /X"
* chroot: that confines path resolution to /X, but module paths
* /X/modA, /X/modB, etc. are not chroot boundaries, so the per-module
* symlink-race defenses (vfs_resolve_open() / do_*_at() in
* syscall.c, gated by `am_daemon && !am_chrooted`) must still fire
* even when the daemon is inside a daemon chroot. */
int am_chrooted = 0;
int connect_timeout = 0;
int keep_partial = 0;
int safe_symlinks = 0;
int copy_unsafe_links = 0;
int insecure_links = 0;
int munge_symlinks = 0;
int use_secure_symlinks = 0;
int size_only = 0;
int daemon_bwlimit = 0;
int bwlimit = 0;
@@ -240,7 +225,7 @@ char *iconv_opt =
struct chmod_mode_struct *chmod_modes = NULL;
static const char *const debug_verbosity[] = {
static const char *debug_verbosity[] = {
/*0*/ NULL,
/*1*/ NULL,
/*2*/ "BIND,CMD,CONNECT,DEL,DELTASUM,DUP,FILTER,FLIST,ICONV",
@@ -251,7 +236,7 @@ static const char *const debug_verbosity[] = {
#define MAX_VERBOSITY ((int)(sizeof debug_verbosity / sizeof debug_verbosity[0]) - 1)
static const char *const info_verbosity[1+MAX_VERBOSITY] = {
static const char *info_verbosity[1+MAX_VERBOSITY] = {
/*0*/ "NONREG",
/*1*/ "COPY,DEL,FLIST,MISC,NAME,STATS,SYMSAFE",
/*2*/ "BACKUP,MISC2,MOUNT,NAME2,REMOVE,SKIP",
@@ -330,7 +315,7 @@ static struct output_struct debug_words[COUNT_DEBUG+1] = {
};
static int verbose = 0;
static int vfs_stats = 0;
static int do_stats = 0;
static int do_progress = 0;
static int daemon_opt; /* sets am_daemon after option error-reporting */
static int F_option_cnt = 0;
@@ -456,10 +441,7 @@ static void parse_output_words(struct output_struct *words, short *levels, const
len--;
}
lev = isDigit(str+len) ? atoi(str+len) : 1;
/* atoi() of an overflowing positive digit string can return a
* negative int (LONG_MAX truncated on LP64); a negative lev
* here later indexes counts[lev] in make_output_option(). */
if (lev > MAX_OUT_LEVEL || lev < 0)
if (lev > MAX_OUT_LEVEL)
lev = MAX_OUT_LEVEL;
if (len == 4 && strncasecmp(str, "help", 4) == 0) {
output_item_help(words);
@@ -492,7 +474,7 @@ static void parse_output_words(struct output_struct *words, short *levels, const
static void output_item_help(struct output_struct *words)
{
short *levels = words == info_words ? info_levels : debug_levels;
const char *const*verbosity = words == info_words ? info_verbosity : debug_verbosity;
const char **verbosity = words == info_words ? info_verbosity : debug_verbosity;
char buf[128], *opt, *fmt = "%-10s %s\n";
int j;
@@ -620,7 +602,7 @@ static struct poptOption long_options[] = {
{"quiet", 'q', POPT_ARG_NONE, 0, 'q', 0, 0 },
{"motd", 0, POPT_ARG_VAL, &output_motd, 1, 0, 0 },
{"no-motd", 0, POPT_ARG_VAL, &output_motd, 0, 0, 0 },
{"stats", 0, POPT_ARG_NONE, &vfs_stats, 0, 0, 0 },
{"stats", 0, POPT_ARG_NONE, &do_stats, 0, 0, 0 },
{"human-readable", 'h', POPT_ARG_NONE, 0, 'h', 0, 0},
{"no-human-readable",0, POPT_ARG_VAL, &human_readable, 0, 0, 0},
{"no-h", 0, POPT_ARG_VAL, &human_readable, 0, 0, 0},
@@ -684,17 +666,12 @@ static struct poptOption long_options[] = {
{"no-write-devices", 0, POPT_ARG_VAL, &write_devices, 0, 0, 0 },
{"specials", 0, POPT_ARG_VAL, &preserve_specials, 1, 0, 0 },
{"no-specials", 0, POPT_ARG_VAL, &preserve_specials, 0, 0, 0 },
{"drop-D", 0, POPT_ARG_VAL, &drop_devices, 1, 0, 0 },
{"no-drop-D", 0, POPT_ARG_VAL, &drop_devices, 0, 0, 0 },
{"confine-root", 0, POPT_ARG_STRING, &confine_root, 0, 0, 0 },
{"links", 'l', POPT_ARG_VAL, &preserve_links, 1, 0, 0 },
{"no-links", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 },
{"no-l", 0, POPT_ARG_VAL, &preserve_links, 0, 0, 0 },
{"copy-links", 'L', POPT_ARG_NONE, &copy_links, 0, 0, 0 },
{"copy-unsafe-links",0, POPT_ARG_NONE, &copy_unsafe_links, 0, 0, 0 },
{"safe-links", 0, POPT_ARG_NONE, &safe_symlinks, 0, 0, 0 },
{"insecure-links", 0, POPT_ARG_VAL, &insecure_links, 1, 0, 0 },
{"no-insecure-links",0, POPT_ARG_VAL, &insecure_links, 0, 0, 0 },
{"munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 1, 0, 0 },
{"no-munge-links", 0, POPT_ARG_VAL, &munge_symlinks, 0, 0, 0 },
{"copy-dirlinks", 'k', POPT_ARG_NONE, &copy_dirlinks, 0, 0, 0 },
@@ -779,8 +756,6 @@ static struct poptOption long_options[] = {
{"skip-compress", 0, POPT_ARG_STRING, &skip_compress, 0, 0, 0 },
{"compress-level", 0, POPT_ARG_INT, &do_compression_level, 0, 0, 0 },
{"zl", 0, POPT_ARG_INT, &do_compression_level, 0, 0, 0 },
{"compress-threads", 0, POPT_ARG_INT, &do_compression_threads, 0, 0, 0 },
{"zt", 0, POPT_ARG_INT, &do_compression_threads, 0, 0, 0 },
{0, 'P', POPT_ARG_NONE, 0, 'P', 0, 0 },
{"progress", 0, POPT_ARG_VAL, &do_progress, 1, 0, 0 },
{"no-progress", 0, POPT_ARG_VAL, &do_progress, 0, 0, 0 },
@@ -869,7 +844,7 @@ static struct poptOption long_options[] = {
{0,0,0,0, 0, 0, 0}
};
static const struct poptOption long_daemon_options[] = {
static struct poptOption long_daemon_options[] = {
/* longName, shortName, argInfo, argPtr, value, descrip, argDesc */
{"address", 0, POPT_ARG_STRING, &bind_address, 0, 0, 0 },
{"bwlimit", 0, POPT_ARG_INT, &daemon_bwlimit, 0, 0, 0 },
@@ -917,54 +892,9 @@ void option_error(void)
}
/* Does this row store a compile-time constant, and if so which?
*
* popt's `val` is not comparable across argInfo kinds. For POPT_ARG_VAL it IS
* the value stored in `arg`; for the others a nonzero `val` is an action code
* handed to the parser's switch, and POPT_ARG_NONE with a destination stores 1
* regardless. Comparing the raw field therefore misses aliases spelled with
* different table shapes -- --del is POPT_ARG_NONE/&delete_during/0 and
* --delete-during is POPT_ARG_VAL/&delete_during/1, and both set it to 1. */
static int refuse_const_assign(const struct poptOption *op, int *valp)
{
if (!op->arg)
return 0;
if (op->argInfo == POPT_ARG_VAL) {
*valp = op->val;
return 1;
}
/* A nonzero val here means the row ALSO runs a parser action, so it is
* not merely an assignment and must not be folded in with one. */
if (op->argInfo == POPT_ARG_NONE && op->val == 0) {
*valp = 1;
return 1;
}
return 0;
}
/* Do two table rows name the same capability? An exact refuse rule names a
* capability, not one spelling of it. */
static int same_refuse_action(const struct poptOption *a, const struct poptOption *b)
{
int a_val, b_val;
/* Constant assignments: same destination, same resulting value. The
* value check keeps opposite switches such as --foo and --no-foo apart,
* since they differ only in what they store. */
if (refuse_const_assign(a, &a_val) && refuse_const_assign(b, &b_val))
return a->arg == b->arg && a_val == b_val;
/* Anything else has to match as a table entry: a row storing a runtime
* value (POPT_ARG_INT, POPT_ARG_STRING) needs the same destination and
* action code, and an action-only row the same nonzero code. */
if (a->argInfo != b->argInfo || a->val != b->val)
return 0;
return a->arg ? a->arg == b->arg : !b->arg && a->val != 0;
}
static void parse_one_refuse_match(int negated, const char *ref, const struct poptOption *list_end)
{
struct poptOption *op, *matched_op = NULL;
struct poptOption *op;
char shortName[2];
int is_wild = strpbrk(ref, "*?[") != NULL;
int found_match = 0;
@@ -985,21 +915,8 @@ static void parse_one_refuse_match(int negated, const char *ref, const struct po
else if (!is_wild)
op->descrip = negated ? "a=" : "r=";
found_match = 1;
if (!is_wild) {
matched_op = op;
if (!is_wild)
break;
}
}
}
if (matched_op) {
for (op = long_options; op != list_end; op++) {
if (op == matched_op || !same_refuse_action(op, matched_op))
continue;
if (op->descrip[1] == '*')
op->descrip = negated ? "a*" : "r*";
else
op->descrip = negated ? "a=" : "r=";
}
}
@@ -1079,11 +996,6 @@ static void set_refuse_options(void)
parse_one_refuse_match(0, "iconv", list_end);
#endif
parse_one_refuse_match(0, "log-file*", list_end);
/* A client must never disable the daemon's symlink confinement:
* --insecure-links is a local-only flag, so the daemon hard-refuses it
* (dropping the connection). The daemon's own opt-out is the
* "insecure links" module parameter, not this flag. */
parse_one_refuse_match(0, "insecure-links", list_end);
}
#ifndef SUPPORT_ATIMES
@@ -1165,8 +1077,6 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op
int reps, mult, len;
const char *arg, *err = "invalid", *min_max = NULL;
ssize_t limit = -1, size = 1;
ssize_t size_max = max_value >= 0 ? max_value : (ssize_t)(SIZE_MAX / 2);
double dsize;
for (arg = size_arg; isDigit(arg); arg++) {}
if (*arg == '.' || *arg == get_decimal_point()) /* backward compatibility: always allow '.' */
@@ -1201,38 +1111,11 @@ static ssize_t parse_size_arg(const char *size_arg, char def_suf, const char *op
mult = 1024, arg += 2;
else
goto failure;
while (reps--) {
if (size > size_max / mult) {
err = "too large";
min_max = "max";
limit = max_value;
goto failure;
}
while (reps--)
size *= mult;
}
errno = 0;
dsize = strtod(size_arg, NULL);
if (errno == ERANGE || dsize < 0 || dsize > (double)size_max / size
|| (max_value < 0 && dsize >= (double)size_max / size)) {
err = "too large";
min_max = "max";
limit = max_value;
goto failure;
}
size = (ssize_t)(dsize * size);
if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg) {
if (*arg == '+') {
if (size == size_max) {
err = "too large";
min_max = "max";
limit = max_value;
goto failure;
}
size++;
} else
size--;
arg += 2;
}
size *= atof(size_arg);
if ((*arg == '+' || *arg == '-') && arg[1] == '1' && arg != size_arg)
size += atoi(arg), arg += 2;
if (*arg)
goto failure;
if (size < 0 || (max_value >= 0 && size > max_value)) {
@@ -1256,8 +1139,6 @@ failure:
min_max, do_big_num(limit, 3, NULL),
unlimited_0 && min_max[1] == 'i' ? " or 0 for unlimited" : "");
}
if (len < 0 || len > (int)sizeof err_buf - 2)
len = sizeof err_buf - 2;
err_buf[len] = '\n';
err_buf[len+1] = '\0';
return -1;
@@ -1275,7 +1156,7 @@ static time_t parse_time(const char *arg)
{
const char *cp;
time_t val, now = time(NULL);
struct tm t, tmp, *today = localtime_r(&now, &tmp);
struct tm t, *today = localtime(&now);
int in_date, old_mday, n;
memset(&t, 0, sizeof t);
@@ -1488,10 +1369,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)
/* TODO: Call poptReadDefaultConfig; handle errors. */
pc = poptGetContext(RSYNC_NAME, argc, argv, long_options, 0);
if (pc == NULL) {
strlcpy(err_buf, "poptGetContext returned NULL\n", sizeof err_buf);
return 0;
}
if (!am_server) {
poptReadDefaultConfig(pc, 0);
popt_unalias(pc, "--daemon");
@@ -1596,6 +1473,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
*argc_p = 0;
} else if (poptDupArgv(argc, argv, argc_p, argv_p) != 0)
out_of_memory("parse_arguments");
argv = *argv_p;
poptFreeContext(pc);
am_starting_up = 0;
@@ -2066,10 +1944,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)
ssize_t size = parse_size_arg(max_alloc_arg, 'B', "max-alloc", 1024*1024, -1, True);
if (size < 0)
goto cleanup;
if (size == 0) {
snprintf(err_buf, sizeof err_buf, "max-alloc must be greater than zero\n");
goto cleanup;
}
max_alloc = size;
}
if (!max_alloc)
@@ -2132,14 +2006,6 @@ int parse_arguments(int *argc_p, const char ***argv_p)
create_refuse_error(refused_compress);
goto cleanup;
}
if (do_compression_threads < 0)
do_compression_threads = 0;
/* A daemon client controls the server-side sender arguments. Keep one
* unauthenticated connection from asking Zstandard to materialize its
* implementation maximum (currently hundreds) of worker threads. Local
* and remote-shell invocations retain the operator-requested value. */
if (am_daemon && do_compression_threads > MAX_DAEMON_COMPRESSION_THREADS)
do_compression_threads = MAX_DAEMON_COMPRESSION_THREADS;
}
#ifdef HAVE_SETVBUF
@@ -2177,7 +2043,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
set_output_verbosity(verbose, DEFAULT_PRIORITY);
if (vfs_stats) {
if (do_stats) {
parse_output_words(info_words, info_levels,
verbose > 1 ? "stats3" : "stats2", DEFAULT_PRIORITY);
}
@@ -2371,33 +2237,13 @@ int parse_arguments(int *argc_p, const char ***argv_p)
STRUCT_STAT st;
char prefix[SYMLINK_PREFIX_LEN]; /* NOT +1 ! */
strlcpy(prefix, SYMLINK_PREFIX, sizeof prefix); /* trim the trailing slash */
if (vfs_stat(VFS_AT_FDCWD, prefix, &st, VFS_ALLOW_SYMLINK) == 0 && S_ISDIR(st.st_mode)) {
if (do_stat(prefix, &st) == 0 && S_ISDIR(st.st_mode)) {
rprintf(FERROR, "Symlink munging is unsafe when a %s directory exists.\n",
prefix);
exit_cleanup(RERR_UNSUPPORTED);
}
}
if (confine_root) {
/* A daemon already has module_dir for this job, and honouring a
* peer-supplied root there could only loosen the module boundary. */
if (am_daemon)
confine_root = NULL;
else if (*confine_root != '/') {
snprintf(err_buf, sizeof err_buf,
"--confine-root must be an absolute path\n");
return 0;
} else if (insecure_links) {
/* The opt-out restores the legacy open, which short-circuits the
* walk that enforces the root -- so the pair would silently mean
* no confinement at all. Say so instead. */
snprintf(err_buf, sizeof err_buf,
"--insecure-links cannot be combined with --confine-root\n");
return 0;
} else
confine_root = normalize_path(confine_root, True, &confine_rootlen);
}
if (sanitize_paths) {
int i;
for (i = argc; i-- > 0; )
@@ -2409,26 +2255,21 @@ int parse_arguments(int *argc_p, const char ***argv_p)
}
if (daemon_filter_list.head && !am_sender) {
filter_rule_list *elp = &daemon_filter_list;
/* Strip the module-dir prefix to get the module-relative name, but keep a
* leading "/" for a "path = /" module (module_dirlen <= 1) so an absolute
* (module-rooted) filter rule still matches. */
if (tmpdir) {
char clean[MAXPATHLEN], *dir;
char *dir;
if (!*tmpdir)
goto options_rejected;
if (!sanitize_path(clean, tmpdir, "/", 0, SP_DEFAULT))
strlcpy(clean, tmpdir, sizeof clean);
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
dir = tmpdir + (*tmpdir == '/' ? module_dirlen : 0);
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
if (check_filter(elp, FLOG, dir, 1) < 0)
goto options_rejected;
}
if (backup_dir) {
char clean[MAXPATHLEN], *dir;
char *dir;
if (!*backup_dir)
goto options_rejected;
if (!sanitize_path(clean, backup_dir, "/", 0, SP_DEFAULT))
strlcpy(clean, backup_dir, sizeof clean);
dir = clean + (*clean == '/' && module_dirlen > 1 ? module_dirlen : 0);
dir = backup_dir + (*backup_dir == '/' ? module_dirlen : 0);
clean_fname(dir, CFN_COLLAPSE_DOT_DOT_DIRS);
if (check_filter(elp, FLOG, dir, 1) < 0)
goto options_rejected;
}
@@ -2605,7 +2446,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
if (files_from) {
char *h, *p;
int q = 0;
int q;
if (argc > 2 || (!am_daemon && !am_server && argc == 1)) {
usage(FERROR);
exit_cleanup(RERR_SYNTAX);
@@ -2639,16 +2480,7 @@ int parse_arguments(int *argc_p, const char ***argv_p)
if (check_filter(&daemon_filter_list, FLOG, dir, 0) < 0)
goto options_rejected;
}
/* Operator-supplied path that may transit attacker-writable
* parents; refuse symlinks not owned by uid 0 or our euid,
* as for --exclude-from/--include-from/--filter in exclude.c.
* A daemon reads this list from a CLIENT-requested path
* (--files-from=:LIST) and it must stay inside the module:
* the is_operator walk also refuses a (trusted-owned) symlink
* that redirects the list outside the module root -- e.g. a
* root-owned backup symlink. No-op off a daemon (the module-root
* check only fires when am_daemon). */
filesfrom_fd = vfs_open_owner_walk(files_from, O_RDONLY|O_BINARY, 0, 1);
filesfrom_fd = open(files_from, O_RDONLY|O_BINARY);
if (filesfrom_fd < 0) {
snprintf(err_buf, sizeof err_buf,
"failed to open files-from file %s: %s\n",
@@ -2688,7 +2520,7 @@ static char SPLIT_ARG_WHEN_OLD[1];
**/
char *safe_arg(const char *opt, const char *arg)
{
#define SHELL_CHARS "!#$&;|<>(){}\"\'` \t\n\r\\"
#define SHELL_CHARS "!#$&;|<>(){}\"'` \t\\"
#define WILD_CHARS "*?[]" /* We don't allow remote brace expansion */
BOOL is_filename_arg = !opt;
char *escapes = is_filename_arg ? SHELL_CHARS : WILD_CHARS SHELL_CHARS;
@@ -2707,16 +2539,7 @@ char *safe_arg(const char *opt, const char *arg)
escape_leading_tilde = 1;
}
for (f = arg; *f; f++) {
if (*f == '\\') {
/* Mirror the writer below: in filename mode a backslash
* before a wildcard is not doubled, so don't reserve a slot
* for it. The "f[1] &&" also avoids the strchr(WILD_CHARS,
* '\0') footgun (which matches the terminator) on a trailing
* backslash -- otherwise the counter and writer disagree and
* an uninitialized heap byte leaks into the result. */
if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))
extras++;
} else if (strchr(escapes, *f))
if (strchr(escapes, *f))
extras++;
}
}
@@ -2741,7 +2564,7 @@ char *safe_arg(const char *opt, const char *arg)
*t++ = '\\';
while (*f) {
if (*f == '\\') {
if (!is_filename_arg || !(f[1] && strchr(WILD_CHARS, f[1])))
if (!is_filename_arg || !strchr(WILD_CHARS, f[1]))
*t++ = '\\';
} else if (strchr(escapes, *f))
*t++ = '\\';
@@ -2781,10 +2604,7 @@ void server_options(char **args, int *argc_p)
if (protect_args)
argstr[x++] = 's';
/* `verbose` is unbounded (one increment per -v on our own command
* line), so an uncapped loop walks past argstr[64]. Anything beyond
* level ~5 is meaningless to the server anyway. */
for (i = 0; i < verbose && i < 9; i++)
for (i = 0; i < verbose; i++)
argstr[x++] = 'v';
if (quiet && msgs2stderr)
@@ -3015,7 +2835,7 @@ void server_options(char **args, int *argc_p)
args[ac++] = "--super";
if (size_only)
args[ac++] = "--size-only";
if (vfs_stats)
if (do_stats)
args[ac++] = "--stats";
} else {
if (skip_compress)
@@ -3061,11 +2881,6 @@ void server_options(char **args, int *argc_p)
if (copy_unsafe_links)
args[ac++] = "--copy-unsafe-links";
/* --insecure-links is NOT forwarded: it is a local-only opt-out. A daemon
* governs its own confinement via the "insecure links" module parameter and
* drops a connection that sends --insecure-links; a remote-shell peer that
* wants it must be given it on its own side (e.g. via --rsync-path). */
if (safe_symlinks)
args[ac++] = "--safe-links";
+1 -1
View File
@@ -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
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env -S python3 -B
# This script turns one or more diff files in the patches dir (which is
# expected to be a checkout of the rsync-patches git repo) into a branch
# in the main rsync git checkout. This allows the applied patch to be
# merged with the latest rsync changes and tested. To update the diff
# with the resulting changes, see the patch-update script.
import os, sys, re, argparse, glob
sys.path = ['packaging'] + sys.path
from pkglib import *
def main():
global created, info, local_branch
cur_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)
local_branch = get_patch_branches(args.base_branch)
if args.delete_local_branches:
for name in sorted(local_branch):
branch = f"patch/{args.base_branch}/{name}"
cmd_chk(['git', 'branch', '-D', branch])
local_branch = set()
if args.add_missing:
for fn in sorted(glob.glob(f"{args.patches_dir}/*.diff")):
name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
if name not in local_branch and fn not in args.patch_files:
args.patch_files.append(fn)
if not args.patch_files:
return
for fn in args.patch_files:
if not fn.endswith('.diff'):
die(f"Filename is not a .diff file: {fn}")
if not os.path.isfile(fn):
die(f"File not found: {fn}")
scanned = set()
info = { }
patch_list = [ ]
for fn in args.patch_files:
m = re.match(r'^(?P<dir>.*?)(?P<name>[^/]+)\.diff$', fn)
patch = argparse.Namespace(**m.groupdict())
if patch.name in scanned:
continue
patch.fn = fn
lines = [ ]
commit_hash = None
with open(patch.fn, 'r', encoding='utf-8') as fh:
for line in fh:
m = re.match(r'^based-on: (\S+)', line)
if m:
commit_hash = m[1]
break
if (re.match(r'^index .*\.\..* \d', line)
or re.match(r'^diff --git ', line)
or re.match(r'^--- (old|a)/', line)):
break
lines.append(re.sub(r'\s*\Z', "\n", line, 1))
info_txt = ''.join(lines).strip() + "\n"
lines = None
parent = args.base_branch
patches = re.findall(r'patch -p1 <%s/(\S+)\.diff' % args.patches_dir, info_txt)
if patches:
last = patches.pop()
if last != patch.name:
warn(f"No identity patch line in {patch.fn}")
patches.append(last)
if patches:
parent = patches.pop()
if parent not in scanned:
diff_fn = patch.dir + parent + '.diff'
if not os.path.isfile(diff_fn):
die(f"Failed to find parent of {patch.fn}: {parent}")
# Add parent to args.patch_files so that we will look for the
# parent's parent. Any duplicates will be ignored.
args.patch_files.append(diff_fn)
else:
warn(f"No patch lines found in {patch.fn}")
info[patch.name] = [ parent, info_txt, commit_hash ]
patch_list.append(patch)
created = set()
for patch in patch_list:
create_branch(patch)
cmd_chk(['git', 'checkout', args.base_branch])
def create_branch(patch):
if patch.name in created:
return
created.add(patch.name)
parent, info_txt, commit_hash = info[patch.name]
parent = argparse.Namespace(dir=patch.dir, name=parent, fn=patch.dir + parent + '.diff')
if parent.name == args.base_branch:
parent_branch = commit_hash if commit_hash else args.base_branch
else:
create_branch(parent)
parent_branch = '/'.join(['patch', args.base_branch, parent.name])
branch = '/'.join(['patch', args.base_branch, patch.name])
print("\n" + '=' * 64)
print(f"Processing {branch} ({parent_branch})")
if patch.name in local_branch:
cmd_chk(['git', 'branch', '-D', branch])
cmd_chk(['git', 'checkout', '-b', branch, parent_branch])
info_fn = 'PATCH.' + patch.name
with open(info_fn, 'w', encoding='utf-8') as fh:
fh.write(info_txt)
cmd_chk(['git', 'add', info_fn])
with open(patch.fn, 'r', encoding='utf-8') as fh:
patch_txt = fh.read()
cmd_run('patch -p1'.split(), input=patch_txt)
for fn in glob.glob('*.orig') + glob.glob('*/*.orig'):
os.unlink(fn)
pos = 0
new_file_re = re.compile(r'\nnew file mode (?P<mode>\d+)\s+--- /dev/null\s+\+\+\+ b/(?P<fn>.+)')
while True:
m = new_file_re.search(patch_txt, pos)
if not m:
break
os.chmod(m['fn'], int(m['mode'], 8))
cmd_chk(['git', 'add', m['fn']])
pos = m.end()
while True:
cmd_chk('git status'.split())
ans = input('Press Enter to commit, Ctrl-C to abort, or type a wild-name to add a new file: ')
if ans == '':
break
cmd_chk("git add " + ans, shell=True)
while True:
s = cmd_run(['git', 'commit', '-a', '-m', f"Creating branch from {patch.name}.diff."])
if not s.returncode:
break
s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
if s.returncode:
die('Aborting due to shell error code')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Create a git patch branch from an rsync patch file.", add_help=False)
parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
parser.add_argument('--add-missing', '-a', action='store_true', help="Add a branch for every patches/*.diff that doesn't have a branch.")
parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
parser.add_argument('--delete', dest='delete_local_branches', action='store_true', help="Delete all the local patch/BASE/* branches, not just the ones that are being recreated.")
parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
args = parser.parse_args()
main()
# vim: sw=4 et ft=python
-2
View File
@@ -1,2 +0,0 @@
- /generated-files/
- /binaries/
+3 -3
View File
@@ -1,6 +1,6 @@
Summary: A fast, versatile, remote (and local) file-copying tool
Name: rsync
Version: 3.5.0
Version: 3.4.0
%define fullversion %{version}
Release: 1
%define srcdir src
@@ -79,5 +79,5 @@ rm -rf $RPM_BUILD_ROOT
%dir /etc/rsync-ssl/certs
%changelog
* Thu Aug 13 2026 Rsync Project <rsync.project@gmail.com>
Released 3.5.0.
* Wed Jan 15 2025 Rsync Project <rsync.project@gmail.com>
Released 3.4.0.
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env -S python3 -B
# This script is used to turn one or more of the "patch/BASE/*" branches
# into one or more diffs in the "patches" directory. Pass the option
# --gen if you want generated files in the diffs. Pass the name of
# one or more diffs if you want to just update a subset of all the
# diffs.
import os, sys, re, argparse, time, shutil
sys.path = ['packaging'] + sys.path
from pkglib import *
MAKE_GEN_CMDS = [
'./prepare-source'.split(),
'cd build && if test -f config.status ; then ./config.status ; else ../configure ; fi',
'make -C build gen'.split(),
]
TMP_DIR = "patches.gen"
os.environ['GIT_MERGE_AUTOEDIT'] = 'no'
def main():
global master_commit, parent_patch, description, completed, last_touch
if not os.path.isdir(args.patches_dir):
die(f'No "{args.patches_dir}" directory was found.')
if not os.path.isdir('.git'):
die('No ".git" directory present in the current dir.')
starting_branch, args.base_branch = check_git_state(args.base_branch, not args.skip_check, args.patches_dir)
master_commit = latest_git_hash(args.base_branch)
if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':
die('You must setup an auto-build-save dir to use this script.')
if args.gen:
if os.path.lexists(TMP_DIR):
die(f'"{TMP_DIR}" must not exist in the current directory.')
gen_files = get_gen_files()
os.mkdir(TMP_DIR, 0o700)
for cmd in MAKE_GEN_CMDS:
cmd_chk(cmd)
cmd_chk(['rsync', '-a', *gen_files, f'{TMP_DIR}/master/'])
last_touch = int(time.time())
# Start by finding all patches so that we can load all possible parents.
patches = sorted(list(get_patch_branches(args.base_branch)))
parent_patch = { }
description = { }
for patch in patches:
branch = f"patch/{args.base_branch}/{patch}"
desc = ''
proc = cmd_pipe(['git', 'diff', '-U1000', f"{args.base_branch}...{branch}", '--', f"PATCH.{patch}"])
in_diff = False
for line in proc.stdout:
if in_diff:
if not re.match(r'^[ +]', line):
continue
line = line[1:]
m = re.search(r'patch -p1 <patches/(\S+)\.diff', line)
if m and m[1] != patch:
parpat = parent_patch[patch] = m[1]
if not parpat in patches:
die(f"Parent of {patch} is not a local branch: {parpat}")
desc += line
elif re.match(r'^@@ ', line):
in_diff = True
description[patch] = desc
proc.communicate()
if args.patch_files: # Limit the list of patches to actually process
valid_patches = patches
patches = [ ]
for fn in args.patch_files:
name = re.sub(r'\.diff$', '', re.sub(r'.+/', '', fn))
if name not in valid_patches:
die(f"Local branch not available for patch: {name}")
patches.append(name)
completed = set()
for patch in patches:
if patch in completed:
continue
if not update_patch(patch):
break
if args.gen:
shutil.rmtree(TMP_DIR)
while last_touch >= int(time.time()):
time.sleep(1)
cmd_chk(['git', 'checkout', starting_branch])
cmd_chk(['packaging/prep-auto-dir'], discard='output')
def update_patch(patch):
global last_touch
completed.add(patch) # Mark it as completed early to short-circuit any (bogus) dependency loops.
parent = parent_patch.get(patch, None)
if parent:
if parent not in completed:
if not update_patch(parent):
return 0
based_on = parent = f"patch/{args.base_branch}/{parent}"
else:
parent = args.base_branch
based_on = master_commit
print(f"======== {patch} ========")
while args.gen and last_touch >= int(time.time()):
time.sleep(1)
branch = f"patch/{args.base_branch}/{patch}"
s = cmd_run(['git', 'checkout', branch])
if s.returncode != 0:
return 0
s = cmd_run(['git', 'merge', based_on])
ok = s.returncode == 0
skip_shell = False
if not ok or args.cmd or args.make or args.shell:
cmd_chk(['packaging/prep-auto-dir'], discard='output')
if not ok:
print(f'"git merge {based_on}" incomplete -- please fix.')
if not run_a_shell(parent, patch):
return 0
if not args.make and not args.cmd:
skip_shell = True
if args.make:
if cmd_run(['packaging/smart-make']).returncode != 0:
if not run_a_shell(parent, patch):
return 0
if not args.cmd:
skip_shell = True
if args.cmd:
if cmd_run(args.cmd).returncode != 0:
if not run_a_shell(parent, patch):
return 0
skip_shell = True
if args.shell and not skip_shell:
if not run_a_shell(parent, patch):
return 0
with open(f"{args.patches_dir}/{patch}.diff", 'w', encoding='utf-8') as fh:
fh.write(description[patch])
fh.write(f"\nbased-on: {based_on}\n")
if args.gen:
gen_files = get_gen_files()
for cmd in MAKE_GEN_CMDS:
cmd_chk(cmd)
cmd_chk(['rsync', '-a', *gen_files, f"{TMP_DIR}/{patch}/"])
else:
gen_files = [ ]
last_touch = int(time.time())
proc = cmd_pipe(['git', 'diff', based_on])
skipping = False
for line in proc.stdout:
if skipping:
if not re.match(r'^diff --git a/', line):
continue
skipping = False
elif re.match(r'^diff --git a/PATCH', line):
skipping = True
continue
if not re.match(r'^index ', line):
fh.write(line)
proc.communicate()
if args.gen:
e_tmp_dir = re.escape(TMP_DIR)
diff_re = re.compile(r'^(diff -Nurp) %s/[^/]+/(.*?) %s/[^/]+/(.*)' % (e_tmp_dir, e_tmp_dir))
minus_re = re.compile(r'^\-\-\- %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
plus_re = re.compile(r'^\+\+\+ %s/[^/]+/([^\t]+)\t.*' % e_tmp_dir)
if parent == args.base_branch:
parent_dir = 'master'
else:
m = re.search(r'([^/]+)$', parent)
parent_dir = m[1]
proc = cmd_pipe(['diff', '-Nurp', f"{TMP_DIR}/{parent_dir}", f"{TMP_DIR}/{patch}"])
for line in proc.stdout:
line = diff_re.sub(r'\1 a/\2 b/\3', line)
line = minus_re.sub(r'--- a/\1', line)
line = plus_re.sub(r'+++ b/\1', line)
fh.write(line)
proc.communicate()
return 1
def run_a_shell(parent, patch):
m = re.search(r'([^/]+)$', parent)
parent_dir = m[1]
os.environ['PS1'] = f"[{parent_dir}] {patch}: "
while True:
s = cmd_run([os.environ.get('SHELL', '/bin/sh')])
if s.returncode != 0:
ans = input("Abort? [n/y] ")
if re.match(r'^y', ans, flags=re.I):
return False
continue
cur_branch, is_clean, status_txt = check_git_status(0)
if is_clean:
break
print(status_txt, end='')
cmd_run('rm -f build/*.o build/*/*.o')
return True
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Turn a git branch back into a diff files in the patches dir.", add_help=False)
parser.add_argument('--branch', '-b', dest='base_branch', metavar='BASE_BRANCH', default='master', help="The branch the patch is based on. Default: master.")
parser.add_argument('--skip-check', action='store_true', help="Skip the check that ensures starting with a clean branch.")
parser.add_argument('--make', '-m', action='store_true', help="Run the smart-make script in every patch branch.")
parser.add_argument('--cmd', '-c', help="Run a command in every patch branch.")
parser.add_argument('--shell', '-s', action='store_true', help="Launch a shell for every patch/BASE/* branch updated, not just when a conflict occurs.")
parser.add_argument('--gen', metavar='DIR', nargs='?', const='', help='Include generated files. Optional DIR value overrides the default of using the "patches" dir.')
parser.add_argument('--patches-dir', '-p', metavar='DIR', default='patches', help="Override the location of the rsync-patches dir. Default: patches.")
parser.add_argument('patch_files', metavar='patches/DIFF_FILE', nargs='*', help="Specify what patch diff files to process. Default: all of them.")
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
args = parser.parse_args()
if args.gen == '':
args.gen = args.patches_dir
elif args.gen is not None:
args.patches_dir = args.gen
main()
# vim: sw=4 et ft=python
+2 -9
View File
@@ -206,14 +206,7 @@ def get_rsync_version():
die("Unable to find RSYNC_VERSION define in version.h")
def get_NEWS_version_info(skip_version=None):
"""Return (last_version, its protocol version, {version: protocol-change date}).
skip_version lets the caller exclude the version it is about to release.
Its NEWS entry may already carry a release date -- dated by hand, or by an
earlier run of --step-3-tweak -- and would otherwise be reported as the
PREVIOUS release, which is both wrong and fatal when it has no table row yet.
"""
def get_NEWS_version_info():
rel_re = re.compile(r'^\| \S{2} \w{3} \d{4}\s+\|\s+(?P<ver>\d+\.\d+\.\d+)\s+\|\s+(?P<pdate>\d{2} \w{3} \d{4})?\s+\|\s+(?P<pver>\d+)\s+\|')
last_version = last_protocol_version = None
pdate = { }
@@ -222,7 +215,7 @@ def get_NEWS_version_info(skip_version=None):
for line in fh:
if not last_version: # Find the first non-dev|pre version with a release date.
m = re.search(r'rsync (\d+\.\d+\.\d+) .*\d\d\d\d', line)
if m and m[1] != skip_version:
if m:
last_version = m[1]
m = rel_re.match(line)
if m:
+414
View File
@@ -0,0 +1,414 @@
#!/usr/bin/env -S python3 -B
# This script expects the directory ~/samba-rsync-ftp to exist and to be a
# copy of the /home/ftp/pub/rsync dir on samba.org. When the script is done,
# the git repository in the current directory will be updated, and the local
# ~/samba-rsync-ftp dir will be ready to be rsynced to samba.org. See the
# script samba-rsync for an easy way to initialize the local ftp copy and to
# thereafter update the remote files from your local copy.
# This script also expects to be able to gpg sign the resulting tar files
# using your default gpg key. Make sure that the html download.html file
# has a link to the relevant keys that are authorized to sign the tar files
# and also make sure that the following commands work as expected:
#
# touch TeMp
# gpg --sign TeMp
# gpg --verify TeMp.gpg
# gpg --sign TeMp
# rm TeMp*
#
# The second time you sign the file it should NOT prompt you for your password
# (unless the timeout period has passed). It will prompt about overriding the
# existing TeMp.gpg file, though.
import os, sys, re, argparse, glob, shutil, signal
from datetime import datetime
from getpass import getpass
sys.path = ['packaging'] + sys.path
from pkglib import *
os.environ['LESS'] = 'mqeiXR'; # Make sure that -F is turned off and -R is turned on.
dest = os.environ['HOME'] + '/samba-rsync-ftp'
ORIGINAL_PATH = os.environ['PATH']
def main():
if not os.path.isfile('packaging/release-rsync'):
die('You must run this script from the top of your rsync checkout.')
now = datetime.now()
cl_today = now.strftime('* %a %b %d %Y')
year = now.strftime('%Y')
ztoday = now.strftime('%d %b %Y')
today = ztoday.lstrip('0')
curdir = os.getcwd()
signal.signal(signal.SIGINT, signal_handler)
if cmd_txt_chk(['packaging/prep-auto-dir']).out == '':
die('You must setup an auto-build-save dir to use this script.');
auto_dir, gen_files = get_gen_files(True)
gen_pathnames = [ os.path.join(auto_dir, fn) for fn in gen_files ]
dash_line = '=' * 74
print(f"""\
{dash_line}
== This will release a new version of rsync onto an unsuspecting world. ==
{dash_line}
""")
with open('build/rsync.1') as fh:
for line in fh:
if line.startswith(r'.\" prefix='):
doc_prefix = line.split('=')[1].strip()
if doc_prefix != '/usr':
warn(f"*** The documentation was built with prefix {doc_prefix} instead of /usr ***")
die("*** Read the md2man script for a way to override this. ***")
break
if line.startswith('.P'):
die("Failed to find the prefix comment at the start of the rsync.1 manpage.")
if not os.path.isdir(dest):
die(dest, "dest does not exist")
if not os.path.isdir('.git'):
die("There is no .git dir in the current directory.")
if os.path.lexists('a'):
die('"a" must not exist in the current directory.')
if os.path.lexists('b'):
die('"b" must not exist in the current directory.')
if os.path.lexists('patches.gen'):
die('"patches.gen" must not exist in the current directory.')
check_git_state(args.master_branch, True, 'patches')
curversion = get_rsync_version()
# All version values are strings!
lastversion, last_protocol_version, pdate = get_NEWS_version_info()
protocol_version, subprotocol_version = get_protocol_versions()
version = curversion
m = re.search(r'pre(\d+)', version)
if m:
version = re.sub(r'pre\d+', 'pre' + str(int(m[1]) + 1), version)
else:
version = version.replace('dev', 'pre1')
ans = input(f"Please enter the version number of this release: [{version}] ")
if ans == '.':
version = re.sub(r'pre\d+', '', version)
elif ans != '':
version = ans
if not re.match(r'^[\d.]+(pre\d+)?$', version):
die(f'Invalid version: "{version}"')
v_ver = 'v' + version
rsync_ver = 'rsync-' + version
if os.path.lexists(rsync_ver):
die(f'"{rsync_ver}" must not exist in the current directory.')
out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out
if out != '':
print(f"Tag {v_ver} already exists.")
ans = input("\nDelete tag or quit? [Q/del] ")
if not re.match(r'^del', ans, flags=re.I):
die("Aborted")
cmd_chk(['git', 'tag', '-d', v_ver])
if os.path.isdir('patches/.git'):
cmd_chk(f"cd patches && git tag -d '{v_ver}'")
version = re.sub(r'[-.]*pre[-.]*', 'pre', version)
if 'pre' in version and not curversion.endswith('dev'):
lastversion = curversion
ans = input(f"Enter the previous version to produce a patch against: [{lastversion}] ")
if ans != '':
lastversion = ans
lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion)
rsync_lastver = 'rsync-' + lastversion
if os.path.lexists(rsync_lastver):
die(f'"{rsync_lastver}" must not exist in the current directory.')
m = re.search(r'(pre\d+)', version)
pre = m[1] if m else ''
release = '0.1' if pre else '1'
ans = input(f"Please enter the RPM release number of this release: [{release}] ")
if ans != '':
release = ans
if pre:
release += '.' + pre
finalversion = re.sub(r'pre\d+', '', version)
proto_changed = protocol_version != last_protocol_version
if proto_changed:
if finalversion in pdate:
proto_change_date = pdate[finalversion]
else:
while True:
ans = input("On what date did the protocol change to {protocol_version} get checked in? (dd Mmm yyyy) ")
if re.match(r'^\d\d \w\w\w \d\d\d\d$', ans):
break
proto_change_date = ans
else:
proto_change_date = ' ' * 11
if 'pre' in lastversion:
if not pre:
die("You should not diff a release version against a pre-release version.")
srcdir = srcdiffdir = lastsrcdir = 'src-previews'
skipping = ' ** SKIPPING **'
elif pre:
srcdir = srcdiffdir = 'src-previews'
lastsrcdir = 'src'
skipping = ' ** SKIPPING **'
else:
srcdir = lastsrcdir = 'src'
srcdiffdir = 'src-diffs'
skipping = ''
print(f"""
{dash_line}
version is "{version}"
lastversion is "{lastversion}"
dest is "{dest}"
curdir is "{curdir}"
srcdir is "{srcdir}"
srcdiffdir is "{srcdiffdir}"
lastsrcdir is "{lastsrcdir}"
release is "{release}"
About to:
- tweak SUBPROTOCOL_VERSION in rsync.h, if needed
- tweak the version in version.h and the spec files
- tweak NEWS.md to ensure header values are correct
- generate configure.sh, config.h.in, and proto.h
- page through the differences
""")
ans = input("<Press Enter to continue> ")
specvars = {
'Version:': finalversion,
'Release:': release,
'%define fullversion': f'%{{version}}{pre}',
'Released': version + '.',
'%define srcdir': srcdir,
}
tweak_files = 'version.h rsync.h'.split()
tweak_files += glob.glob('packaging/*.spec')
tweak_files += glob.glob('packaging/*/*.spec')
for fn in tweak_files:
with open(fn, 'r', encoding='utf-8') as fh:
old_txt = txt = fh.read()
if fn == 'version.h':
x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M)
msg = f"Unable to update RSYNC_VERSION in {fn}"
txt = replace_or_die(x_re, r'\1 "%s"' % version, txt, msg)
elif '.spec' in fn:
for var, val in specvars.items():
x_re = re.compile(r'^%s .*' % re.escape(var), re.M)
txt = replace_or_die(x_re, var + ' ' + val, txt, f"Unable to update {var} in {fn}")
x_re = re.compile(r'^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)', re.M)
txt = replace_or_die(x_re, r'%s \1' % cl_today, txt, f"Unable to update ChangeLog header in {fn}")
elif fn == 'rsync.h':
x_re = re.compile('(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)')
repl = lambda m: m[1] + ' ' + ('0' if not pre or not proto_changed else '1' if m[2] == '0' else m[2])
txt = replace_or_die(x_re, repl, txt, f"Unable to find SUBPROTOCOL_VERSION define in {fn}")
elif fn == 'NEWS.md':
efv = re.escape(finalversion)
x_re = re.compile(r'^# NEWS for rsync %s \(UNRELEASED\)\s+## Changes in this version:\n' % efv
+ r'(\n### PROTOCOL NUMBER:\s+- The protocol number was changed to \d+\.\n)?')
rel_day = 'UNRELEASED' if pre else today
repl = (f'# NEWS for rsync {finalversion} ({rel_day})\n\n'
+ '## Changes in this version:\n')
if proto_changed:
repl += f'\n### PROTOCOL NUMBER:\n\n - The protocol number was changed to {protocol_version}.\n'
good_top = re.sub(r'\(.*?\)', '(UNRELEASED)', repl, 1)
msg = f"The top lines of {fn} are not in the right format. It should be:\n" + good_top
txt = replace_or_die(x_re, repl, txt, msg)
x_re = re.compile(r'^(\| )(\S{2} \S{3} \d{4})(\s+\|\s+%s\s+\| ).{11}(\s+\| )\S{2}(\s+\|+)$' % efv, re.M)
repl = lambda m: m[1] + (m[2] if pre else ztoday) + m[3] + proto_change_date + m[4] + protocol_version + m[5]
txt = replace_or_die(x_re, repl, txt, f'Unable to find "| ?? ??? {year} | {finalversion} | ... |" line in {fn}')
else:
die(f"Unrecognized file in tweak_files: {fn}")
if txt != old_txt:
print(f"Updating {fn}")
with open(fn, 'w', encoding='utf-8') as fh:
fh.write(txt)
cmd_chk(['packaging/year-tweak'])
print(dash_line)
cmd_run("git diff".split())
srctar_name = f"{rsync_ver}.tar.gz"
pattar_name = f"rsync-patches-{version}.tar.gz"
diff_name = f"{rsync_lastver}-{version}.diffs.gz"
srctar_file = os.path.join(dest, srcdir, srctar_name)
pattar_file = os.path.join(dest, srcdir, pattar_name)
diff_file = os.path.join(dest, srcdiffdir, diff_name)
lasttar_file = os.path.join(dest, lastsrcdir, rsync_lastver + '.tar.gz')
print(f"""\
{dash_line}
About to:
- git commit all changes
- run a full build, ensuring that the manpages & configure.sh are up-to-date
- merge the {args.master_branch} branch into the patch/{args.master_branch}/* branches
- update the files in the "patches" dir and OPTIONALLY (if you type 'y') to
run patch-update with the --make option (which opens a shell on error)
""")
ans = input("<Press Enter OR 'y' to continue> ")
s = cmd_run(['git', 'commit', '-a', '-m', f'Preparing for release of {version} [buildall]'])
if s.returncode:
die('Aborting')
cmd_chk('touch configure.ac && packaging/smart-make && make gen')
print('Creating any missing patch branches.')
s = cmd_run(f'packaging/branch-from-patch --branch={args.master_branch} --add-missing')
if s.returncode:
die('Aborting')
print('Updating files in "patches" dir ...')
s = cmd_run(f'packaging/patch-update --branch={args.master_branch}')
if s.returncode:
die('Aborting')
if re.match(r'^y', ans, re.I):
print(f'\nRunning smart-make on all "patch/{args.master_branch}/*" branches ...')
cmd_run(f"packaging/patch-update --branch={args.master_branch} --skip-check --make")
if os.path.isdir('patches/.git'):
s = cmd_run(f"cd patches && git commit -a -m 'The patches for {version}.'")
if s.returncode:
die('Aborting')
print(f"""\
{dash_line}
About to:
- create signed tag for this release: {v_ver}
- create release diffs, "{diff_name}"
- create release tar, "{srctar_name}"
- generate {rsync_ver}/patches/* files
- create patches tar, "{pattar_name}"
- update top-level README.md, NEWS.md, TODO, and ChangeLog
- update top-level rsync*.html manpages
- gpg-sign the release files
- update hard-linked top-level release files{skipping}
""")
ans = input("<Press Enter to continue> ")
# TODO: is there a better way to ensure that our passphrase is in the agent?
cmd_run("touch TeMp; gpg --sign TeMp; rm TeMp*")
out = cmd_txt(f"git tag -s -m 'Version {version}.' {v_ver}", capture='combined').out
print(out, end='')
if 'bad passphrase' in out or 'failed' in out:
die('Aborting')
if os.path.isdir('patches/.git'):
out = cmd_txt(f"cd patches && git tag -s -m 'Version {version}.' {v_ver}", capture='combined').out
print(out, end='')
if 'bad passphrase' in out or 'failed' in out:
die('Aborting')
os.environ['PATH'] = ORIGINAL_PATH
# Extract the generated files from the old tar.
tweaked_gen_files = [ os.path.join(rsync_lastver, fn) for fn in gen_files ]
cmd_run(['tar', 'xzf', lasttar_file, *tweaked_gen_files])
os.rename(rsync_lastver, 'a')
print(f"Creating {diff_file} ...")
cmd_chk(['rsync', '-a', *gen_pathnames, 'b/'])
sed_script = r's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:' # CAUTION: must not contain any single quotes!
cmd_chk(f"(git diff v{lastversion} {v_ver} -- ':!.github'; diff -upN a b | sed -r '{sed_script}') | gzip -9 >{diff_file}")
shutil.rmtree('a')
os.rename('b', rsync_ver)
print(f"Creating {srctar_file} ...")
cmd_chk(f"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | tar xf -")
cmd_chk(f"support/git-set-file-times --quiet --prefix={rsync_ver}/")
cmd_chk(['fakeroot', 'tar', 'czf', srctar_file, '--exclude=.github', rsync_ver])
shutil.rmtree(rsync_ver)
print(f'Updating files in "{rsync_ver}/patches" dir ...')
os.mkdir(rsync_ver, 0o755)
os.mkdir(f"{rsync_ver}/patches", 0o755)
cmd_chk(f"packaging/patch-update --skip-check --branch={args.master_branch} --gen={rsync_ver}/patches".split())
print(f"Creating {pattar_file} ...")
cmd_chk(['fakeroot', 'tar', 'chzf', pattar_file, rsync_ver + '/patches'])
shutil.rmtree(rsync_ver)
print(f"Updating the other files in {dest} ...")
md_files = 'README.md NEWS.md INSTALL.md'.split()
html_files = [ fn for fn in gen_pathnames if fn.endswith('.html') ]
cmd_chk(['rsync', '-a', *md_files, *html_files, dest])
cmd_chk(["./md-convert", "--dest", dest, *md_files])
cmd_chk(f"git log --name-status | gzip -9 >{dest}/ChangeLog.gz")
for fn in (srctar_file, pattar_file, diff_file):
asc_fn = fn + '.asc'
if os.path.lexists(asc_fn):
os.unlink(asc_fn)
res = cmd_run(['gpg', '--batch', '-ba', fn])
if res.returncode != 0 and res.returncode != 2:
die("gpg signing failed")
if not pre:
for find in f'{dest}/rsync-*.gz {dest}/rsync-*.asc {dest}/src-previews/rsync-*diffs.gz*'.split():
for fn in glob.glob(find):
os.unlink(fn)
top_link = [
srctar_file, f"{srctar_file}.asc",
pattar_file, f"{pattar_file}.asc",
diff_file, f"{diff_file}.asc",
]
for fn in top_link:
os.link(fn, re.sub(r'/src(-\w+)?/', '/', fn))
print(f"""\
{dash_line}
Local changes are done. When you're satisfied, push the git repository
and rsync the release files. Remember to announce the release on *BOTH*
rsync-announce@lists.samba.org and rsync@lists.samba.org (and the web)!
""")
def replace_or_die(regex, repl, txt, die_msg):
m = regex.search(txt)
if not m:
die(die_msg)
return regex.sub(repl, txt, 1)
def signal_handler(sig, frame):
die("\nAborting due to SIGINT.")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Prepare a new release of rsync in the git repo & ftp dir.", add_help=False)
parser.add_argument('--branch', '-b', dest='master_branch', default='master', help="The branch to release. Default: master.")
parser.add_argument("--help", "-h", action="help", help="Output this help message and exit.")
args = parser.parse_args()
main()
# vim: sw=4 et ft=python
-714
View File
@@ -1,714 +0,0 @@
#!/usr/bin/env python3
# Step-based release script for rsync. Each step is a separate invocation
# selected by a --step-N-XX option, so the maintainer drives the release
# manually one piece at a time.
#
# All persistent state and working files live in ../release/ (a sibling of
# the rsync git checkout):
#
# ../release/rsync-ftp/ mirror of samba.org:/home/ftp/pub/rsync
# ../release/rsync-html/ release-time snapshot of the html site
# ../release/work/ scratch space for tarball / diff staging
# ../release/release-state.json info shared between steps
#
# The rsync-patches archive is no longer maintained and has been dropped.
#
# Run "packaging/release.py --list" to see the step list.
import os, sys, re, argparse, glob, shutil, json, signal, subprocess
from datetime import datetime
sys.path = ['packaging'] + sys.path
from pkglib import (
warn, die, cmd_run, cmd_chk, cmd_txt, cmd_txt_chk, cmd_pipe,
check_git_state, get_rsync_version,
get_NEWS_version_info, get_protocol_versions,
)
# ---------- Paths ----------
RELEASE_DIR = os.path.realpath('../release')
FTP_DIR = os.path.join(RELEASE_DIR, 'rsync-ftp')
HTML_DIR = os.path.join(RELEASE_DIR, 'rsync-html')
WORK_DIR = os.path.join(RELEASE_DIR, 'work')
STATE_FILE = os.path.join(RELEASE_DIR, 'release-state.json')
# The rsync-web/ subdirectory in the rsync source tree is the source-of-truth
# for the git-tracked html content. step-1-fetch snapshots it into HTML_DIR
# for the release flow, where it can be edited or augmented with server-side
# content before step-11-push-html sends it to samba.org.
HTML_SRC = os.path.realpath('rsync-web')
FTP_REMOTE_PATH = '/home/ftp/pub/rsync'
HTML_REMOTE_PATH = '/home/httpd/html/rsync'
# Files that ./configure + make produce and that the release tarball / diff
# need to bundle alongside the git-tracked source. Mirrors the GENFILES
# definition in Makefile.in (with rrsync.1{,.html} since we always configure
# --with-rrsync in --step-4-build).
GEN_FILES = [
'configure.sh',
'aclocal.m4',
'config.h.in',
'rsync.1', 'rsync.1.html',
'rsync-ssl.1', 'rsync-ssl.1.html',
'rsyncd.conf.5', 'rsyncd.conf.5.html',
'rrsync.1', 'rrsync.1.html',
]
# ---------- Step registry ----------
STEPS = [
('step-1-fetch', 'mirror ../release/rsync-ftp from samba.org and snapshot ../release/rsync-html from rsync-web/'),
('step-2-prepare', 'gather release info interactively and write release-state.json'),
('step-3-tweak', 'update version.h, rsync.h, NEWS.md, and packaging/*.spec'),
('step-4-build', 'run smart-make + make gen'),
('step-5-commit', 'git commit -a (commit the prepared release changes)'),
('step-6-tag', 'create the gpg-signed git tag'),
('step-7-tarball', 'build the source tarball and diffs.gz against the previous release'),
('step-8-update-ftp', 'refresh README/NEWS/INSTALL/html in the ftp dir, regen ChangeLog.gz, gpg-sign tarballs'),
('step-9-toplinks', 'hard-link top-level release files (final releases only)'),
('step-10-push-ftp', 'rsync ../release/rsync-ftp/ to samba.org'),
('step-11-push-html', 'rsync ../release/rsync-html/ to samba.org (after any manual edits)'),
('step-12-push-git', 'print the git push commands for you to run'),
]
STEP_FLAGS = [s[0] for s in STEPS]
DASH_LINE = '=' * 74
# ---------- State helpers ----------
def load_state():
if not os.path.isfile(STATE_FILE):
die(f"{STATE_FILE} not found. Run --step-2-prepare first.")
with open(STATE_FILE, 'r', encoding='utf-8') as fh:
return json.load(fh)
def save_state(state):
os.makedirs(RELEASE_DIR, exist_ok=True)
with open(STATE_FILE, 'w', encoding='utf-8') as fh:
json.dump(state, fh, indent=2, sort_keys=True)
fh.write('\n')
def require_samba_host():
host = os.environ.get('RSYNC_SAMBA_HOST', '')
if not host.endswith('.samba.org'):
die("Set RSYNC_SAMBA_HOST in your environment to the samba hostname (e.g. hr3.samba.org).")
return 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).")
def replace_or_die(regex, repl, txt, die_msg):
m = regex.search(txt)
if not m:
die(die_msg)
return regex.sub(repl, txt, 1)
def section(title):
print(f"\n{DASH_LINE}\n== {title}\n{DASH_LINE}")
def confirm(prompt, default_no=True):
suffix = '[n] ' if default_no else '[y] '
ans = input(f"{prompt} {suffix}").strip().lower()
if default_no:
return ans.startswith('y')
return ans == '' or ans.startswith('y')
# ---------- Step 1: fetch ftp + html ----------
def step_1_fetch(args):
host = require_samba_host()
os.makedirs(RELEASE_DIR, exist_ok=True)
os.makedirs(WORK_DIR, exist_ok=True)
section(f"Fetching ftp dir into {FTP_DIR}")
if not os.path.isdir(FTP_DIR):
os.makedirs(FTP_DIR)
# packaging/ftp.filt is the authoritative copy of the .filt filter file
# that controls which subtrees rsync excludes from the FTP mirror.
# Seed FTP_DIR/.filt from it so the bundled version is what step-1's
# rsync uses here, and so step-10-push-ftp propagates it back to the
# server. --exclude=/.filt below stops the server's copy from
# overwriting our bundled one on the way down.
filt = os.path.join(FTP_DIR, '.filt')
bundled_filt = os.path.realpath('packaging/ftp.filt')
if not os.path.isfile(bundled_filt):
die(f"{bundled_filt} not found; cannot seed .filt for the FTP pull.")
shutil.copyfile(bundled_filt, filt)
cmd_chk(['rsync', '-aivOHP', f'-f:_{filt}', '--exclude=/.filt',
f'{host}:{FTP_REMOTE_PATH}/', f'{FTP_DIR}/'])
section(f"Snapshotting html dir from {HTML_SRC} into {HTML_DIR}")
if not os.path.isdir(HTML_SRC):
die(f"{HTML_SRC} not found. This should be the in-tree rsync-web/ "
f"subdirectory; something is wrong with your checkout.")
os.makedirs(HTML_DIR, exist_ok=True)
cmd_chk(['rsync', '-aiv', f'{HTML_SRC}/', f'{HTML_DIR}/'])
# Then mirror non-git html content from the server, skipping files that
# the html git already provides (driven by the 'filt' file in HTML_DIR).
filt = os.path.join(HTML_DIR, 'filt')
if os.path.exists(filt):
tmp_filt = os.path.join(HTML_DIR, 'tmp-filt')
cmd_chk(f"sed -n -e 's/[-P]/H/p' '{filt}' >'{tmp_filt}'")
cmd_chk(['rsync', '-aivOHP', f'-f._{tmp_filt}',
f'{host}:{HTML_REMOTE_PATH}/', f'{HTML_DIR}/'])
os.unlink(tmp_filt)
print(f"\nFetch complete. Local dirs are now in {RELEASE_DIR}.")
# ---------- Step 2: prepare ----------
def step_2_prepare(args):
require_top_of_checkout()
os.makedirs(RELEASE_DIR, exist_ok=True)
if not os.path.isdir(FTP_DIR):
die(f"{FTP_DIR} does not exist. Run --step-1-fetch first.")
now = datetime.now().astimezone()
cl_today = now.strftime('* %a %b %d %Y')
year = now.strftime('%Y')
ztoday = now.strftime('%d %b %Y')
today = ztoday.lstrip('0')
tz_now = now.strftime('%z')
tz_num = tz_now[0:1].replace('+', '') + str(float(tz_now[1:3]) + float(tz_now[3:]) / 60)
curversion = get_rsync_version()
# Skip the version we are releasing: its NEWS entry may already be dated,
# in which case it would otherwise be taken for the previous release.
lastversion, last_protocol_version, pdate = get_NEWS_version_info(
skip_version=re.sub(r'(pre\d+|dev)$', '', curversion))
protocol_version, subprotocol_version = get_protocol_versions()
# Default next version: bump preN, or move dev -> pre1.
version = curversion
m = re.search(r'pre(\d+)', version)
if m:
version = re.sub(r'pre\d+', 'pre' + str(int(m[1]) + 1), version)
else:
version = version.replace('dev', 'pre1')
print(f"\nCurrent version (version.h): {curversion}")
print(f"Last released version (NEWS.md): {lastversion}")
print(f"Current protocol version: {protocol_version} (last released: {last_protocol_version})")
ans = input(f"\nVersion to release [{version}, '.' to drop the preN suffix]: ").strip()
if ans == '.':
version = re.sub(r'pre\d+', '', version)
elif ans:
version = ans
if not re.match(r'^[\d.]+(pre\d+)?$', version):
die(f'Invalid version: "{version}"')
version = re.sub(r'[-.]*pre[-.]*', 'pre', version)
if 'pre' in version and not curversion.endswith('dev'):
lastversion = curversion
ans = input(f"Previous version to diff against [{lastversion}]: ").strip()
if ans:
lastversion = ans
lastversion = re.sub(r'[-.]*pre[-.]*', 'pre', lastversion)
m = re.search(r'(pre\d+)', version)
pre = m[1] if m else ''
finalversion = re.sub(r'pre\d+', '', version)
release = '0.1' if pre else '1'
ans = input(f"RPM release number [{release}]: ").strip()
if ans:
release = ans
if pre:
release += '.' + pre
proto_changed = protocol_version != last_protocol_version
if proto_changed:
if finalversion in pdate:
proto_change_date = pdate[finalversion]
else:
while True:
ans = input(f"Date the protocol changed to {protocol_version} (dd Mmm yyyy): ").strip()
if re.match(r'^\d\d \w\w\w \d\d\d\d$', ans):
break
proto_change_date = ans
else:
proto_change_date = ' ' * 11
if 'pre' in lastversion:
if not pre:
die("Refusing to diff a release version against a pre-release version.")
srcdir = srcdiffdir = lastsrcdir = 'src-previews'
elif pre:
srcdir = srcdiffdir = 'src-previews'
lastsrcdir = 'src'
else:
srcdir = lastsrcdir = 'src'
srcdiffdir = 'src-diffs'
state = {
'version': version,
'lastversion': lastversion,
'finalversion': finalversion,
'pre': pre,
'release': release,
'protocol_version': protocol_version,
'subprotocol_version': subprotocol_version,
'proto_changed': proto_changed,
'proto_change_date': proto_change_date,
'srcdir': srcdir,
'srcdiffdir': srcdiffdir,
'lastsrcdir': lastsrcdir,
'today': today,
'ztoday': ztoday,
'cl_today': cl_today,
'year': year,
'tz_num': tz_num,
'master_branch': args.master_branch,
}
save_state(state)
section("Release info")
for k in ('version', 'lastversion', 'release', 'srcdir', 'srcdiffdir', 'lastsrcdir',
'protocol_version', 'proto_changed', 'proto_change_date'):
print(f" {k}: {state[k]}")
print(f"\nWrote {STATE_FILE}. Re-run --step-2-prepare to change anything.")
# ---------- Step 3: tweak version files ----------
def step_3_tweak(args):
require_top_of_checkout()
state = load_state()
version = state['version']
finalversion = state['finalversion']
pre = state['pre']
release = state['release']
today = state['today']
ztoday = state['ztoday']
cl_today = state['cl_today']
year = state['year']
tz_num = state['tz_num']
proto_changed = state['proto_changed']
proto_change_date = state['proto_change_date']
protocol_version = state['protocol_version']
srcdir = state['srcdir']
specvars = {
'Version:': finalversion,
'Release:': release,
'%define fullversion': f'%{{version}}{pre}',
'Released': version + '.',
'%define srcdir': srcdir,
}
tweak_files = ['version.h', 'rsync.h', 'NEWS.md']
tweak_files += glob.glob('packaging/*.spec')
tweak_files += glob.glob('packaging/*/*.spec')
for fn in tweak_files:
with open(fn, 'r', encoding='utf-8') as fh:
old_txt = txt = fh.read()
if fn == 'version.h':
x_re = re.compile(r'^(#define RSYNC_VERSION).*', re.M)
txt = replace_or_die(x_re, r'\1 "%s"' % version, txt,
f"Unable to update RSYNC_VERSION in {fn}")
x_re = re.compile(r'^(#define MAINTAINER_TZ_OFFSET).*', re.M)
txt = replace_or_die(x_re, r'\1 ' + tz_num, txt,
f"Unable to update MAINTAINER_TZ_OFFSET in {fn}")
elif fn == 'rsync.h':
x_re = re.compile(r'(#define\s+SUBPROTOCOL_VERSION)\s+(\d+)')
repl = lambda m: m[1] + ' ' + (
'0' if not pre or not proto_changed
else '1' if m[2] == '0'
else m[2])
txt = replace_or_die(x_re, repl, txt,
f"Unable to find SUBPROTOCOL_VERSION in {fn}")
elif fn == 'NEWS.md':
efv = re.escape(finalversion)
# Accept either "(UNRELEASED)" or an already-filled date, so a
# release entry that was dated by hand (or by an earlier run of
# this step) does not have to be reverted before releasing.
x_re = re.compile(
r'^# NEWS for rsync %s \((?:UNRELEASED|\d+ \w{3} \d{4})\)\s+## Changes in this version:\n' % efv
+ r'(\n### PROTOCOL NUMBER:\s+- The protocol number was changed to \d+\.\n)?')
rel_day = 'UNRELEASED' if pre else today
repl = (f'# NEWS for rsync {finalversion} ({rel_day})\n\n'
+ '## Changes in this version:\n')
if proto_changed:
repl += f'\n### PROTOCOL NUMBER:\n\n - The protocol number was changed to {protocol_version}.\n'
good_top = re.sub(r'\(.*?\)', '(UNRELEASED)', repl, 1)
msg = (f"The top of {fn} is not in the right format. It should be:\n" + good_top
+ "(an already-filled release date in place of UNRELEASED is also accepted)")
txt = replace_or_die(x_re, repl, txt, msg)
x_re = re.compile(
r'^(\| )(\S{2} \S{3} \d{4})(\s+\|\s+%s\s+\| ).{11}(\s+\| )\S{2}(\s+\|+)$' % efv,
re.M)
repl = lambda m: (m[1] + (m[2] if pre else ztoday) + m[3]
+ proto_change_date + m[4] + protocol_version + m[5])
txt = replace_or_die(x_re, repl, txt,
f'Unable to find "| ?? ??? {year} | {finalversion} | ... |" line in {fn}')
elif '.spec' in fn:
for var, val in specvars.items():
x_re = re.compile(r'^%s .*' % re.escape(var), re.M)
txt = replace_or_die(x_re, var + ' ' + val, txt,
f"Unable to update {var} in {fn}")
x_re = re.compile(r'^\* \w\w\w \w\w\w \d\d \d\d\d\d (.*)', re.M)
txt = replace_or_die(x_re, r'%s \1' % cl_today, txt,
f"Unable to update ChangeLog header in {fn}")
else:
die(f"Unrecognized file in tweak_files: {fn}")
if txt != old_txt:
print(f"Updating {fn}")
with open(fn, 'w', encoding='utf-8') as fh:
fh.write(txt)
cmd_chk(['packaging/year-tweak'])
section("git diff after tweaks")
cmd_run(['git', '--no-pager', 'diff'])
# ---------- Step 4: build ----------
def step_4_build(args):
require_top_of_checkout()
load_state() # just to ensure we've prepared
section("Running prepare-source + configure --prefix=/usr --with-rrsync + make + make gen")
# Always re-prepare so configure.sh is current; we run configure ourselves
# with the release-required flags rather than relying on the cached
# config.status (which may have been produced with different options).
if os.path.isfile('.fetch'):
cmd_chk(['./prepare-source', 'fetch'])
else:
cmd_chk(['./prepare-source'])
cmd_chk(['./configure', '--prefix=/usr', '--with-rrsync'])
cmd_chk(['make'])
cmd_chk(['make', 'gen'])
# ---------- Step 5: commit ----------
def step_5_commit(args):
require_top_of_checkout()
state = load_state()
version = state['version']
section("git status")
cmd_run(['git', 'status'])
if not confirm("Commit all current changes with the release message?"):
die("Aborted.")
cmd_chk(['git', 'commit', '-a', '-m', f'Preparing for release of {version} [buildall]'])
# ---------- Step 6: tag ----------
def step_6_tag(args):
require_top_of_checkout()
state = load_state()
version = state['version']
v_ver = 'v' + version
out = cmd_txt_chk(['git', 'tag', '-l', v_ver]).out
if out.strip():
if not confirm(f"Tag {v_ver} already exists. Delete and recreate?"):
die("Aborted.")
cmd_chk(['git', 'tag', '-d', v_ver])
# Prime the gpg agent so the actual tag signing won't prompt.
section("Priming gpg agent")
cmd_run("touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg")
section(f"Creating signed tag {v_ver}")
out = cmd_txt(['git', 'tag', '-s', '-m', f'Version {version}.', v_ver],
capture='combined').out
print(out, end='')
if 'bad passphrase' in out.lower() or 'failed' in out.lower():
die("Tag creation failed.")
# ---------- Step 7: tarball + diff ----------
def step_7_tarball(args):
require_top_of_checkout()
state = load_state()
version = state['version']
lastversion = state['lastversion']
pre = state['pre']
srcdir = state['srcdir']
srcdiffdir = state['srcdiffdir']
lastsrcdir = state['lastsrcdir']
rsync_ver = 'rsync-' + version
rsync_lastver = 'rsync-' + lastversion
v_ver = 'v' + version
srctar_name = f"{rsync_ver}.tar.gz"
diff_name = f"{rsync_lastver}-{version}.diffs.gz"
srctar_file = os.path.join(FTP_DIR, srcdir, srctar_name)
diff_file = os.path.join(FTP_DIR, srcdiffdir, diff_name)
lasttar_file = os.path.join(FTP_DIR, lastsrcdir, rsync_lastver + '.tar.gz')
for d in (os.path.dirname(srctar_file), os.path.dirname(diff_file)):
os.makedirs(d, exist_ok=True)
if not os.path.isfile(lasttar_file):
die(f"Previous tarball not found: {lasttar_file}")
# Stage in ../release/work to keep the source checkout clean.
if os.path.isdir(WORK_DIR):
shutil.rmtree(WORK_DIR)
os.makedirs(WORK_DIR)
a_dir = os.path.join(WORK_DIR, 'a')
b_dir = os.path.join(WORK_DIR, 'b')
# Extract gen files from the previous tarball into work/a/.
tweaked_gen_files = [os.path.join(rsync_lastver, fn) for fn in GEN_FILES]
cmd_chk(['tar', '-C', WORK_DIR, '-xzf', lasttar_file, *tweaked_gen_files])
os.rename(os.path.join(WORK_DIR, rsync_lastver), a_dir)
# Copy current gen files (built in the top-level checkout) into work/b/.
os.makedirs(b_dir)
cmd_chk(['rsync', '-a', *GEN_FILES, b_dir + '/'])
section(f"Creating {diff_file}")
sed_script = r's:^((---|\+\+\+) [ab]/[^\t]+)\t.*:\1:' # no single quotes!
cmd_chk(
f"(git diff v{lastversion} {v_ver} -- ':!.github'; "
f"diff -upN {a_dir} {b_dir} | sed -r '{sed_script}') | gzip -9 >{diff_file}")
section(f"Creating {srctar_file}")
# Reuse work/b/ (which already holds the fresh gen files) as the release
# staging dir, then let "git archive" overlay the git-tracked source files
# on top. That way the tarball ends up with both gen files and source.
rsync_ver_dir = os.path.join(WORK_DIR, rsync_ver)
shutil.rmtree(a_dir)
os.rename(b_dir, rsync_ver_dir)
cmd_chk(f"git archive --format=tar --prefix={rsync_ver}/ {v_ver} | "
f"tar -C {WORK_DIR} -xf -")
cmd_chk(f"support/git-set-file-times --quiet --prefix={rsync_ver_dir}/")
cmd_chk(['fakeroot', 'tar', '-C', WORK_DIR, '-czf', srctar_file,
'--exclude=.github', rsync_ver])
# Leave staging in place; --step-8-update-ftp does its own thing.
print(f"\nCreated:\n {srctar_file}\n {diff_file}")
# ---------- Step 8: update ftp ----------
def step_8_update_ftp(args):
require_top_of_checkout()
state = load_state()
version = state['version']
lastversion = state['lastversion']
srcdir = state['srcdir']
srcdiffdir = state['srcdiffdir']
rsync_ver = 'rsync-' + version
rsync_lastver = 'rsync-' + lastversion
srctar_file = os.path.join(FTP_DIR, srcdir, f"{rsync_ver}.tar.gz")
diff_file = os.path.join(FTP_DIR, srcdiffdir,
f"{rsync_lastver}-{version}.diffs.gz")
section(f"Refreshing top-of-tree files in {FTP_DIR}")
md_files = ['README.md', 'NEWS.md', 'INSTALL.md']
html_files = [fn for fn in GEN_FILES if fn.endswith('.html')]
cmd_chk(['rsync', '-a', *md_files, *html_files, FTP_DIR + '/'])
cmd_chk(['./md-convert', '--dest', FTP_DIR, *md_files])
section(f"Regenerating {FTP_DIR}/ChangeLog.gz")
cmd_chk(f"git log --name-status | gzip -9 >{FTP_DIR}/ChangeLog.gz")
# Prime gpg agent and then sign the tar + diff.
section("Priming gpg agent")
cmd_run("touch TeMp; gpg --sign TeMp; rm -f TeMp TeMp.gpg")
for fn in (srctar_file, diff_file):
if not os.path.isfile(fn):
die(f"Missing file to sign: {fn}. Did --step-7-tarball run successfully?")
asc_fn = fn + '.asc'
if os.path.lexists(asc_fn):
os.unlink(asc_fn)
section(f"GPG-signing {fn}")
res = cmd_run(['gpg', '--batch', '-ba', fn])
if res.returncode not in (0, 2):
die("gpg signing failed.")
# ---------- Step 9: top-level hard links ----------
def step_9_toplinks(args):
require_top_of_checkout()
state = load_state()
pre = state['pre']
if pre:
print("Skipping: pre-releases do not get top-level hard links.")
return
version = state['version']
lastversion = state['lastversion']
srcdir = state['srcdir']
srcdiffdir = state['srcdiffdir']
rsync_ver = 'rsync-' + version
rsync_lastver = 'rsync-' + lastversion
srctar_file = os.path.join(FTP_DIR, srcdir, f"{rsync_ver}.tar.gz")
diff_file = os.path.join(FTP_DIR, srcdiffdir,
f"{rsync_lastver}-{version}.diffs.gz")
section("Removing stale top-level rsync-* files")
for find in [f'{FTP_DIR}/rsync-*.gz',
f'{FTP_DIR}/rsync-*.asc',
f'{FTP_DIR}/src-previews/rsync-*diffs.gz*']:
for fn in glob.glob(find):
os.unlink(fn)
top_link = [
srctar_file, srctar_file + '.asc',
diff_file, diff_file + '.asc',
]
for fn in top_link:
target = re.sub(r'/src(-\w+)?/', '/', fn)
if os.path.lexists(target):
os.unlink(target)
os.link(fn, target)
print(f" linked {target}")
# ---------- Step 10: push ftp ----------
def step_10_push_ftp(args):
host = require_samba_host()
if not os.path.isdir(FTP_DIR):
die(f"{FTP_DIR} does not exist. Run --step-1-fetch first.")
section(f"rsync ftp dir to {host}")
rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del',
f'-f._{os.path.join(FTP_DIR, ".filt")}',
f'{FTP_DIR}/', f'{host}:{FTP_REMOTE_PATH}/'])
# ---------- Step 11: push html ----------
def step_11_push_html(args):
host = require_samba_host()
if not os.path.isdir(HTML_DIR):
die(f"{HTML_DIR} does not exist. Run --step-1-fetch first.")
section(f"rsync html dir to {host}")
filt = os.path.join(HTML_DIR, 'filt')
rsync_with_confirm(['-aivOHP', '--chown=:rsync', '--del',
f'-f._{filt}',
f'{HTML_DIR}/', f'{host}:{HTML_REMOTE_PATH}/'])
# ---------- Step 12: print push-git instructions ----------
def step_12_push_git(args):
state = load_state()
version = state['version']
master_branch = state['master_branch']
v_ver = 'v' + version
print(f"""\
{DASH_LINE}
Run these from the rsync-git checkout (this script does not push for you):
git push origin {master_branch}
git push origin {v_ver}
If you have a 'samba' remote configured (git.samba.org:/data/git/rsync.git):
git push samba {master_branch}
git push samba {v_ver}
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
""")
# ---------- shared rsync-with-confirm ----------
def rsync_with_confirm(rsync_args):
"""Run an rsync command in dry-run mode, then ask before running for real."""
cmd_run(['rsync', '--dry-run', *rsync_args])
if confirm("Run without --dry-run?"):
cmd_run(['rsync', *rsync_args])
# ---------- dispatch ----------
STEP_FUNCS = {
'step-1-fetch': step_1_fetch,
'step-2-prepare': step_2_prepare,
'step-3-tweak': step_3_tweak,
'step-4-build': step_4_build,
'step-5-commit': step_5_commit,
'step-6-tag': step_6_tag,
'step-7-tarball': step_7_tarball,
'step-8-update-ftp': step_8_update_ftp,
'step-9-toplinks': step_9_toplinks,
'step-10-push-ftp': step_10_push_ftp,
'step-11-push-html': step_11_push_html,
'step-12-push-git': step_12_push_git,
}
def signal_handler(sig, frame):
die("\nAborting due to SIGINT.")
def main():
parser = argparse.ArgumentParser(
description="Step-based release script for rsync.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="Run --list to see the steps. Each invocation runs exactly one --step-* option.")
parser.add_argument('--branch', '-b', dest='master_branch', default='master',
help="The branch to release (default: master).")
parser.add_argument('--list', action='store_true',
help="List all release steps and exit.")
grp = parser.add_mutually_exclusive_group()
for flag, descr in STEPS:
grp.add_argument('--' + flag, dest='step', action='store_const',
const=flag, help=descr)
args = parser.parse_args()
if args.list:
print("Release steps:")
for flag, descr in STEPS:
print(f" --{flag:18s} {descr}")
return
if not args.step:
parser.error("pick one --step-N-XX option (or --list to see them).")
signal.signal(signal.SIGINT, signal_handler)
os.environ['LESS'] = 'mqeiXR'
STEP_FUNCS[args.step](args)
if __name__ == '__main__':
main()
# vim: sw=4 et ft=python
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# This script makes it easy to update the ftp & html directories on the samba.org server.
# It expects the 2 *_DEST directories to contain updated files that need to be sent to
# the remote server. If these directories don't exist yet, they will be copied from the
# remote server (while also making the html dir a git checkout).
FTP_SRC="$HOME/samba-rsync-ftp"
HTML_SRC="$HOME/samba-rsync-html"
FTP_DEST="/home/ftp/pub/rsync"
HTML_DEST="/home/httpd/html/rsync"
HTML_GIT='git.samba.org:/data/git/rsync-web.git'
export RSYNC_PARTIAL_DIR=''
case "$RSYNC_SAMBA_HOST" in
*.samba.org) ;;
*)
echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2
exit 1
;;
esac
MODE=''
REVERSE=''
while (( $# )); do
case "$1" in
-R|--reverse) REVERSE=yes ;;
f|ftp) MODE=ftp ;;
h|html) MODE=html ;;
-h|--help)
echo "Usage: [-R] [f|ftp|h|html]"
echo "-R --reverse Copy the files from the server to the local host."
echo " The default is to update the remote files."
echo "-h --help Output this help message."
echo " "
echo "The script will prompt if ftp or html is not specified on the command line."
echo "Only one category can be copied at a time. When pulling html files, a git"
echo "checkout will be either created or updated prior to the rsync copy."
exit
;;
*)
echo "Invalid option: $1" >&2
exit 1
;;
esac
shift
done
while [ ! "$MODE" ]; do
if [ "$REVERSE" = yes ]; then
DIRECTION=FROM
else
DIRECTION=TO
fi
echo -n "Copy which files $DIRECTION the server? ftp or html? "
read ans
case "$ans" in
f*) MODE=ftp ;;
h*) MODE=html ;;
'') exit 1 ;;
*) echo "You must answer f or h to copy the ftp or html data." ;;
esac
done
if [ "$MODE" = ftp ]; then
SRC_DIR="$FTP_SRC"
DEST_DIR="$FTP_DEST"
FILT=".filt"
else
SRC_DIR="$HTML_SRC"
DEST_DIR="$HTML_DEST"
FILT="filt"
fi
function do_rsync {
rsync --dry-run "${@}" | grep -v 'is uptodate$'
echo ''
echo -n "Run without --dry-run? [n] "
read ans
case "$ans" in
y*) rsync "${@}" | grep -v 'is uptodate$' ;;
esac
}
if [ -d "$SRC_DIR" ]; then
REVERSE_RSYNC=do_rsync
else
echo "The directory $SRC_DIR does not exist yet."
echo -n "Do you want to create it? [n] "
read ans
case "$ans" in
y*) ;;
*) exit 1 ;;
esac
REVERSE=yes
REVERSE_RSYNC=rsync
fi
if [ "$REVERSE" = yes ]; then
OPTS='-aivOHP'
TMP_FILT="$SRC_DIR/tmp-filt"
echo "Copying files from $RSYNC_SAMBA_HOST to $SRC_DIR ..."
if [ "$MODE" = html ]; then
if [ $REVERSE_RSYNC = rsync ]; then
git clone "$HTML_GIT" "$SRC_DIR" || exit 1
else
cd "$SRC_DIR" || exit 1
git pull || exit 1
fi
sed -n -e 's/[-P]/H/p' "$SRC_DIR/$FILT" >"$TMP_FILT"
OPTS="${OPTS}f._$TMP_FILT"
else
OPTS="${OPTS}f:_$FILT"
fi
$REVERSE_RSYNC "$OPTS" "$RSYNC_SAMBA_HOST:$DEST_DIR/" "$SRC_DIR/"
rm -f "$TMP_FILT"
exit
fi
cd "$SRC_DIR" || exit 1
echo "Copying files from $SRC_DIR to $RSYNC_SAMBA_HOST ..."
do_rsync -aivOHP --del -f._$FILT . "$RSYNC_SAMBA_HOST:$DEST_DIR/"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash -e
# This script expects the ~/src/rsync directory to contain the rsync
# source that has been updated. It also expects the auto-build-save
# directory to have been created prior to the running of configure so
# that each branch has its own build directory underneath. This supports
# the maintainer workflow for the rsync-patches files maintenace.
FTP_SRC="$HOME/samba-rsync-ftp"
FTP_DEST="/home/ftp/pub/rsync"
MD_FILES="README.md INSTALL.md NEWS.md"
case "$RSYNC_SAMBA_HOST" in
*.samba.org) ;;
*)
echo "You must set RSYNC_SAMBA_HOST in your environment to the samba hostname to use." >&2
exit 1
;;
esac
if [ ! -d "$FTP_SRC" ]; then
packaging/samba-rsync ftp # Ask to initialize the local ftp dir
fi
cd ~/src/rsync
make man
./md-convert --dest="$FTP_SRC" $MD_FILES
rsync -aiic $MD_FILES auto-build-save/master/*.?.html "$FTP_SRC"
cd "$FTP_SRC"
rsync -aiic README.* INSTALL.* NEWS.* *.?.html "$RSYNC_SAMBA_HOST:$FTP_DEST/"
+55 -1
View File
@@ -7,6 +7,9 @@
import sys, os, re, argparse, subprocess
from datetime import datetime
MAINTAINER_NAME = 'Wayne Davison'
MAINTAINER_SUF = ' ' + MAINTAINER_NAME + "\n"
def main():
latest_year = '2000'
@@ -19,6 +22,10 @@ def main():
m = argparse.Namespace(**m.groupdict())
if m.year > latest_year:
latest_year = m.year
if m.fn.startswith('zlib/') or m.fn.startswith('popt/'):
continue
if re.search(r'\.(c|h|sh|test)$', m.fn):
maybe_edit_copyright_year(m.fn, m.year)
proc.communicate()
fn = 'latest-year.h'
@@ -32,8 +39,55 @@ def main():
fh.write(txt)
def maybe_edit_copyright_year(fn, year):
opening_lines = [ ]
copyright_line = None
with open(fn, 'r', encoding='utf-8') as fh:
for lineno, line in enumerate(fh):
opening_lines.append(line)
if lineno > 3 and not re.search(r'\S', line):
break
m = re.match(r'^(?P<pre>.*Copyright\s+\S+\s+)(?P<year>\d\d\d\d(?:-\d\d\d\d)?(,\s+\d\d\d\d)*)(?P<suf>.+)', line)
if not m:
continue
copyright_line = argparse.Namespace(**m.groupdict())
copyright_line.lineno = len(opening_lines)
copyright_line.is_maintainer_line = MAINTAINER_NAME in copyright_line.suf
copyright_line.txt = line
if copyright_line.is_maintainer_line:
break
if not copyright_line:
return
if copyright_line.is_maintainer_line:
cyears = copyright_line.year.split('-')
if year == cyears[0]:
cyears = [ year ]
else:
cyears = [ cyears[0], year ]
txt = copyright_line.pre + '-'.join(cyears) + MAINTAINER_SUF
if txt == copyright_line.txt:
return
opening_lines[copyright_line.lineno - 1] = txt
else:
if fn.startswith('lib/') or fn.startswith('testsuite/'):
return
txt = copyright_line.pre + year + MAINTAINER_SUF
opening_lines[copyright_line.lineno - 1] += txt
remaining_txt = fh.read()
print(f"Updating {fn} with year {year}")
with open(fn, 'w', encoding='utf-8') as fh:
fh.write(''.join(opening_lines))
fh.write(remaining_txt)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Grab the year of the last mod for our c & h files and make sure the LATEST_YEAR value is accurate.")
parser = argparse.ArgumentParser(description="Grab the year of last mod for our c & h files and make sure the Copyright comment is up-to-date.")
args = parser.parse_args()
main()
+2 -9
View File
@@ -416,7 +416,7 @@ static int include_config(char *include, int manage_globals)
char *match = manage_globals ? "*.conf" : "*.inc";
int ret;
if (vfs_stat(VFS_AT_FDCWD, include, &sb, VFS_ALLOW_SYMLINK) < 0) {
if (do_stat(include, &sb) < 0) {
rsyserr(FLOG, errno, "unable to stat config file \"%s\"", include);
return 0;
}
@@ -580,14 +580,7 @@ static FILE *OpenConfFile( char *FileName )
return( NULL );
}
/* rsyncd.conf path (--config or default): a planted symlink could redirect
* the daemon's config read. Refuse symlinks not owned by uid 0 or euid. */
{
int cfg_fd = vfs_open_owner_walk( FileName, O_RDONLY, 0 , 0);
OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
if( !OpenedFile && cfg_fd >= 0 )
close( cfg_fd );
}
OpenedFile = fopen( FileName, "r" );
if( NULL == OpenedFile )
{
rsyserr(FLOG, errno, "unable to open config file \"%s\"",
+55
View File
@@ -0,0 +1,55 @@
/** \ingroup popt
* \file popt/findme.c
*/
/* (C) 1998-2002 Red Hat, Inc. -- Licensing details are in the COPYING
file accompanying popt source distributions, available from
ftp://ftp.rpm.org/pub/rpm/dist. */
#include "system.h"
#include "findme.h"
const char * findProgramPath(const char * argv0)
{
char * path = getenv("PATH");
char * pathbuf;
char * start, * chptr;
char * buf;
size_t bufsize;
if (argv0 == NULL) return NULL; /* XXX can't happen */
/* If there is a / in the argv[0], it has to be an absolute path */
if (strchr(argv0, '/'))
return xstrdup(argv0);
if (path == NULL) return NULL;
bufsize = strlen(path) + 1;
start = pathbuf = alloca(bufsize);
if (pathbuf == NULL) return NULL; /* XXX can't happen */
strlcpy(pathbuf, path, bufsize);
bufsize += sizeof "/" - 1 + strlen(argv0);
buf = malloc(bufsize);
if (buf == NULL) return NULL; /* XXX can't happen */
chptr = NULL;
/*@-branchstate@*/
do {
if ((chptr = strchr(start, ':')))
*chptr = '\0';
snprintf(buf, bufsize, "%s/%s", start, argv0);
if (!access(buf, X_OK))
return buf;
if (chptr)
start = chptr + 1;
else
start = NULL;
} while (start && *start);
/*@=branchstate@*/
free(buf);
return NULL;
}
+20
View File
@@ -0,0 +1,20 @@
/** \ingroup popt
* \file popt/findme.h
*/
/* (C) 1998-2000 Red Hat, Inc. -- Licensing details are in the COPYING
file accompanying popt source distributions, available from
ftp://ftp.rpm.org/pub/rpm/dist. */
#ifndef H_FINDME
#define H_FINDME
/**
* Return absolute path to executable by searching PATH.
* @param argv0 name of executable
* @return (malloc'd) absolute path to executable (or NULL)
*/
/*@null@*/ const char * findProgramPath(/*@null@*/ const char * argv0)
/*@*/;
#endif
Loaded 100 of 664 files, more files were not shown because too many files have changed in this diff. Show more