mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-10 20:29:42 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7ebb57c5a |
No files matched your search
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: RsyncProject
|
||||
patreon: AndrewTridgell
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,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 |
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -57,6 +57,5 @@
|
||||
printf("%lu", (unsigned long)gid);
|
||||
printf("\n");
|
||||
|
||||
free(list);
|
||||
return 0;
|
||||
}
|
||||
+10
-25
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
#define LATEST_YEAR "2026"
|
||||
#define LATEST_YEAR "2025"
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
@@ -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, ©_links, 0, 0, 0 },
|
||||
{"copy-unsafe-links",0, POPT_ARG_NONE, ©_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, ©_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,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
|
||||
|
||||
|
||||
Executable
+174
@@ -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
|
||||
@@ -1,2 +0,0 @@
|
||||
- /generated-files/
|
||||
- /binaries/
|
||||
@@ -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.
|
||||
Executable
+244
@@ -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
@@ -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:
|
||||
|
||||
Executable
+414
@@ -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
|
||||
@@ -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
|
||||
Executable
+124
@@ -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/"
|
||||
Executable
+33
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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\"",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user