mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-09 11:51:46 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a5fa44a8 | ||
|
|
7fef1c827e | ||
|
|
59be391641 | ||
|
|
a93490077d | ||
|
|
a68b32cd5c | ||
|
|
a4bfa170fa | ||
|
|
c529eccff7 | ||
|
|
270202affb | ||
|
|
90992088d6 | ||
|
|
240bd9df96 | ||
|
|
7c20b077c9 | ||
|
|
f0177d82a8 | ||
|
|
0145b9128d | ||
|
|
9371b33320 | ||
|
|
7f01ae0af1 | ||
|
|
3d2caaa0cb | ||
|
|
3b1eb8dd7f | ||
|
|
324a1f0716 | ||
|
|
010aa15dec | ||
|
|
a49f085a4f | ||
|
|
4c029dd9d3 | ||
|
|
5351b53276 | ||
|
|
ec52eed3ed | ||
|
|
195b4c6d30 | ||
|
|
bd48f0751b | ||
|
|
dcef974c7c |
No files matched your search
@@ -1,5 +1,9 @@
|
||||
name: Lint GitHub Actions workflows
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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.
|
||||
@@ -14,7 +18,7 @@ on:
|
||||
- '.github/actionlint.yaml'
|
||||
- '.github/actionlint.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- '.github/workflows/*.yml'
|
||||
- '.github/actionlint.yaml'
|
||||
@@ -25,9 +29,6 @@ permissions:
|
||||
|
||||
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:
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on AlmaLinux 8
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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).
|
||||
@@ -13,7 +17,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/almalinux-8-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/almalinux-8-build.yml'
|
||||
@@ -22,9 +26,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: ubuntu-latest
|
||||
container:
|
||||
image: almalinux:8
|
||||
@@ -51,7 +52,8 @@ jobs:
|
||||
attr libattr-devel acl libacl-devel \
|
||||
zstd libzstd-devel \
|
||||
lz4 lz4-devel \
|
||||
xxhash xxhash-devel
|
||||
xxhash xxhash-devel \
|
||||
libidn2 libidn2-devel
|
||||
alternatives --set python3 /usr/bin/python3.9
|
||||
pip3 install commonmark
|
||||
- name: configure
|
||||
@@ -65,7 +67,7 @@ jobs:
|
||||
# crtimes-not-supported skip matches the other Linux jobs;
|
||||
# daemon-chroot-acl and proxy-response-line-too-long skip because
|
||||
# the default (secure) transport opens no listening socket.
|
||||
run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt make check
|
||||
run: RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/linux.txt,@testsuite/skiplist/almalinux-8.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
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Build static rsync for Android
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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
|
||||
@@ -16,7 +20,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/android-static-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/android-static-build.yml'
|
||||
@@ -31,9 +35,6 @@ env:
|
||||
|
||||
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:
|
||||
@@ -79,7 +80,7 @@ jobs:
|
||||
# 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-zstd --disable-lz4 --disable-xxhash --disable-openssl --disable-idn \
|
||||
--disable-iconv --disable-iconv-open \
|
||||
--disable-acl-support --disable-xattr-support \
|
||||
--disable-md2man --disable-roll-simd \
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: rsync ASan+UBSan (clang)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/asan-build.yml'
|
||||
@@ -21,9 +25,6 @@ on:
|
||||
|
||||
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:
|
||||
@@ -44,7 +45,7 @@ jobs:
|
||||
- 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
|
||||
sudo apt-get install -y clang acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
# -DNDEBUG builds as a shipped release does (assert() compiled out), so
|
||||
@@ -57,7 +58,7 @@ jobs:
|
||||
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
|
||||
./configure --with-rrsync --disable-md2man
|
||||
- name: make
|
||||
# check-progs builds rsync plus the test helper programs (tls, trimslash,
|
||||
# t_unsafe, ...) that runtests.py requires; plain "make" builds only rsync
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Coverage (Ubuntu)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/coverage.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/coverage.yml'
|
||||
@@ -17,9 +21,6 @@ on:
|
||||
|
||||
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:
|
||||
@@ -29,7 +30,7 @@ jobs:
|
||||
- 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
|
||||
sudo apt-get install -y acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl gcovr
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --enable-coverage --with-rrsync
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on Cygwin
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/cygwin-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/cygwin-build.yml'
|
||||
@@ -16,9 +20,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:
|
||||
@@ -26,11 +27,49 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: cygwin
|
||||
run: choco install -y --no-progress cygwin cyg-get
|
||||
- name: prep
|
||||
run: |
|
||||
cyg-get make autoconf automake gcc-core attr libattr-devel python39 python39-pip libzstd-devel liblz4-devel libssl-devel libxxhash0 libxxhash-devel
|
||||
echo "C:/tools/cygwin/bin" >>$Env:GITHUB_PATH
|
||||
$setup = Join-Path $Env:RUNNER_TEMP 'setup-x86_64.exe'
|
||||
$sums = Join-Path $Env:RUNNER_TEMP 'cygwin-sha512.sum'
|
||||
$packages = 'make,autoconf,automake,gcc-core,attr,libattr-devel,python39,python39-pip,libzstd-devel,liblz4-devel,libssl-devel,libxxhash0,libxxhash-devel,libidn2-devel'
|
||||
|
||||
Invoke-WebRequest https://cygwin.com/setup-x86_64.exe -OutFile $setup
|
||||
Invoke-WebRequest https://cygwin.com/sha512.sum -OutFile $sums
|
||||
|
||||
$sum = Select-String -LiteralPath $sums -Pattern '^[0-9a-fA-F]{128}\s+\*?setup-x86_64\.exe$' | Select-Object -First 1
|
||||
if (-not $sum) {
|
||||
throw 'setup-x86_64.exe is missing from Cygwin sha512.sum'
|
||||
}
|
||||
$expected = ($sum.Line -split '\s+')[0]
|
||||
$actual = (Get-FileHash -LiteralPath $setup -Algorithm SHA512).Hash
|
||||
if ($actual -ine $expected) {
|
||||
throw 'Cygwin setup SHA-512 mismatch'
|
||||
}
|
||||
|
||||
$arguments = @(
|
||||
'--quiet-mode',
|
||||
'--no-desktop',
|
||||
'--no-startmenu',
|
||||
'--no-shortcuts',
|
||||
'--root', 'C:\tools\cygwin',
|
||||
'--local-package-dir', (Join-Path $Env:RUNNER_TEMP 'cygwin-packages'),
|
||||
'--site', 'https://mirrors.kernel.org/sourceware/cygwin/',
|
||||
'--packages', $packages
|
||||
)
|
||||
$install = Start-Process -FilePath $setup -ArgumentList $arguments -Wait -PassThru -NoNewWindow
|
||||
if ($install.ExitCode -ne 0) {
|
||||
exit $install.ExitCode
|
||||
}
|
||||
|
||||
$bash = 'C:\tools\cygwin\bin\bash.exe'
|
||||
if (-not (Test-Path -LiteralPath $bash)) {
|
||||
throw 'Cygwin setup did not install bash'
|
||||
}
|
||||
& $bash -lc 'command -v make aclocal gcc python3 >/dev/null'
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Cygwin setup did not install all required build tools'
|
||||
}
|
||||
|
||||
echo 'C:/tools/cygwin/bin' >>$Env:GITHUB_PATH
|
||||
- name: commonmark
|
||||
run: bash -c 'python3 -mpip install --user commonmark'
|
||||
- name: configure
|
||||
@@ -46,8 +85,9 @@ jobs:
|
||||
# (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'
|
||||
# RESOLVE_BENEATH symlink-race tests. Cygwin runs non-root, so the
|
||||
# namecvt empty-response regression can run despite its common root skip.
|
||||
run: bash -c 'RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/common.txt,@testsuite/skiplist/cygwin.txt,-daemon-namecvt-empty-response 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.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test fleettest harness
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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
|
||||
@@ -16,7 +20,7 @@ on:
|
||||
- 'testsuite/skiplist/**'
|
||||
- 'testsuite/skiplist-spec_test.py'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'testsuite/fleettest.py'
|
||||
- '.github/workflows/fleettest.yml'
|
||||
@@ -29,9 +33,6 @@ on:
|
||||
|
||||
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:
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
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 \
|
||||
acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev \
|
||||
python3-cmarkgfm openssl rsync openssh-server
|
||||
- name: set up ssh to localhost
|
||||
run: |
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on FreeBSD
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/freebsd-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/freebsd-build.yml'
|
||||
@@ -16,9 +20,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: ubuntu-latest
|
||||
name: Test rsync on FreeBSD
|
||||
steps:
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
pkg install -y bash autotools m4 devel/xxhash zstd liblz4 python3 archivers/liblz4 git
|
||||
run: |
|
||||
freebsd-version
|
||||
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
|
||||
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on macOS
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/macos-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/macos-build.yml'
|
||||
@@ -16,9 +20,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:
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: prep
|
||||
run: |
|
||||
brew install automake openssl xxhash zstd lz4
|
||||
brew install automake openssl xxhash zstd lz4 libidn2
|
||||
pip3 install --user --break-system-packages commonmark
|
||||
echo "$(brew --prefix)/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on NetBSD
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/netbsd-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/netbsd-build.yml'
|
||||
@@ -16,9 +20,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: ubuntu-latest
|
||||
name: Test rsync on NetBSD
|
||||
steps:
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
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
|
||||
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on OpenBSD
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/openbsd-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/openbsd-build.yml'
|
||||
@@ -16,9 +20,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: ubuntu-latest
|
||||
name: Test rsync on OpenBSD
|
||||
steps:
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
uname -a
|
||||
export AUTOCONF_VERSION=2.71
|
||||
export AUTOMAKE_VERSION=1.16
|
||||
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4
|
||||
./configure --with-rrsync --disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
|
||||
make
|
||||
./rsync --version
|
||||
# The flipper (symlink-race) tests are excluded on OpenBSD, as on the
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: rsync scan-build (clang analyzer)
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/scan-build.yml'
|
||||
@@ -20,9 +24,6 @@ jobs:
|
||||
# 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:
|
||||
@@ -32,7 +33,7 @@ jobs:
|
||||
- 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
|
||||
sudo apt-get install -y clang-18 clang-tools-18 acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-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.
|
||||
@@ -64,9 +65,6 @@ jobs:
|
||||
# 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
|
||||
@@ -77,7 +75,7 @@ jobs:
|
||||
- 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
|
||||
sudo apt-get install -y clang clang-tools acl libacl1-dev attr libattr1-dev liblz4-dev libidn2-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)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on Solaris
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/solaris-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/solaris-build.yml'
|
||||
@@ -16,9 +20,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: ubuntu-latest
|
||||
name: Test rsync on Solaris
|
||||
steps:
|
||||
@@ -34,7 +35,7 @@ jobs:
|
||||
pkg install bash automake gnu-m4 pkg://solaris/runtime/python-35 autoconf gcc git
|
||||
run: |
|
||||
uname -a
|
||||
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4
|
||||
./configure --with-rrsync -disable-zstd --disable-md2man --disable-xxhash --disable-lz4 --disable-idn
|
||||
make
|
||||
./rsync --version
|
||||
make check
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on Ubuntu 22.04
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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).
|
||||
@@ -11,7 +15,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-22.04-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-22.04-build.yml'
|
||||
@@ -20,9 +24,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: ubuntu-22.04
|
||||
name: Test rsync on Ubuntu 22.04
|
||||
steps:
|
||||
@@ -31,7 +32,8 @@ jobs:
|
||||
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
|
||||
sudo apt-get update
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync on Ubuntu
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-build.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-build.yml'
|
||||
@@ -16,9 +20,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: ubuntu-latest
|
||||
name: Test rsync on Ubuntu
|
||||
steps:
|
||||
@@ -27,7 +28,8 @@ jobs:
|
||||
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
|
||||
sudo apt-get update
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Test rsync version mixing on Ubuntu
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# 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
|
||||
@@ -28,7 +32,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-version-mix.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/ubuntu-version-mix.yml'
|
||||
@@ -37,9 +41,6 @@ on:
|
||||
|
||||
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:
|
||||
@@ -48,7 +49,8 @@ jobs:
|
||||
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
|
||||
sudo apt-get update
|
||||
sudo apt-get install acl libacl1-dev attr libattr1-dev liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: Valgrind memcheck
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
@@ -7,7 +11,7 @@ on:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- '.github/workflows/*.yml'
|
||||
- '!.github/workflows/valgrind.yml'
|
||||
@@ -17,9 +21,6 @@ on:
|
||||
|
||||
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:
|
||||
@@ -36,7 +37,7 @@ jobs:
|
||||
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
|
||||
liblz4-dev libzstd-dev libxxhash-dev libidn2-dev python3-cmarkgfm openssl
|
||||
echo "/usr/local/bin" >>"$GITHUB_PATH"
|
||||
- name: configure
|
||||
run: ./configure --with-rrsync --enable-debug
|
||||
|
||||
+10
-10
@@ -43,19 +43,19 @@ 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
|
||||
/simdtest
|
||||
/t_acl
|
||||
/t_chmod_secure
|
||||
/t_clean_fname
|
||||
/t_hashtable_overflow
|
||||
/t_iwildmatch
|
||||
/t_rename_secure
|
||||
/t_safe_arg
|
||||
/t_safe_arg_main
|
||||
/t_secure_relpath
|
||||
/t_symlink_secure
|
||||
/rounding.h
|
||||
/doc/rsync.pdf
|
||||
/doc/rsync.ps
|
||||
|
||||
+16
@@ -114,6 +114,16 @@ checksums.
|
||||
|
||||
[4]: https://www.openssl.org/docs/man1.0.2/man3/crypto.html
|
||||
|
||||
## libidn2
|
||||
|
||||
The [libidn2 library][5] converts an internationalized domain name into the
|
||||
IDNA A-label ("Punycode") form that a resolver understands. Installing this
|
||||
development library lets rsync connect to a daemon whose name has non-ASCII
|
||||
characters in it, and lets a daemon's "hosts allow" & "hosts deny" settings be
|
||||
written the same way.
|
||||
|
||||
[5]: https://www.gnu.org/software/libidn/#libidn2
|
||||
|
||||
## Package summary
|
||||
|
||||
To help you get the libraries installed, here are some package install commands
|
||||
@@ -131,6 +141,7 @@ like.
|
||||
> sudo apt install -y libzstd-dev
|
||||
> sudo apt install -y liblz4-dev
|
||||
> sudo apt install -y libssl-dev
|
||||
> sudo apt install -y libidn2-dev
|
||||
|
||||
Or run support/install_deps_ubuntu.sh
|
||||
|
||||
@@ -144,6 +155,7 @@ Or run support/install_deps_ubuntu.sh
|
||||
> sudo yum -y install libzstd-devel
|
||||
> sudo yum -y install lz4-devel
|
||||
> sudo yum -y install openssl-devel
|
||||
> sudo yum -y install libidn2-devel
|
||||
> python3 -mpip install --user commonmark
|
||||
|
||||
- For Fedora 33:
|
||||
@@ -154,6 +166,7 @@ Or run support/install_deps_ubuntu.sh
|
||||
> sudo dnf -y install libzstd-devel
|
||||
> sudo dnf -y install lz4-devel
|
||||
> sudo dnf -y install openssl-devel
|
||||
> sudo dnf -y install libidn2-devel
|
||||
|
||||
- For FreeBSD (this assumes that the python3 version is 3.7):
|
||||
|
||||
@@ -161,6 +174,7 @@ Or run support/install_deps_ubuntu.sh
|
||||
> sudo pkg install -y xxhash
|
||||
> sudo pkg install -y zstd
|
||||
> sudo pkg install -y liblz4
|
||||
> sudo pkg install -y libidn2
|
||||
|
||||
- For macOS:
|
||||
|
||||
@@ -169,6 +183,7 @@ Or run support/install_deps_ubuntu.sh
|
||||
> brew install zstd
|
||||
> brew install lz4
|
||||
> brew install openssl
|
||||
> brew install libidn2
|
||||
|
||||
- For Cygwin (with all cygwin programs stopped, run the appropriate setup program from a cmd shell):
|
||||
|
||||
@@ -177,6 +192,7 @@ Or run support/install_deps_ubuntu.sh
|
||||
> setup-x86_64 --quiet-mode -P libzstd-devel
|
||||
> setup-x86_64 --quiet-mode -P liblz4-devel
|
||||
> setup-x86_64 --quiet-mode -P libssl-devel
|
||||
> setup-x86_64 --quiet-mode -P libidn2-devel
|
||||
|
||||
Sometimes cygwin has commonmark packaged and sometimes it doesn't. Now that
|
||||
its python38 has stabilized, you could install python38-commonmark. Or just
|
||||
|
||||
+22
-45
@@ -18,12 +18,10 @@ CXXFLAGS=@CXXFLAGS@
|
||||
EXEEXT=@EXEEXT@
|
||||
LDFLAGS=@LDFLAGS@
|
||||
LIBOBJDIR=lib/
|
||||
AR=@AR@
|
||||
ARFLAGS=cr
|
||||
RANLIB=@RANLIB@
|
||||
|
||||
INSTALLCMD=@INSTALL@
|
||||
INSTALLMAN=@INSTALL@
|
||||
STRIP=@STRIP@
|
||||
|
||||
srcdir=@srcdir@
|
||||
MKDIR_P=@MKDIR_P@
|
||||
@@ -41,13 +39,13 @@ 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@
|
||||
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
|
||||
util1.o util2.o checksum.o match.o syscall.o log.o backup.o delete.o
|
||||
OBJS1=$(OBJS1_NO_MAIN) main.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
|
||||
@@ -55,10 +53,9 @@ 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/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) \
|
||||
@@ -71,7 +68,7 @@ CHECK_SYMLINKS = testsuite/chown-fake_test.py testsuite/devices-fake_test.py \
|
||||
# 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_COMPILE_OBJS=syscall-no-at-fdcwd.o
|
||||
|
||||
# note that the -I. is needed to handle config.h when using VPATH
|
||||
.c.o:
|
||||
@@ -84,20 +81,9 @@ 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 $@
|
||||
syscall-no-at-fdcwd.o: syscall.c $(HEADERS)
|
||||
$(CC) -I. -I$(srcdir) $(CFLAGS) $(CPPFLAGS) \
|
||||
-DRSYNC_TEST_NO_AT_FDCWD -c $(srcdir)/syscall.c -o $@
|
||||
|
||||
.PHONY: install
|
||||
install: all
|
||||
@@ -131,7 +117,8 @@ install-ssl-daemon: stunnel-rsyncd.conf
|
||||
install-all: install install-ssl-daemon
|
||||
|
||||
install-strip:
|
||||
$(MAKE) INSTALL_STRIP='-s' install
|
||||
$(MAKE) install
|
||||
$(STRIP) $(DESTDIR)$(bindir)/rsync$(EXEEXT)
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
@@ -156,17 +143,7 @@ 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
|
||||
|
||||
@@ -227,15 +204,15 @@ 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_OBJ = t_hashtable_overflow.o hashtable.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
|
||||
t_hashtable_overflow$(EXEEXT): $(T_HASHTABLE_OVERFLOW_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_HASHTABLE_OVERFLOW_OBJ) $(LIBS)
|
||||
|
||||
@@ -243,7 +220,7 @@ 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_OBJ = t_clean_fname.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o
|
||||
t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_CLEAN_FNAME_OBJ) $(LIBS)
|
||||
|
||||
@@ -256,23 +233,23 @@ t_clean_fname$(EXEEXT): $(T_CLEAN_FNAME_OBJ)
|
||||
# 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_OBJ = t_safe_arg.o t_safe_arg_main.o $(OBJS1_NO_MAIN) $(OBJS2) $(OBJS3) $(DAEMON_OBJ) $(LIBOBJ) @BUILD_ZLIB@ @BUILD_POPT@
|
||||
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_OBJ = t_chmod_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_chmod_secure$(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_OBJ = t_rename_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
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_OBJ = t_symlink_secure.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
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_OBJ = t_secure_relpath.o syscall.o util1.o util2.o t_stub.o lib/compat.o lib/snprintf.o lib/wildmatch.o lib/permstring.o
|
||||
t_secure_relpath$(EXEEXT): $(T_SECURE_RELPATH_OBJ)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(T_SECURE_RELPATH_OBJ) $(LIBS)
|
||||
|
||||
@@ -373,10 +350,10 @@ 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_COMPILE_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 -f *.gcno *.gcda lib/*.gcno lib/*.gcda zlib/*.gcno zlib/*.gcda popt/*.gcno popt/*.gcda
|
||||
rm -rf coverage coverage-tcp coverage-all coverage-fallback
|
||||
|
||||
.PHONY: cleantests
|
||||
|
||||
+34
-28
@@ -146,28 +146,29 @@ 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.
|
||||
Resolution of attacker-influenceable paths goes through `secure_relative_open()`,
|
||||
`secure_relative_dirfd()`, 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()` opens the resolved endpoint with
|
||||
the caller's requested access. `secure_relative_dirfd()` instead returns
|
||||
traversal authority for `fchdir()` or an at-style operation on a known child;
|
||||
it does not imply permission to enumerate the directory.
|
||||
|
||||
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.
|
||||
as traversal authority, `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
|
||||
@@ -190,13 +191,17 @@ 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`.
|
||||
every platform: a per-component walk on a stack of held directory fds. On
|
||||
Linux, anchors and traversal components use
|
||||
`O_PATH|O_DIRECTORY|O_NOFOLLOW`; other platforms retain the
|
||||
`O_RDONLY|O_DIRECTORY` fallback. 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). A final directory endpoint is reopened with the caller's requested
|
||||
flags. Thus `secure_opendir()` still receives a readable fd, while known-name
|
||||
operations beneath a searchable but unreadable directory do not require
|
||||
permission to list it.
|
||||
|
||||
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
|
||||
@@ -235,8 +240,9 @@ chmod-ing through a raced leaf symlink.
|
||||
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.
|
||||
resolves a parent used only as at-style authority with
|
||||
`secure_relative_dirfd()`. Use `secure_relative_open()` when the returned fd
|
||||
itself must be readable or otherwise support the caller's requested access.
|
||||
* 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()`),
|
||||
|
||||
@@ -33,6 +33,9 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
|
||||
struct hostent *hp;
|
||||
unsigned int i;
|
||||
const char *host = *host_ptr;
|
||||
#ifdef SUPPORT_IDN
|
||||
char idn_tok[1024];
|
||||
#endif
|
||||
|
||||
if (!host || !*host)
|
||||
return 0;
|
||||
@@ -42,6 +45,14 @@ static int match_hostname(const char **host_ptr, const char *addr, const char *t
|
||||
return innetgr(tok + 1, host, NULL, NULL);
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_IDN
|
||||
/* A hostname reaches us from DNS as ASCII, so fold an IDN token to its
|
||||
* A-label form before comparing. An all-ASCII token, and a token we
|
||||
* can't fold, are both left as they are. */
|
||||
if (idn_to_ascii(tok, 0, idn_tok, sizeof idn_tok))
|
||||
tok = idn_tok;
|
||||
#endif
|
||||
|
||||
/* First check if the reverse-DNS-determined hostname matches. */
|
||||
if (iwildmatch(tok, host))
|
||||
return 1;
|
||||
|
||||
@@ -1213,7 +1213,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
|
||||
* 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) {
|
||||
else if (secure_relpath_active() && am_root) {
|
||||
errno = ELOOP;
|
||||
rc = -1;
|
||||
} else
|
||||
@@ -1334,7 +1334,7 @@ static int set_rsync_acl(int fd, int dirfd, const char *leaf, const char *fname,
|
||||
sxp->st.st_mode = cur_mode;
|
||||
return 0;
|
||||
}
|
||||
if (vfs_relpath_active() && am_root) {
|
||||
if (secure_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
|
||||
|
||||
+5
-5
@@ -156,7 +156,7 @@ static const char *check_secret(int module, const char *user, const char *group,
|
||||
if (!fname || !*fname)
|
||||
return "no secrets file";
|
||||
{
|
||||
int fd = vfs_open_owner_walk(fname, O_RDONLY, 0, 0);
|
||||
int fd = open_no_attacker_symlinks(fname, O_RDONLY, 0);
|
||||
if (fd < 0)
|
||||
return "no secrets file";
|
||||
fh = fdopen(fd, "r");
|
||||
@@ -166,7 +166,7 @@ static const char *check_secret(int module, const char *user, const char *group,
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -239,10 +239,10 @@ static const char *getpassf(const char *filename)
|
||||
|
||||
/* --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()
|
||||
* (e.g. shadow hashes) to a malicious daemon; the do_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_no_attacker_symlinks(filename, O_RDONLY, 0)) < 0) {
|
||||
rsyserr(FERROR, errno, "could not open password file %s", filename);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
@@ -252,7 +252,7 @@ static const char *getpassf(const char *filename)
|
||||
* 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) {
|
||||
if (do_fstat(fd, &st) == -1) {
|
||||
rsyserr(FERROR, errno, "fstat(%s)", filename);
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ extern int preserve_links;
|
||||
extern int safe_symlinks;
|
||||
extern int backup_dir_len;
|
||||
extern unsigned int backup_dir_remainder;
|
||||
extern int operator_path_resolve;
|
||||
extern char backup_dir_buf[MAXPATHLEN];
|
||||
extern char *backup_suffix;
|
||||
extern char *backup_dir;
|
||||
@@ -43,14 +44,19 @@ extern char *backup_dir;
|
||||
* backup_metadata_hardened() to tell the two -1 cases apart). */
|
||||
int backup_metadata_hardened(void)
|
||||
{
|
||||
return vfs_relpath_active() && !vfs_symlink_optout_allowed();
|
||||
return secure_relpath_active() && !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);
|
||||
if (backup_metadata_hardened() && path && *path) {
|
||||
int save = operator_path_resolve, fd;
|
||||
operator_path_resolve = 1;
|
||||
fd = do_open_at(path, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);
|
||||
operator_path_resolve = save;
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
@@ -60,7 +66,7 @@ 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_at(backup_dir_buf, &st) < 0) {
|
||||
if (errno == ENOENT)
|
||||
return 0;
|
||||
rsyserr(FERROR, errno, "backup lstat %s failed", backup_dir_buf);
|
||||
@@ -119,7 +125,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_at(backup_dir_buf, ACCESSPERMS) < 0) {
|
||||
if (errno == EEXIST) {
|
||||
val = validate_backup_dir();
|
||||
if (val > 0)
|
||||
@@ -135,7 +141,7 @@ 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;
|
||||
@@ -164,7 +170,7 @@ static BOOL copy_valid_path(const char *fname)
|
||||
close(bfd);
|
||||
}
|
||||
#endif
|
||||
set_file_attrs(backup_dir_buf, file, NULL, NULL, ATTRS_OPERATOR_PATH);
|
||||
set_file_attrs(backup_dir_buf, file, NULL, NULL, 0);
|
||||
unmake_file(file);
|
||||
}
|
||||
|
||||
@@ -197,7 +203,7 @@ char *get_backup_name(const char *fname)
|
||||
}
|
||||
if (backup_dir_len > 1)
|
||||
dirbuf[backup_dir_len-1] = '\0';
|
||||
ret = vfs_make_path(dirbuf, 0, VFS_OPERATOR_PATH);
|
||||
ret = make_path(dirbuf, 0);
|
||||
if (ret < 0)
|
||||
return NULL;
|
||||
initialized = 1;
|
||||
@@ -230,11 +236,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_at(from, to) == 0) {
|
||||
if (DEBUG_GTE(BACKUP, 1))
|
||||
rprintf(FINFO, "make_backup: HLINK %s successful.\n", from);
|
||||
return 2;
|
||||
@@ -244,12 +246,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_at(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);
|
||||
@@ -271,7 +272,7 @@ 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)))
|
||||
@@ -287,7 +288,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
|
||||
* unsafe symlink. */
|
||||
if (preserve_links && S_ISLNK(sx.st.st_mode) && safe_symlinks) {
|
||||
char lnkbuf[MAXPATHLEN];
|
||||
int llen = vfs_readlink(fname, lnkbuf, MAXPATHLEN - 1);
|
||||
int llen = do_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. */
|
||||
@@ -316,7 +317,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_at(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;
|
||||
@@ -356,7 +357,7 @@ static int make_backup_inner(const char *fname, BOOL prefer_rename)
|
||||
/* 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_at(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 +374,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_at(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 +398,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 +417,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);
|
||||
@@ -441,6 +442,8 @@ int make_backup(const char *fname, BOOL prefer_rename)
|
||||
* symlink component is refused while the operator's own is followed --
|
||||
* absolute and relative alike. --insecure-links / "insecure links ="
|
||||
* restores legacy following. */
|
||||
operator_path_resolve = 1;
|
||||
ret = make_backup_inner(fname, prefer_rename);
|
||||
operator_path_resolve = 0;
|
||||
return ret;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ void open_batch_files(void)
|
||||
|
||||
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 = open_no_attacker_symlinks(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 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);
|
||||
@@ -259,24 +259,24 @@ void open_batch_files(void)
|
||||
|
||||
/* 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);
|
||||
* do_open(), open_no_attacker_symlinks passes flags verbatim. */
|
||||
batch_fd = open_no_attacker_symlinks(batch_name, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 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 = open_no_attacker_symlinks(batch_name, O_RDONLY | O_BINARY, 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. */
|
||||
/* --read-batch: the file's bytes drive the protocol parser,
|
||||
* allow FIFOs used by shell process substitution while continuing to reject other non-regular inputs. */
|
||||
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",
|
||||
if (do_fstat(batch_fd, &st) == 0 && !S_ISREG(st.st_mode) && !S_ISFIFO(st.st_mode)) {
|
||||
rprintf(FERROR, "Batch file %s is neither a regular file nor a FIFO\n",
|
||||
full_fname(batch_name));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
}
|
||||
|
||||
+1
-1
@@ -423,7 +423,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;
|
||||
|
||||
@@ -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_at(cleanup_fname);
|
||||
if (exit_code)
|
||||
kill_all(SIGUSR1);
|
||||
if (cleanup_pid && cleanup_pid == getpid()) {
|
||||
|
||||
+16
-24
@@ -185,7 +185,7 @@ static int exchange_protocols(int f_in, int f_out, char *buf, size_t bufsiz, int
|
||||
/* '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);
|
||||
int motd_fd = open_no_attacker_symlinks(motd, O_RDONLY, 0);
|
||||
FILE *f = motd_fd >= 0 ? fdopen(motd_fd, "r") : NULL;
|
||||
if (!f && motd_fd >= 0) close(motd_fd);
|
||||
while (f && !feof(f)) {
|
||||
@@ -300,10 +300,10 @@ int start_inband_exchange(int f_in, int f_out, const char *user, int argc, char
|
||||
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);
|
||||
int ei_fd = open_no_attacker_symlinks(early_input_file, O_RDONLY, 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) {
|
||||
if (!f || do_fstat(fileno(f), &st) < 0) {
|
||||
rsyserr(FERROR, errno, "failed to open %s", early_input_file);
|
||||
if (f)
|
||||
fclose(f);
|
||||
@@ -925,12 +925,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. */
|
||||
@@ -1070,8 +1064,6 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
#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 +1073,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);
|
||||
@@ -1095,11 +1087,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char
|
||||
* 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
|
||||
* gate in secure_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();
|
||||
&& !symlink_optout_allowed();
|
||||
|
||||
if (gid_list.count) {
|
||||
gid_t *gid_array = gid_list.items;
|
||||
@@ -1485,7 +1477,7 @@ int start_daemon(int f_in, int f_out)
|
||||
}
|
||||
/* 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
|
||||
* (secure_relative_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,
|
||||
@@ -1494,7 +1486,7 @@ int start_daemon(int f_in, int f_out)
|
||||
* 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()
|
||||
* defenses. Leave am_chrooted=0 here so secure_relative_open()
|
||||
* still fires for "use chroot = no" modules. */
|
||||
if (chdir("/") < 0) {
|
||||
rsyserr(FLOG, errno, "daemon chdir(\"/\") failed");
|
||||
@@ -1626,18 +1618,18 @@ static void create_pid_file(void)
|
||||
dir = dirbuf;
|
||||
base = slash + 1;
|
||||
}
|
||||
if ((pdfd = vfs_open(dir, O_RDONLY|O_DIRECTORY, 0)) < 0) {
|
||||
if ((pdfd = do_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)
|
||||
#define PID_LSTAT(stp) do_lstat_atfd(pdfd, base, stp)
|
||||
#define PID_UNLINK() do_unlink_atfd(pdfd, base, 0)
|
||||
#define PID_OPEN() do_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_LSTAT(stp) do_lstat(base, stp)
|
||||
#define PID_UNLINK() unlink(base)
|
||||
#define PID_OPEN() vfs_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
|
||||
#define PID_OPEN() do_open(base, O_RDWR|O_CREAT|SAFE_NOFOLLOW, 0664)
|
||||
#endif
|
||||
|
||||
/* These tests make sure that a temp-style lock dir is handled safely. */
|
||||
@@ -1648,7 +1640,7 @@ static void create_pid_file(void)
|
||||
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";
|
||||
@@ -1659,7 +1651,7 @@ static void create_pid_file(void)
|
||||
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 {
|
||||
|
||||
+25
-11
@@ -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)
|
||||
sys/resource.h bsd/string.h idn2.h)
|
||||
AC_CHECK_HEADERS([netinet/ip.h], [], [], [[#include <netinet/in.h>]])
|
||||
AC_HEADER_MAJOR_FIXED
|
||||
|
||||
@@ -59,9 +59,8 @@ AC_PROG_CXX
|
||||
AC_PROG_AWK
|
||||
AC_PROG_EGREP
|
||||
AC_PROG_INSTALL
|
||||
AC_CHECK_TOOL([STRIP], [strip], [strip])
|
||||
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])
|
||||
@@ -106,12 +105,6 @@ 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)
|
||||
@@ -379,7 +372,7 @@ return SYS_openat2 + (int)how.resolve;
|
||||
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.])
|
||||
[Define to use Linux openat2(RESOLVE_BENEATH) in secure_relative_open where available.])
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -634,6 +627,27 @@ else
|
||||
AC_MSG_RESULT(no)
|
||||
fi
|
||||
|
||||
AC_MSG_CHECKING([whether to enable IDN support])
|
||||
AC_ARG_ENABLE([idn],
|
||||
AS_HELP_STRING([--disable-idn], [disable to omit IDN (Internationalized Domain Name) support]))
|
||||
AH_TEMPLATE([SUPPORT_IDN],
|
||||
[Undefine if you do not want IDN support. By default this is defined.])
|
||||
if test x"$enable_idn" != x"no"; then
|
||||
if test x"$ac_cv_header_idn2_h" = x"yes"; then
|
||||
AC_MSG_RESULT(yes)
|
||||
AC_SEARCH_LIBS(idn2_lookup_ul, idn2,
|
||||
[AC_DEFINE(SUPPORT_IDN)],
|
||||
[err_msg="$err_msg$nl- Failed to find idn2_lookup_ul function in idn2 lib.";
|
||||
no_lib="$no_lib idn"])
|
||||
else
|
||||
AC_MSG_RESULT(no)
|
||||
err_msg="$err_msg$nl- Failed to find idn2.h for IDN support."
|
||||
no_lib="$no_lib idn"
|
||||
fi
|
||||
else
|
||||
AC_MSG_RESULT(no)
|
||||
fi
|
||||
|
||||
if test x"$no_lib" != x; then
|
||||
echo ""
|
||||
echo "Configure found the following issues:"
|
||||
@@ -1550,7 +1564,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
-1
@@ -32,7 +32,7 @@ int claim_connection(char *fname, int max_connections)
|
||||
|
||||
/* '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_no_attacker_symlinks(fname, O_RDWR|O_CREAT, 0600)) < 0)
|
||||
return 0;
|
||||
|
||||
/* Find a free spot. */
|
||||
|
||||
@@ -63,23 +63,18 @@ 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);
|
||||
do_chmod_atfd(dfd, leaf, mode);
|
||||
else
|
||||
vfs_chmod(VFS_AT_FDCWD, fbuf, mode, 0);
|
||||
do_chmod_at(fbuf, mode);
|
||||
}
|
||||
|
||||
/* 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)
|
||||
static int del_unlink(const char *fbuf)
|
||||
{
|
||||
const char *leaf;
|
||||
int dfd = del_held_dfd(fbuf, &leaf);
|
||||
if (dfd >= 0 && vfs_unlink(dfd, leaf, 0) == 0)
|
||||
if (dfd >= 0 && do_unlink_atfd(dfd, leaf, 0) == 0)
|
||||
return 0;
|
||||
return robust_unlink(fbuf, vfs_flags); /* fall back (ETXTBSY retry, or not held) */
|
||||
return robust_unlink(fbuf); /* fall back (ETXTBSY retry, or not held) */
|
||||
}
|
||||
|
||||
static inline int is_backup_file(char *fn)
|
||||
@@ -138,7 +133,7 @@ static enum delret delete_dir_contents(char *fname, uint16 flags)
|
||||
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);
|
||||
del_dirfd = open_dir_secure(fname);
|
||||
fname[dlen] = '/';
|
||||
del_dir_prefix = fname;
|
||||
del_dir_prefix_len = dlen;
|
||||
@@ -228,20 +223,18 @@ enum delret delete_item(char *fbuf, uint16 mode, uint16 flags)
|
||||
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 = (dfd >= 0 ? do_unlink_atfd(dfd, leaf, AT_REMOVEDIR) : do_rmdir_at(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 = del_unlink(fbuf) == 0;
|
||||
}
|
||||
} else {
|
||||
what = "unlink";
|
||||
ok = del_unlink(fbuf, (flags & DEL_FOR_BACKUP) ? VFS_OPERATOR_PATH : 0) == 0;
|
||||
ok = del_unlink(fbuf) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ extern int sanitize_paths;
|
||||
extern int protocol_version;
|
||||
extern int trust_sender_args;
|
||||
extern int module_id;
|
||||
extern int operator_path_resolve;
|
||||
|
||||
/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */
|
||||
int daemon_config_filter_file = 0;
|
||||
@@ -135,6 +136,8 @@ static void filter_rule_err(const char *msg, const char *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 +155,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];
|
||||
@@ -754,9 +757,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
|
||||
@@ -850,7 +853,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)
|
||||
{
|
||||
@@ -1017,10 +1020,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. */
|
||||
@@ -1662,18 +1665,24 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_
|
||||
open_path = line;
|
||||
} 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).
|
||||
* ROOT-owned -- exactly what open_no_attacker_symlinks() 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);
|
||||
int save_opr = operator_path_resolve;
|
||||
if (!daemon_config_filter_file)
|
||||
operator_path_resolve = 1;
|
||||
fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0);
|
||||
operator_path_resolve = save_opr;
|
||||
|
||||
if (fd < 0)
|
||||
fp = NULL;
|
||||
else if (!(fp = fdopen(fd, "rb")))
|
||||
|
||||
@@ -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 {
|
||||
@@ -76,17 +76,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
|
||||
* 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). */
|
||||
* position past it (both do_lseek() and do_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) {
|
||||
if (do_lseek(f, sparse_seek, SEEK_CUR) < 0) {
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
}
|
||||
} else if (vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
|
||||
} else if (do_punch_hole(f, sparse_past_write, sparse_seek) < 0) {
|
||||
sparse_seek = 0;
|
||||
return -1;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ 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 do_lseek(f, len, SEEK_CUR) < 0 ? -1 : 0;
|
||||
return full_sparse_write(f, buf, len);
|
||||
}
|
||||
|
||||
@@ -262,7 +262,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 +345,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));
|
||||
|
||||
@@ -33,6 +33,7 @@ extern int am_chrooted;
|
||||
extern char *module_dir;
|
||||
extern unsigned int module_dirlen;
|
||||
extern int module_dirfd;
|
||||
extern unsigned int curr_dir_len;
|
||||
extern int am_sender;
|
||||
extern int am_generator;
|
||||
extern int inc_recurse;
|
||||
@@ -86,6 +87,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;
|
||||
|
||||
@@ -248,8 +250,8 @@ static int scan_readlink(const char *path, char *linkbuf, size_t bufsiz)
|
||||
&& 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);
|
||||
return do_readlink_atfd(scan_dirfd, path + scan_dir_prefix_len + 1, linkbuf, bufsiz);
|
||||
return do_readlink(path, linkbuf, bufsiz);
|
||||
}
|
||||
|
||||
static int readlink_stat(const char *path, STRUCT_STAT *stp, char *linkbuf)
|
||||
@@ -267,7 +269,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 +279,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,17 +287,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);
|
||||
return x_stat(path, stp, NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -309,17 +311,17 @@ int link_stat_at(int dfd, const char *name, STRUCT_STAT *stp, int follow_dirlink
|
||||
{
|
||||
#ifdef SUPPORT_LINKS
|
||||
if (copy_links)
|
||||
return vfs_stat(dfd, name, stp, 0);
|
||||
if (vfs_lstat(dfd, name, stp, 0) < 0)
|
||||
return do_stat_atfd(dfd, name, stp);
|
||||
if (do_lstat_atfd(dfd, name, stp) < 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))
|
||||
if (do_stat_atfd(dfd, name, &st) == 0 && S_ISDIR(st.st_mode))
|
||||
*stp = st;
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
return vfs_stat(dfd, name, stp, 0);
|
||||
return do_stat_atfd(dfd, name, stp);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1447,7 +1449,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",
|
||||
@@ -1562,7 +1564,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);
|
||||
@@ -1675,7 +1677,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)
|
||||
@@ -2013,14 +2015,14 @@ 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
|
||||
* secure_relative_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
|
||||
* O_NOFOLLOW makes secure_relative_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(). */
|
||||
@@ -2031,9 +2033,9 @@ static DIR *secure_opendir(const char *fbuf)
|
||||
|
||||
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] == '/')) {
|
||||
&& curr_dir_len >= module_dirlen
|
||||
&& strncmp(curr_dir, module_dir, module_dirlen) == 0
|
||||
&& (curr_dir[module_dirlen] == '\0' || 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
|
||||
@@ -2041,11 +2043,11 @@ static DIR *secure_opendir(const char *fbuf)
|
||||
* 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
|
||||
* drop EACCES), and without assuming the lexical 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;
|
||||
const char *p = curr_dir + module_dirlen;
|
||||
char modrel[MAXPATHLEN];
|
||||
while (*p == '/')
|
||||
p++;
|
||||
@@ -2054,7 +2056,7 @@ static DIR *secure_opendir(const char *fbuf)
|
||||
errno = ENAMETOOLONG;
|
||||
return NULL;
|
||||
}
|
||||
dfd = vfs_resolve_open_at(module_dirfd, *modrel ? modrel : ".",
|
||||
dfd = secure_relative_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
|
||||
@@ -2062,11 +2064,11 @@ static DIR *secure_opendir(const char *fbuf)
|
||||
const char *relp = fbuf;
|
||||
while (*relp == '/')
|
||||
relp++;
|
||||
dfd = vfs_resolve_open("/", relp, O_RDONLY | O_DIRECTORY, 0);
|
||||
dfd = secure_relative_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);
|
||||
dfd = secure_relative_open(NULL, fbuf, O_RDONLY | O_DIRECTORY, 0);
|
||||
}
|
||||
|
||||
if (dfd < 0)
|
||||
@@ -2104,7 +2106,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
|
||||
/* 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
|
||||
* - a daemon/hardened sender (secure_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;
|
||||
@@ -2116,7 +2118,7 @@ static void send_directory(int f, struct file_list *flist, char *fbuf, int len,
|
||||
* 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()
|
||||
if (f >= 0 && !symlink_optout_allowed() && (secure_relpath_active()
|
||||
|| !(copy_links || copy_unsafe_links || copy_dirlinks || insecure_links)))
|
||||
d = secure_opendir(fbuf);
|
||||
else
|
||||
@@ -2558,7 +2560,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;
|
||||
|
||||
+105
-96
@@ -29,6 +29,7 @@ extern int do_xfers;
|
||||
extern int stdout_format_has_i;
|
||||
extern int logfile_format_has_i;
|
||||
extern int am_root;
|
||||
extern int operator_path_resolve;
|
||||
extern int am_server;
|
||||
extern int am_daemon;
|
||||
extern int inc_recurse;
|
||||
@@ -132,7 +133,7 @@ static int start_delete_delay_temp(void)
|
||||
|
||||
dry_run = 0;
|
||||
if (!get_tmpname(fnametmp, "deldelay", False)
|
||||
|| (deldelay_fd = vfs_mkstemp(fnametmp, 0600)) < 0) {
|
||||
|| (deldelay_fd = do_mkstemp(fnametmp, 0600)) < 0) {
|
||||
rprintf(FINFO, "NOTE: Unable to create delete-delay temp file%s.\n",
|
||||
inc_recurse ? "" : " -- switching to --delete-after");
|
||||
delete_during = 0;
|
||||
@@ -413,7 +414,7 @@ static inline int any_time_differs(stat_x *sxp, struct file_struct *file, UNUSED
|
||||
#ifdef SUPPORT_CRTIMES
|
||||
if (!differs && crtimes_ndx) {
|
||||
if (sxp->crtime == 0)
|
||||
sxp->crtime = vfs_get_create_time(fname, &sxp->st);
|
||||
sxp->crtime = get_create_time(fname, &sxp->st);
|
||||
differs = !same_time(sxp->crtime, 0, F_CRTIME(file), 0);
|
||||
}
|
||||
#endif
|
||||
@@ -539,7 +540,7 @@ void itemize(const char *fnamecmp, struct file_struct *file, int ndx, int statre
|
||||
#ifdef SUPPORT_CRTIMES
|
||||
if (crtimes_ndx) {
|
||||
if (sxp->crtime == 0)
|
||||
sxp->crtime = vfs_get_create_time(fnamecmp, &sxp->st);
|
||||
sxp->crtime = get_create_time(fnamecmp, &sxp->st);
|
||||
if (!same_time(sxp->crtime, 0, F_CRTIME(file), 0))
|
||||
iflags |= ITEM_REPORT_CRTIME;
|
||||
}
|
||||
@@ -655,7 +656,7 @@ int quick_check_ok(enum filetype ftype, const char *fn, struct file_struct *file
|
||||
case FT_SYMLINK: {
|
||||
#ifdef SUPPORT_LINKS
|
||||
char lnk[MAXPATHLEN];
|
||||
int len = vfs_readlink(fn, lnk, MAXPATHLEN-1);
|
||||
int len = do_readlink(fn, lnk, MAXPATHLEN-1);
|
||||
if (len <= 0)
|
||||
return 0;
|
||||
lnk[len] = '\0';
|
||||
@@ -931,15 +932,15 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
|
||||
copy_to = buf;
|
||||
}
|
||||
cleanup_set(copy_to, NULL, NULL, -1, -1);
|
||||
if (copy_file(src, copy_to, fd_w, file->mode, 0) < 0) {
|
||||
if (copy_file(src, copy_to, fd_w, file->mode) < 0) {
|
||||
if (INFO_GTE(COPY, 1)) {
|
||||
rsyserr(FINFO, errno, "copy_file %s => %s",
|
||||
full_fname(src), copy_to);
|
||||
}
|
||||
/* Try to clean up. copy_to's parent components are peer-named
|
||||
* and can be raced to a symlink, so resolve each with O_NOFOLLOW
|
||||
* via vfs_unlink_at() like the other generator-side unlinks. */
|
||||
vfs_unlink(VFS_AT_FDCWD, copy_to, 0);
|
||||
* via do_unlink_at() like the other generator-side unlinks. */
|
||||
do_unlink_at(copy_to);
|
||||
cleanup_disable();
|
||||
return -1;
|
||||
}
|
||||
@@ -954,13 +955,14 @@ static int copy_altdest_file(const char *src, const char *dest, struct file_stru
|
||||
/* Stat an alternate-basis candidate (basis_dir[j]/fname) for a daemon /./
|
||||
* inner-module chroot through the secure resolver, so a --compare/copy/link-dest
|
||||
* basis can't reach outside the inner module via a symlinked parent (the kernel
|
||||
* chroot confines only the outer path). vfs_resolve_open() refuses a parent
|
||||
* chroot confines only the outer path). secure_relative_open() refuses a parent
|
||||
* that escapes beneath the module root. Plain link_stat() everywhere else --
|
||||
* the non-chroot daemon sanitizes basis paths already, and a local receiver must
|
||||
* still follow an operator's --link-dest=../backup. */
|
||||
static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
{
|
||||
extern int am_chrooted;
|
||||
extern int operator_path_resolve;
|
||||
extern unsigned int module_dirlen;
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* The basis dir (--link-dest/--compare-dest/--copy-dest) is an operator-
|
||||
@@ -974,11 +976,9 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
* resolver) below. Only when am_root >= 0: link_stat_at() omits the
|
||||
* fake-super %stat xattr that link_stat() folds in, so --fake-super keeps
|
||||
* the plain path (a lower-severity, non-root basis lookup). */
|
||||
if (!am_daemon && am_root >= 0 && !vfs_symlink_optout_allowed()) {
|
||||
if (!am_daemon && am_root >= 0 && !symlink_optout_allowed()) {
|
||||
const char *leaf;
|
||||
/* non-daemon path: is_operator only gates the daemon module-confinement
|
||||
* (a no-op here), so the ownership walk is identical either way. */
|
||||
int dfd = vfs_owner_walk_parent(path, &leaf, 0);
|
||||
int dfd = owner_walk_parent(path, &leaf);
|
||||
int r, e;
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
@@ -989,7 +989,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
return r;
|
||||
}
|
||||
/* A non-chroot daemon serving an operator/peer alt-dest basis: resolve through
|
||||
* the ownership walk with module-ROOT confinement (is_operator=1) so an
|
||||
* the ownership walk with module-ROOT confinement (operator_path_resolve) so an
|
||||
* in-module symlink whose target lands OUTSIDE the module is refused -- the
|
||||
* basis then looks absent and the file transfers normally instead of being
|
||||
* stat'd/read/linked through the link (closes the --compare-dest=/E read
|
||||
@@ -1001,14 +1001,16 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
* and must keep the plain link_stat below (#915/#930). The leaf is taken
|
||||
* under the confined parent with O_NOFOLLOW/AT_SYMLINK_NOFOLLOW, so
|
||||
* --copy-links can't follow a leaf symlink out of the module. */
|
||||
if (am_daemon && !am_chrooted && path[0] == '/' && !vfs_symlink_optout_allowed()) {
|
||||
if (am_daemon && !am_chrooted && path[0] == '/' && !symlink_optout_allowed()) {
|
||||
const char *leaf;
|
||||
int dfd, e;
|
||||
dfd = vfs_owner_walk_parent(path, &leaf, 1);
|
||||
int dfd, e, save = operator_path_resolve;
|
||||
operator_path_resolve = 1;
|
||||
dfd = owner_walk_parent(path, &leaf);
|
||||
operator_path_resolve = save;
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
if (am_root >= 0) {
|
||||
int r = vfs_lstat(dfd, leaf, stp, 0);
|
||||
int r = do_lstat_atfd(dfd, leaf, stp);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
@@ -1019,12 +1021,12 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
/* --fake-super: O_NOFOLLOW-open the held leaf (the daemon owns its
|
||||
* fake-super files) so the %stat xattr link_stat() would fold is
|
||||
* preserved while a leaf symlink is still refused. */
|
||||
int lfd = vfs_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
|
||||
int lfd = do_open_atfd(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK, 0);
|
||||
STRUCT_STAT xst;
|
||||
e = errno;
|
||||
close(dfd);
|
||||
if (lfd < 0) { errno = e; return -1; }
|
||||
if (vfs_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }
|
||||
if (do_fstat(lfd, stp) < 0) { e = errno; close(lfd); errno = e; return -1; }
|
||||
if (get_stat_xattr(NULL, lfd, stp, &xst) == 0)
|
||||
*stp = xst;
|
||||
close(lfd);
|
||||
@@ -1032,7 +1034,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
}
|
||||
#else
|
||||
{
|
||||
int r = vfs_lstat(dfd, leaf, stp, 0);
|
||||
int r = do_lstat_atfd(dfd, leaf, stp);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
@@ -1041,7 +1043,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !vfs_symlink_optout_allowed()) {
|
||||
if (am_daemon && am_chrooted && module_dirlen && path[0] != '/' && !symlink_optout_allowed()) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
if (slash) {
|
||||
char dir[MAXPATHLEN];
|
||||
@@ -1050,7 +1052,7 @@ static int basis_link_stat(const char *path, STRUCT_STAT *stp)
|
||||
if (dlen >= sizeof dir) { errno = ENAMETOOLONG; return -1; }
|
||||
memcpy(dir, path, dlen);
|
||||
dir[dlen] = '\0';
|
||||
if ((dfd = vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0)) < 0)
|
||||
if ((dfd = secure_relative_dirfd(NULL, dir)) < 0)
|
||||
return -1;
|
||||
r = link_stat_at(dfd, slash + 1, stp, 0);
|
||||
e = errno;
|
||||
@@ -1113,7 +1115,7 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
if (find_exact_for_existing) {
|
||||
if (alt_dest_type == LINK_DEST && real_st.st_dev == sxp->st.st_dev && real_st.st_ino == sxp->st.st_ino)
|
||||
return -1;
|
||||
if (vfs_unlink(VFS_AT_FDCWD, fname, 0) < 0 && errno != ENOENT)
|
||||
if (do_unlink_at(fname) < 0 && errno != ENOENT)
|
||||
goto got_nothing_for_ya;
|
||||
}
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
@@ -1122,11 +1124,15 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
* resolve the link source via the ownership walk so a foreign-owned
|
||||
* symlink raced in after the basis_link_stat() check is still
|
||||
* refused (matching basis_link_stat's !am_daemon gate). A daemon
|
||||
* keeps its stronger module-anchored confinement (vfs_link_at's
|
||||
* vfs_relpath_active path) -- the ownership walk would follow an
|
||||
* keeps its stronger module-anchored confinement (do_link_at's
|
||||
* secure_relpath_active path) -- the ownership walk would follow an
|
||||
* operator-owned symlink out of the module. */
|
||||
int hlok = hard_link_one(file, fname, cmpbuf, 1,
|
||||
!am_daemon ? VFS_OPERATOR_PATH : 0);
|
||||
int hlok, op = !am_daemon;
|
||||
if (op)
|
||||
operator_path_resolve = 1;
|
||||
hlok = hard_link_one(file, fname, cmpbuf, 1);
|
||||
if (op)
|
||||
operator_path_resolve = 0;
|
||||
if (!hlok)
|
||||
goto try_a_copy;
|
||||
if (atimes_ndx)
|
||||
@@ -1158,10 +1164,9 @@ static int try_dests_reg(struct file_struct *file, char *fname, int ndx,
|
||||
#endif
|
||||
/* NB: the copy-dest basis read is deliberately NOT routed through the
|
||||
* ownership walk: copy_altdest_file()->copy_file() also opens the dest
|
||||
* and copies xattrs through a held O_NOFOLLOW fd, and passing
|
||||
* VFS_OPERATOR_PATH across that re-opens the copy_xattrs parent-
|
||||
* symlink race (copy-xattrs-symlink-race) -- so copy_file gets flags 0.
|
||||
* basis_link_stat() already
|
||||
* and copies xattrs through a held O_NOFOLLOW fd, and forcing
|
||||
* operator_path_resolve across that re-opens the copy_xattrs parent-
|
||||
* symlink race (copy-xattrs-symlink-race). basis_link_stat() already
|
||||
* refuses a foreign-owned basis symlink, closing the static escape; the
|
||||
* post-stat race on an absolute copy-dest basis is a documented residual. */
|
||||
if (!dry_run && copy_altdest_file(cmpbuf, fname, file) < 0) {
|
||||
@@ -1261,11 +1266,7 @@ static int try_dests_non(struct file_struct *file, char *fname, int ndx,
|
||||
&& !IS_SPECIAL(file->mode) && !IS_DEVICE(file->mode)
|
||||
#endif
|
||||
&& !S_ISDIR(file->mode)) {
|
||||
/* cmpbuf is the alt-dest (--link-dest) basis: for a non-daemon
|
||||
* receiver it is an operator path (owner walk; matches the
|
||||
* hard_link_one() path above and basis_link_stat's !am_daemon gate).
|
||||
* fname is the transfer destination (secure receiver resolve). */
|
||||
if (vfs_link_at(cmpbuf, fname, !am_daemon ? VFS_OPERATOR_PATH : 0, 0) < 0) {
|
||||
if (do_link_at(cmpbuf, fname) < 0) {
|
||||
/* CAN_HARDLINK_SYMLINK/_SPECIAL answer for whatever
|
||||
* filesystem the build tree sat on; the destination is
|
||||
* free to disagree, and one host can hold both (macOS
|
||||
@@ -1381,8 +1382,8 @@ static BOOL is_below(struct file_struct *file, struct file_struct *subtree)
|
||||
|
||||
/* Held-dirfd helpers for the per-entry ops below: when the secure resolver is
|
||||
* active they act on the entry's basename relative to its cached directory fd
|
||||
* (vfs_cached_dirfd, keyed on file->dirname), else fall back to the full-path
|
||||
* do_*_at wrappers (behaviour-identical). vfs_cached_dirfd() declines when fname
|
||||
* (held_dfd_for, keyed on file->dirname), else fall back to the full-path
|
||||
* do_*_at wrappers (behaviour-identical). held_dfd_for() declines when fname
|
||||
* isn't in file->dirname (e.g. the single-file local_name dest), and the leaf
|
||||
* is derived from fname, not file->basename. */
|
||||
static int gen_entry_stat(const char *fname, struct file_struct *file,
|
||||
@@ -1391,7 +1392,7 @@ static int gen_entry_stat(const char *fname, struct file_struct *file,
|
||||
int dfd;
|
||||
/* link_stat_at folds in no fake-super xattr, so only use it when
|
||||
* am_root >= 0 (where link_stat's get_stat_xattr is a no-op anyway). */
|
||||
if (am_root >= 0 && (dfd = vfs_cached_dirfd(fname, file)) >= 0) {
|
||||
if (am_root >= 0 && (dfd = held_dfd_for(fname, file)) >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return link_stat_at(dfd, slash ? slash + 1 : fname, stp, follow_dirlinks);
|
||||
}
|
||||
@@ -1400,27 +1401,27 @@ static int gen_entry_stat(const char *fname, struct file_struct *file,
|
||||
|
||||
static int gen_entry_mkdir(char *fname, struct file_struct *file, mode_t mode)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(fname, file);
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
char *slash = strrchr(fname, '/');
|
||||
return vfs_mkdir(dfd, slash ? slash + 1 : fname, mode, 0);
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return do_mkdir_atfd(dfd, slash ? slash + 1 : fname, mode);
|
||||
}
|
||||
return vfs_mkdir(VFS_AT_FDCWD, fname, mode, 0);
|
||||
return do_mkdir_at(fname, mode);
|
||||
}
|
||||
|
||||
static int gen_entry_chmod(const char *fname, struct file_struct *file, mode_t mode)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(fname, file);
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
return vfs_chmod(dfd, slash ? slash + 1 : fname, mode, 0);
|
||||
return do_chmod_atfd(dfd, slash ? slash + 1 : fname, mode);
|
||||
}
|
||||
return vfs_chmod(VFS_AT_FDCWD, fname, mode, 0);
|
||||
return do_chmod_at(fname, mode);
|
||||
}
|
||||
|
||||
static void gen_entry_set_times(const char *fname, struct file_struct *file, STRUCT_STAT *stp)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(fname, file);
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
if (set_times_at(dfd, slash ? slash + 1 : fname, stp) != -2)
|
||||
@@ -1431,12 +1432,12 @@ static void gen_entry_set_times(const char *fname, struct file_struct *file, STR
|
||||
|
||||
static int gen_entry_symlink(const char *slnk, const char *path, struct file_struct *file)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(path, file);
|
||||
int dfd = held_dfd_for(path, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return vfs_symlink(slnk, dfd, slash ? slash + 1 : path, 0);
|
||||
return do_symlink_atfd(slnk, dfd, slash ? slash + 1 : path);
|
||||
}
|
||||
return vfs_symlink(slnk, VFS_AT_FDCWD, path, 0);
|
||||
return do_symlink_at(slnk, path);
|
||||
}
|
||||
|
||||
/* True when this build compiled no fd-relative primitive able to create this
|
||||
@@ -1470,10 +1471,10 @@ static int no_atfd_mknod_primitive(mode_t mode)
|
||||
static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mode, dev_t rdev)
|
||||
{
|
||||
int dfd;
|
||||
/* vfs_mknod_atfd can't create a socket (no portable bindat); fall back. */
|
||||
if (!S_ISSOCK(mode) && (dfd = vfs_cached_dirfd(path, file)) >= 0) {
|
||||
/* do_mknod_atfd can't create a socket (no portable bindat); fall back. */
|
||||
if (!S_ISSOCK(mode) && (dfd = held_dfd_for(path, file)) >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
int ret = vfs_mknod(dfd, slash ? slash + 1 : path, mode, rdev, 0);
|
||||
int ret = do_mknod_atfd(dfd, slash ? slash + 1 : path, mode, rdev);
|
||||
/* Fall through to the unconfined path-based create only where this
|
||||
* build compiled no fd-relative primitive for this kind of node --
|
||||
* SECURITY.md's rule for a platform that cannot be secure at all.
|
||||
@@ -1484,17 +1485,17 @@ static int gen_entry_mknod(const char *path, struct file_struct *file, mode_t mo
|
||||
if (ret == 0 || !no_atfd_mknod_primitive(mode))
|
||||
return ret;
|
||||
}
|
||||
return vfs_mknod(VFS_AT_FDCWD, path, mode, rdev, 0);
|
||||
return do_mknod_at(path, mode, rdev);
|
||||
}
|
||||
|
||||
static int gen_entry_unlink(const char *path, struct file_struct *file)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(path, file);
|
||||
int dfd = held_dfd_for(path, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
return vfs_unlink(dfd, slash ? slash + 1 : path, 0);
|
||||
return do_unlink_atfd(dfd, slash ? slash + 1 : path, 0);
|
||||
}
|
||||
return vfs_unlink(VFS_AT_FDCWD, path, 0);
|
||||
return do_unlink_at(path);
|
||||
}
|
||||
|
||||
/* opath and npath are both expected to live in the entry's directory (the
|
||||
@@ -1502,14 +1503,14 @@ static int gen_entry_unlink(const char *path, struct file_struct *file)
|
||||
* single renameat() within it, else fall back to the full-path wrapper. */
|
||||
static int gen_entry_rename(const char *opath, const char *npath, struct file_struct *file)
|
||||
{
|
||||
int odfd = vfs_cached_dirfd(opath, file);
|
||||
int ndfd = vfs_cached_dirfd(npath, file);
|
||||
int odfd = held_dfd_for(opath, file);
|
||||
int ndfd = held_dfd_for(npath, file);
|
||||
if (odfd >= 0 && ndfd >= 0) {
|
||||
const char *os = strrchr(opath, '/');
|
||||
const char *ns = strrchr(npath, '/');
|
||||
return vfs_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);
|
||||
return do_rename_atfd(odfd, os ? os + 1 : opath, ndfd, ns ? ns + 1 : npath);
|
||||
}
|
||||
return vfs_rename_at(opath, npath, 0, 0); /* both live in the entry's dir (transfer) */
|
||||
return do_rename_at(opath, npath);
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
@@ -1521,7 +1522,7 @@ static int gen_entry_rename(const char *opath, const char *npath, struct file_st
|
||||
* set_file_attrs' held-fd handling). */
|
||||
static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file_struct *file)
|
||||
{
|
||||
int dfd = vfs_cached_dirfd(fname, file);
|
||||
int dfd = held_dfd_for(fname, file);
|
||||
int xfd = -1, sfd = -1, ret;
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
@@ -1539,18 +1540,18 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
|
||||
}
|
||||
}
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
else if (vfs_relpath_active()) {
|
||||
/* No cached parent dirfd (e.g. a path deeper than the dirfd cache, or a
|
||||
* raced parent) but we must confine: re-pin the dest leaf through the
|
||||
* secure resolver so copy_xattrs uses fsetxattr, not a path-based
|
||||
* lsetxattr a flipped parent could redirect out of tree. A raced
|
||||
* parent/leaf makes this fail -> refuse rather than path-write. */
|
||||
else if (secure_relpath_active()) {
|
||||
/* No cached parent dirfd (a path deeper than the dirfd cache, or a raced
|
||||
* parent) but we must confine: re-pin the dest leaf through the secure
|
||||
* resolver so copy_xattrs uses fsetxattr, not a path-based lsetxattr a
|
||||
* flipped parent could redirect out of tree. A raced parent/leaf makes
|
||||
* this fail -> refuse rather than path-write. */
|
||||
int odir = 0;
|
||||
# ifdef O_DIRECTORY
|
||||
if (S_ISDIR(file->mode))
|
||||
odir = O_DIRECTORY;
|
||||
# endif
|
||||
xfd = vfs_resolve_open(NULL, fname,
|
||||
xfd = secure_relative_open(NULL, fname,
|
||||
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);
|
||||
if (xfd < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
@@ -1566,18 +1567,20 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
|
||||
* through the operator ownership walk. Refuse (don't path-read) when we are
|
||||
* meant to confine but can't pin; a non-hardened receiver path-reads (sfd<0). */
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW
|
||||
if (vfs_relpath_active() && src && *src && !vfs_symlink_optout_allowed()) {
|
||||
if (secure_relpath_active() && src && *src && !symlink_optout_allowed()) {
|
||||
int odir = 0;
|
||||
#ifdef O_DIRECTORY
|
||||
if (S_ISDIR(file->mode)) /* vfs_resolve_open rejects a dir leaf without this */
|
||||
if (S_ISDIR(file->mode)) /* secure_relative_open rejects a dir leaf without this */
|
||||
odir = O_DIRECTORY;
|
||||
#endif
|
||||
if (src[0] != '/')
|
||||
sfd = vfs_resolve_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);
|
||||
sfd = secure_relative_open(NULL, src, O_RDONLY | O_NOFOLLOW | odir, 0);
|
||||
else {
|
||||
int sdfd, e;
|
||||
int save = operator_path_resolve, sdfd, e;
|
||||
const char *leaf;
|
||||
sdfd = vfs_owner_walk_parent(src, &leaf, 1);
|
||||
operator_path_resolve = 1;
|
||||
sdfd = owner_walk_parent(src, &leaf);
|
||||
operator_path_resolve = save;
|
||||
if (sdfd >= 0) {
|
||||
sfd = openat(sdfd, leaf, O_RDONLY | O_NOFOLLOW | odir | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
|
||||
e = errno; close(sdfd); errno = e;
|
||||
@@ -1592,13 +1595,6 @@ static int gen_entry_copy_xattrs(const char *src, const char *fname, struct file
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef STRICT_CONFINEMENT
|
||||
/* In the confined regime the dfd/re-pin paths above yield xfd >= 0 or already
|
||||
* returned -1; reaching copy_xattrs with xfd < 0 while confined would let it
|
||||
* path-write the dest xattrs (the copy-xattrs fallback class) -- abort. */
|
||||
if (xfd < 0 && vfs_must_be_confined(fname, 0))
|
||||
vfs_strict_confine_fail(fname, "gen_entry_copy_xattrs dest");
|
||||
#endif
|
||||
ret = copy_xattrs(src, sfd, fname, xfd);
|
||||
if (sfd >= 0)
|
||||
@@ -1720,10 +1716,10 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
}
|
||||
if (relative_paths && !implied_dirs && file->mode != 0
|
||||
&& vfs_stat(VFS_AT_FDCWD, dn, &sx.st, 0) < 0) {
|
||||
&& do_stat_at(dn, &sx.st) < 0) {
|
||||
if (dry_run)
|
||||
goto parent_is_dry_missing;
|
||||
if (vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0) {
|
||||
if (make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"recv_generator: mkdir %s failed",
|
||||
full_fname(dn));
|
||||
@@ -1877,9 +1873,9 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
if (real_ret != 0 && gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST) {
|
||||
/* The parent may have just been created by make_path(), so
|
||||
* drop any cached (failed) dir fd before the retry. */
|
||||
vfs_dircache_reset();
|
||||
reset_dir_fd_cache();
|
||||
if (!relative_paths || errno != ENOENT
|
||||
|| vfs_make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH, 0) < 0
|
||||
|| make_path(fname, MKP_DROP_NAME | MKP_SKIP_SLASH) < 0
|
||||
|| (gen_entry_mkdir(fname, file, file->mode|added_perms) < 0 && errno != EEXIST)) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"recv_generator: mkdir %s failed",
|
||||
@@ -2229,7 +2225,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
|
||||
if (write_devices && IS_DEVICE(sx.st.st_mode) && sx.st.st_size == 0) {
|
||||
/* This early open into fd skips the regular open below. */
|
||||
if ((fd = vfs_open_nofollow(fnamecmp, O_RDONLY)) >= 0)
|
||||
if ((fd = do_open_nofollow(fnamecmp, O_RDONLY)) >= 0)
|
||||
real_sx.st.st_size = sx.st.st_size = get_device_size(fd, fnamecmp);
|
||||
}
|
||||
|
||||
@@ -2242,7 +2238,9 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
/* The --partial-dir basis is an operator/peer path: unlink it
|
||||
* through the exclude-aware ownership walk so a symlinked
|
||||
* partial-dir can't delete a file in an excluded subtree. */
|
||||
vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH);
|
||||
operator_path_resolve = 1;
|
||||
do_unlink_at(partialptr);
|
||||
operator_path_resolve = 0;
|
||||
handle_partial_dir(partialptr, PDIR_DELETE);
|
||||
}
|
||||
set_file_attrs(fname, file, &sx, NULL, maybe_ATTRS_REPORT | maybe_ATTRS_ACCURATE_TIME);
|
||||
@@ -2282,20 +2280,25 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
if (read_batch || whole_file) {
|
||||
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
|
||||
/* The --backup-dir (backupptr) is an operator path; this in-place
|
||||
* backup bypasses make_backup(), so get_backup_name() (make_path) and
|
||||
* copy_file() below are passed VFS_OPERATOR_PATH to resolve it with the
|
||||
* ownership walk instead of following any symlink. */
|
||||
* backup bypasses make_backup(), so set operator_path_resolve here
|
||||
* too -- get_backup_name() (make_path) and copy_file() then resolve
|
||||
* it with the ownership walk instead of following any symlink. */
|
||||
operator_path_resolve = 1;
|
||||
if (!(backupptr = get_backup_name(fname))) {
|
||||
operator_path_resolve = 0;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
|
||||
operator_path_resolve = 0;
|
||||
goto pretend_missing;
|
||||
}
|
||||
if (copy_file(fname, backupptr, -1, back_file->mode, VFS_OPERATOR_PATH) < 0) {
|
||||
if (copy_file(fname, backupptr, -1, back_file->mode) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
unmake_file(back_file);
|
||||
back_file = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
goto notify_others;
|
||||
}
|
||||
@@ -2307,7 +2310,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
}
|
||||
|
||||
/* open the file */
|
||||
if (fd < 0 && (fd = vfs_open_checklinks(fnamecmp)) < 0) {
|
||||
if (fd < 0 && (fd = do_open_checklinks(fnamecmp)) < 0) {
|
||||
rsyserr(FERROR, errno, "failed to open %s, continuing",
|
||||
full_fname(fnamecmp));
|
||||
pretend_missing:
|
||||
@@ -2325,25 +2328,31 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
if (inplace && make_backups > 0 && fnamecmp_type == FNAMECMP_FNAME) {
|
||||
/* Operator --backup-dir, bypassing make_backup(): resolve get_backup_name()
|
||||
* (make_path), the unlink and the create with the ownership walk. */
|
||||
operator_path_resolve = 1;
|
||||
if (!(backupptr = get_backup_name(fname))) {
|
||||
operator_path_resolve = 0;
|
||||
goto cleanup;
|
||||
}
|
||||
if (!(back_file = make_file(fname, NULL, NULL, 0, NO_FILTERS))) {
|
||||
operator_path_resolve = 0;
|
||||
goto pretend_missing;
|
||||
}
|
||||
if (robust_unlink(backupptr, VFS_OPERATOR_PATH) && errno != ENOENT) {
|
||||
if (robust_unlink(backupptr) && errno != ENOENT) {
|
||||
operator_path_resolve = 0;
|
||||
rsyserr(FERROR_XFER, errno, "unlink %s",
|
||||
full_fname(backupptr));
|
||||
unmake_file(back_file);
|
||||
back_file = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
if ((f_copy = vfs_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600, VFS_OPERATOR_PATH)) < 0) {
|
||||
if ((f_copy = do_open_at(backupptr, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600)) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
rsyserr(FERROR_XFER, errno, "open %s", full_fname(backupptr));
|
||||
unmake_file(back_file);
|
||||
back_file = NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
operator_path_resolve = 0;
|
||||
fnamecmp_type = FNAMECMP_BACKUP;
|
||||
}
|
||||
|
||||
@@ -2427,7 +2436,7 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
|
||||
if (f_copy >= 0)
|
||||
close(f_copy);
|
||||
/* backupptr's data/xattrs were written safely (confined create under
|
||||
* VFS_OPERATOR_PATH, held-fd xattr copy above). This metadata set
|
||||
* operator_path_resolve, held-fd xattr copy above). This metadata set
|
||||
* re-resolves backupptr by path and is NOT wrapped in operator mode:
|
||||
* set_file_attrs() also drives the path-based xattr set whose held-fd
|
||||
* race-fix operator mode would defeat (cf. the copy-dest note in
|
||||
@@ -2489,7 +2498,7 @@ int atomic_create(struct file_struct *file, char *fname, const char *slnk, const
|
||||
#endif
|
||||
} else if (hlnk) {
|
||||
#ifdef SUPPORT_HARD_LINKS
|
||||
if (!hard_link_one(file, create_name, hlnk, 0, 0))
|
||||
if (!hard_link_one(file, create_name, hlnk, 0))
|
||||
return 0;
|
||||
#else
|
||||
return 0;
|
||||
|
||||
@@ -473,12 +473,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_at(oldname, fname) < 0) {
|
||||
enum logcode code;
|
||||
if (terse) {
|
||||
if (!INFO_GTE(NAME, 1))
|
||||
|
||||
@@ -117,6 +117,13 @@ static time_t last_io_out;
|
||||
* transfer timeout and may be supplied by the module or client. */
|
||||
static time_t daemon_handshake_deadline;
|
||||
|
||||
/* Wall-clock bound the client puts on establishing a daemon connection made
|
||||
* through a remote shell (daemon_connection == 1): spawning the helper, its
|
||||
* connect()/TLS handshake, and the exchange of the daemon greeting all happen
|
||||
* before any buffered I/O begins, so --contimeout can time the whole phase the
|
||||
* same way the socket path times its connect(). */
|
||||
static time_t client_connect_deadline;
|
||||
|
||||
static int write_batch_monitor_in = -1;
|
||||
static int write_batch_monitor_out = -1;
|
||||
|
||||
@@ -146,17 +153,28 @@ static int handshake_poll_timeout_ms(void)
|
||||
time_t now, left;
|
||||
int timeout = poll_timeout_ms();
|
||||
|
||||
if (!daemon_handshake_deadline)
|
||||
if (!daemon_handshake_deadline && !client_connect_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 (daemon_handshake_deadline) {
|
||||
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;
|
||||
}
|
||||
if (client_connect_deadline) {
|
||||
left = client_connect_deadline - now;
|
||||
if (left <= 0) {
|
||||
rprintf(FERROR, "[%s] connection timed out -- exiting\n", who_am_i());
|
||||
exit_cleanup(RERR_CONTIMEOUT);
|
||||
}
|
||||
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
|
||||
timeout = (int)left * 1000;
|
||||
}
|
||||
if (left <= INT_MAX / 1000 && left * 1000 < timeout)
|
||||
timeout = (int)left * 1000;
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@@ -1303,6 +1321,14 @@ void set_daemon_handshake_timeout(int secs)
|
||||
daemon_handshake_deadline = 0;
|
||||
}
|
||||
|
||||
void set_client_connect_timeout(int secs)
|
||||
{
|
||||
if (secs > 0)
|
||||
client_connect_deadline = time(NULL) + secs;
|
||||
else
|
||||
client_connect_deadline = 0;
|
||||
}
|
||||
|
||||
static void check_for_d_option_error(const char *msg)
|
||||
{
|
||||
static const char rsync263_opts[] = "BCDHIKLPRSTWabceghlnopqrtuvxz";
|
||||
|
||||
@@ -55,6 +55,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];
|
||||
@@ -165,8 +166,8 @@ static void logfile_open(void)
|
||||
* 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);
|
||||
int fd = open_no_attacker_symlinks(logfile_name,
|
||||
O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||||
logfile_fp = fd >= 0 ? fdopen(fd, "a") : NULL;
|
||||
if (!logfile_fp && fd >= 0)
|
||||
close(fd);
|
||||
@@ -647,7 +648,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);
|
||||
|
||||
@@ -70,6 +70,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 +106,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;
|
||||
@@ -519,6 +521,16 @@ static pid_t do_cmd(char *cmd, char *machine, char *user, char **remote_argv, in
|
||||
char *args[MAX_ARGS], *need_to_free = NULL;
|
||||
pid_t pid;
|
||||
int dash_l_set = 0;
|
||||
#ifdef SUPPORT_IDN
|
||||
char idn_machine[1024];
|
||||
|
||||
/* A daemon-over-remote-shell host is ours to resolve, so give the helper
|
||||
* the A-label form. A "host:path" transfer is left alone because that
|
||||
* name belongs to the user's ssh, which may be matching it against an
|
||||
* ssh_config Host pattern. */
|
||||
if (machine && daemon_connection > 0 && idn_to_ascii(machine, 1, idn_machine, sizeof idn_machine))
|
||||
machine = idn_machine;
|
||||
#endif
|
||||
|
||||
if (!read_batch && !local_server) {
|
||||
char *t, *f, in_quote = '\0';
|
||||
@@ -735,13 +747,13 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
}
|
||||
|
||||
/* 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)) {
|
||||
@@ -752,7 +764,7 @@ static char *get_local_name(struct file_list *flist, char *dest_path)
|
||||
*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 +811,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,7 +850,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) {
|
||||
if (dry_run && mkpath_dest_arg && do_stat(dest_path, &st) < 0) {
|
||||
/* --mkpath would have created this parent dir, but a dry run did
|
||||
* not, so don't chdir into it; flag the destination as not yet
|
||||
* present (as the dir-creation path above does) so the generator
|
||||
@@ -860,12 +872,12 @@ 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++) {
|
||||
@@ -875,13 +887,13 @@ static void check_alt_basis_dirs(void)
|
||||
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
|
||||
* (vs the destination 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;
|
||||
int len = curr_dir_len + 1 + bd_len + 1;
|
||||
char *new = new_array(char, len);
|
||||
if (dry_run > 1 && slash && strncmp(bdir, "../", 3) == 0) {
|
||||
/* We want to remove only one leading "../" prefix for
|
||||
@@ -889,13 +901,13 @@ static void check_alt_basis_dirs(void)
|
||||
* 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 +1035,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 +1055,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);
|
||||
@@ -1600,7 +1612,7 @@ static int start_client(int argc, char *argv[])
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
}
|
||||
|
||||
if (connect_timeout) {
|
||||
if (connect_timeout && !daemon_connection) {
|
||||
rprintf(FERROR, "The --contimeout option may only be "
|
||||
"used when connecting to an rsync daemon.\n");
|
||||
exit_cleanup(RERR_SYNTAX);
|
||||
@@ -1632,6 +1644,14 @@ static int start_client(int argc, char *argv[])
|
||||
(void)env_port;
|
||||
#endif
|
||||
|
||||
/* For a daemon reached through a remote shell, the "connection" rsync
|
||||
* waits on is: the helper is spawned, it establishes its own link (e.g.
|
||||
* rsync-ssl's openssl connect + TLS handshake), and the daemon greeting is
|
||||
* exchanged. Bound that whole phase with --contimeout so the option
|
||||
* behaves for daemon-via-rsh the way it does for a socket connection. */
|
||||
if (daemon_connection && connect_timeout > 0)
|
||||
set_client_connect_timeout(connect_timeout);
|
||||
|
||||
pid = do_cmd(shell_cmd, shell_machine, shell_user, remote_argv, remote_argc, &f_in, &f_out);
|
||||
|
||||
/* if we're running an rsync server on the remote host over a
|
||||
@@ -1639,6 +1659,7 @@ static int start_client(int argc, char *argv[])
|
||||
if (daemon_connection) {
|
||||
int tmpret;
|
||||
tmpret = start_inband_exchange(f_in, f_out, shell_user, remote_argc, remote_argv);
|
||||
set_client_connect_timeout(0);
|
||||
if (tmpret < 0)
|
||||
return tmpret;
|
||||
}
|
||||
@@ -1780,7 +1801,7 @@ static void unset_env_var(const char *var)
|
||||
}
|
||||
|
||||
|
||||
/* The symlink-race-safe path resolver (vfs_resolve_open) holds one open
|
||||
/* The symlink-race-safe path resolver (secure_relative_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.
|
||||
@@ -1812,8 +1833,6 @@ int main(int argc,char *argv[])
|
||||
raw_argc = argc;
|
||||
raw_argv = argv;
|
||||
|
||||
vfs_init();
|
||||
|
||||
raise_fd_limit();
|
||||
|
||||
#ifdef HAVE_SIGACTION
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
extern int module_id;
|
||||
extern int local_server;
|
||||
extern int sanitize_paths;
|
||||
extern int operator_path_resolve;
|
||||
extern int trust_sender_args;
|
||||
extern int trust_sender_filter;
|
||||
extern unsigned int module_dirlen;
|
||||
@@ -60,7 +61,7 @@ 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 */
|
||||
char *confine_root = NULL; /* --confine-root: see syscall.c */
|
||||
unsigned int confine_rootlen = 0;
|
||||
int preserve_uid = 0;
|
||||
int preserve_gid = 0;
|
||||
@@ -122,7 +123,7 @@ int am_daemon = 0;
|
||||
* 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
|
||||
* symlink-race defenses (secure_relative_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;
|
||||
@@ -330,7 +331,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;
|
||||
@@ -620,7 +621,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},
|
||||
@@ -1156,6 +1157,12 @@ static int count_args(const char **argv)
|
||||
return i;
|
||||
}
|
||||
|
||||
/* The largest value parse_size_arg() will accept when no explicit max_value is
|
||||
* given. It is SIZE_MAX/2 rather than SIZE_MAX because the parser computes and
|
||||
* returns the size as a signed ssize_t (with a negative return meaning error),
|
||||
* so this keeps every accepted size representable as a positive ssize_t. */
|
||||
#define SIZE_ARG_MAX ((ssize_t)(SIZE_MAX / 2))
|
||||
|
||||
/* If the size_arg is an invalid string or the value is < min_value, an error
|
||||
* is put into err_buf & the return is -1. Note that this parser does NOT
|
||||
* support negative numbers, so a min_value < 0 doesn't make any sense. */
|
||||
@@ -1165,7 +1172,7 @@ 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);
|
||||
ssize_t size_max = max_value >= 0 ? max_value : SIZE_ARG_MAX;
|
||||
double dsize;
|
||||
|
||||
for (arg = size_arg; isDigit(arg); arg++) {}
|
||||
@@ -2066,14 +2073,17 @@ 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;
|
||||
}
|
||||
/* A 0 value means "as large as this build allows". We resolve it to the
|
||||
* same ceiling parse_size_arg() enforces, so that --max-alloc=0 is exactly
|
||||
* the largest value a user could also have typed, and never a limit that
|
||||
* only the 0 spelling can reach. Note that max_alloc_arg is forwarded to
|
||||
* the peer un-normalized (see server_options()), which is what lets each
|
||||
* side resolve 0 against its own SIZE_MAX -- a 64-bit client and a 32-bit
|
||||
* daemon each get their own ceiling from the one portable spelling. */
|
||||
if (!max_alloc)
|
||||
max_alloc = SIZE_MAX;
|
||||
max_alloc = SIZE_ARG_MAX;
|
||||
|
||||
if (old_style_args < 0) {
|
||||
if (!am_server && protect_args <= 0 && (arg = getenv("RSYNC_OLD_ARGS")) != NULL && *arg) {
|
||||
@@ -2177,7 +2187,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,7 +2381,7 @@ 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);
|
||||
@@ -2644,11 +2654,14 @@ int parse_arguments(int *argc_p, const char ***argv_p)
|
||||
* 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);
|
||||
* operator_path_resolve makes the ownership walk also refuse 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). */
|
||||
int save_opr = operator_path_resolve;
|
||||
operator_path_resolve = 1;
|
||||
filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0);
|
||||
operator_path_resolve = save_opr;
|
||||
if (filesfrom_fd < 0) {
|
||||
snprintf(err_buf, sizeof err_buf,
|
||||
"failed to open files-from file %s: %s\n",
|
||||
@@ -3015,7 +3028,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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -583,7 +583,7 @@ static FILE *OpenConfFile( char *FileName )
|
||||
/* 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);
|
||||
int cfg_fd = open_no_attacker_symlinks( FileName, O_RDONLY, 0 );
|
||||
OpenedFile = cfg_fd >= 0 ? fdopen( cfg_fd, "r" ) : NULL;
|
||||
if( !OpenedFile && cfg_fd >= 0 )
|
||||
close( cfg_fd );
|
||||
|
||||
+71
-60
@@ -72,6 +72,7 @@ extern int fuzzy_basis;
|
||||
extern struct name_num_item *xfer_sum_nni;
|
||||
extern int xfer_sum_len;
|
||||
extern int use_secure_symlinks;
|
||||
extern int operator_path_resolve;
|
||||
|
||||
static struct bitbag *delayed_bits = NULL;
|
||||
static int phase = 0, redoing = 0;
|
||||
@@ -86,7 +87,7 @@ static int updating_basis_or_equiv;
|
||||
|
||||
/* Open a basis/output path that may legitimately be an operator-trusted
|
||||
* ABSOLUTE path -- e.g. an absolute --partial-dir ("a directory reserved for
|
||||
* partial-dir work") or --backup-dir. vfs_resolve_open() deliberately
|
||||
* partial-dir work") or --backup-dir. secure_relative_open() deliberately
|
||||
* rejects an absolute relpath, so feeding it the whole absolute partialptr
|
||||
* (with a NULL basedir) returns EINVAL: the basis fd is then -1, no basis is
|
||||
* mapped, and receive_data() omits every matched block from the whole-file
|
||||
@@ -98,7 +99,7 @@ static int updating_basis_or_equiv;
|
||||
* (trusted) and leaf and confine just the leaf -- exactly how secure_relative_
|
||||
* open already trusts an absolute basedir while O_NOFOLLOW-confining the leaf.
|
||||
* Anything else is a straight pass-through that preserves the strict contract. */
|
||||
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode, int is_operator)
|
||||
static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)
|
||||
{
|
||||
extern int am_daemon, am_chrooted;
|
||||
extern unsigned int module_dirlen;
|
||||
@@ -106,25 +107,25 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
/* "insecure links = yes": restore the 3.2.7 plain open so an operator/peer
|
||||
* alt-dest basis follows symlinks like legacy rsync, the same opt-out the
|
||||
* other daemon symlink sites honour. */
|
||||
if (vfs_symlink_optout_allowed()) {
|
||||
if (symlink_optout_allowed()) {
|
||||
if (basedir) {
|
||||
char fullpath[MAXPATHLEN];
|
||||
if (pathjoin(fullpath, sizeof fullpath, basedir, relpath) >= sizeof fullpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return vfs_open(fullpath, flags, mode);
|
||||
return do_open(fullpath, flags, mode);
|
||||
}
|
||||
return vfs_open(relpath, flags, mode);
|
||||
return do_open(relpath, flags, mode);
|
||||
}
|
||||
|
||||
/* A peer-supplied --partial-dir basis/staging path (is_operator, set by the
|
||||
* recv_files caller) may be absolute (module_dir-prefixed on a non-chroot
|
||||
* daemon) and traverse a symlink the vfs_resolve_open path can't confine:
|
||||
* resolve it with the ownership walk, which follows a uid0/euid-owned symlink
|
||||
* but refuses a foreign one AND (via abspath_outside_confinement) refuses a
|
||||
* target the module's exclude hides -- closing the partial-dir exclude bypass. */
|
||||
if (is_operator) {
|
||||
/* A peer-supplied --partial-dir basis/staging path (operator_path_resolve set
|
||||
* by recv_files) may be absolute (module_dir-prefixed on a non-chroot daemon)
|
||||
* and traverse a symlink the secure_relative_open path can't confine: resolve
|
||||
* it with the ownership walk, which follows a uid0/euid-owned symlink but
|
||||
* refuses a foreign one AND (via abspath_excluded_by_module) refuses a target
|
||||
* the module's exclude hides -- closing the partial-dir exclude bypass. */
|
||||
if (operator_path_resolve) {
|
||||
char fullpath[MAXPATHLEN];
|
||||
const char *p = relpath;
|
||||
if (basedir) {
|
||||
@@ -134,7 +135,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
}
|
||||
p = fullpath;
|
||||
}
|
||||
return vfs_open_owner_walk(p, flags, mode, is_operator);
|
||||
return open_no_attacker_symlinks(p, flags, mode);
|
||||
}
|
||||
|
||||
/* The confined resolver is needed for the sanitizing daemon
|
||||
@@ -146,7 +147,7 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
* "use chroot = yes" makes the kernel root the boundary, so there an alt-dest
|
||||
* basis like --link-dest=../01 must resolve against the cwd as a bare open did
|
||||
* before the hardening (confining it would reject the legitimate sibling
|
||||
* "..", #915). The re-anchoring in vfs_resolve_open() covers the
|
||||
* "..", #915). The re-anchoring in secure_relative_open() covers the
|
||||
* in-module ".." climb for the inner-module case too. */
|
||||
if (!am_daemon || (am_chrooted && !module_dirlen)) {
|
||||
if (basedir) {
|
||||
@@ -155,9 +156,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return vfs_open(fullpath, flags, mode);
|
||||
return do_open(fullpath, flags, mode);
|
||||
}
|
||||
return vfs_open(relpath, flags, mode);
|
||||
return do_open(relpath, flags, mode);
|
||||
}
|
||||
|
||||
if (!basedir && relpath && *relpath == '/') {
|
||||
@@ -177,9 +178,9 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
dirbuf[dlen] = '\0';
|
||||
dir = dirbuf;
|
||||
}
|
||||
return vfs_resolve_open(dir, leaf, flags, mode);
|
||||
return secure_relative_open(dir, leaf, flags, mode);
|
||||
}
|
||||
return vfs_resolve_open(basedir, relpath, flags, mode);
|
||||
return secure_relative_open(basedir, relpath, flags, mode);
|
||||
}
|
||||
|
||||
/* Keep the ownership policy for every attempt to open a one-inplace partial
|
||||
@@ -187,8 +188,13 @@ static int secure_basis_open(const char *basedir, const char *relpath, int flags
|
||||
* must not downgrade an operator-path open to the ordinary path resolver. */
|
||||
static int secure_recv_open(const char *path, int flags, mode_t mode, int owner_walk)
|
||||
{
|
||||
return secure_basis_open(NULL, path, flags, mode,
|
||||
owner_walk ? VFS_OPERATOR_PATH : 0);
|
||||
int fd, save = operator_path_resolve;
|
||||
|
||||
if (owner_walk)
|
||||
operator_path_resolve = 1;
|
||||
fd = secure_basis_open(NULL, path, flags, mode);
|
||||
operator_path_resolve = save;
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* Open a read-only regular file for an in-place update without leaving its
|
||||
@@ -210,7 +216,7 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
|
||||
cfd = secure_recv_open(fname, O_RDONLY|O_NOFOLLOW, 0, one_inplace);
|
||||
if (cfd < 0)
|
||||
goto failed;
|
||||
if (vfs_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {
|
||||
if (do_fstat(cfd, &cst) < 0 || !S_ISREG(cst.st_mode)) {
|
||||
errno = EACCES; /* refused: not the read-only regular file we recover */
|
||||
goto failed;
|
||||
}
|
||||
@@ -246,10 +252,10 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
|
||||
|
||||
/* Local and chrooted transfers retain the existing pathname semantics.
|
||||
* Note the S_ISREG test here is a type check on a stable path, NOT race
|
||||
* protection: vfs_stat() follows a leaf symlink and each call below
|
||||
* protection: do_stat() follows a leaf symlink and each call below
|
||||
* re-resolves the name. The fd-based branch above is the one that
|
||||
* pins an inode; a chroot is what confines this one. */
|
||||
if (vfs_stat(VFS_AT_FDCWD, fname, &cst, VFS_ALLOW_SYMLINK) < 0) {
|
||||
if (do_stat(fname, &cst) < 0) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
@@ -258,11 +264,11 @@ static int open_readonly_inplace(const char *fname, int one_inplace)
|
||||
return -1;
|
||||
}
|
||||
prior_mode = cst.st_mode & CHMOD_BITS;
|
||||
if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode | S_IWUSR, 0) < 0)
|
||||
if (do_chmod_at(fname, prior_mode | S_IWUSR) < 0)
|
||||
return -1;
|
||||
fd = vfs_open(fname, O_WRONLY, 0600);
|
||||
fd = do_open(fname, O_WRONLY, 0600);
|
||||
open_errno = errno;
|
||||
if (vfs_chmod(VFS_AT_FDCWD, fname, prior_mode, 0) < 0) {
|
||||
if (do_chmod_at(fname, prior_mode) < 0) {
|
||||
restore_errno = errno;
|
||||
if (fd >= 0)
|
||||
close(fd);
|
||||
@@ -412,34 +418,34 @@ int open_tmpfile(char *fnametmp, const char *fname, struct file_struct *file)
|
||||
* access to ensure that there is no race condition. They will be
|
||||
* correctly updated after the right owner and group info is set.
|
||||
* (Thanks to snabb@epipe.fi for pointing this out.) */
|
||||
/* For any non-chrooted receiver (vfs_relpath_active()), create the
|
||||
/* For any non-chrooted receiver (secure_relpath_active()), create the
|
||||
* temp file securely so a parent-symlink race can't redirect it. When
|
||||
* the temp lives in the entry's own dir (the common case, no --temp-dir)
|
||||
* use the cached held dir fd; otherwise fall back to vfs_secure_mkstemp. An
|
||||
* use the cached held dir fd; otherwise fall back to secure_mkstemp. An
|
||||
* operator-supplied --temp-dir (tmpdir) gets the ownership-walk resolver
|
||||
* (it may legitimately point outside the tree); the deep-entry-dir fallback,
|
||||
* when the held-dirfd cache declines, gets the strict transfer-path one. */
|
||||
if (vfs_relpath_active()) {
|
||||
int dfd = vfs_cached_dirfd(fnametmp, file);
|
||||
if (secure_relpath_active()) {
|
||||
int dfd = held_dfd_for(fnametmp, file);
|
||||
if (dfd >= 0) {
|
||||
char *slash = strrchr(fnametmp, '/');
|
||||
fd = vfs_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
|
||||
fd = do_mkstemp_atfd(dfd, slash ? slash + 1 : fnametmp,
|
||||
(file->mode|added_perms) & INITACCESSPERMS);
|
||||
} else
|
||||
fd = vfs_secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
|
||||
fd = secure_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS,
|
||||
tmpdir != NULL);
|
||||
} else
|
||||
fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
|
||||
#if 0
|
||||
/* In most cases parent directories will already exist because their
|
||||
* information should have been previously transferred, but that may
|
||||
* not be the case with -R */
|
||||
if (fd == -1 && relative_paths && errno == ENOENT
|
||||
&& vfs_make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME, 0) == 0) {
|
||||
&& make_path(fnametmp, MKP_SKIP_SLASH | MKP_DROP_NAME) == 0) {
|
||||
/* Get back to name with XXXXXX in it. */
|
||||
get_tmpname(fnametmp, fname, False);
|
||||
fd = vfs_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
fd = do_mkstemp(fnametmp, (file->mode|added_perms) & INITACCESSPERMS);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -470,14 +476,14 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
if (preallocate_files && fd != -1 && total_size > 0 && (!inplace_sizing || total_size > size_r)) {
|
||||
/* Try to preallocate enough space for file's eventual length. Can
|
||||
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
|
||||
if ((preallocated_len = vfs_fallocate(fd, 0, total_size)) < 0)
|
||||
rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(fname));
|
||||
if ((preallocated_len = do_fallocate(fd, 0, total_size)) < 0)
|
||||
rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(fname));
|
||||
} else
|
||||
#endif
|
||||
if (inplace_sizing) {
|
||||
#ifdef HAVE_FTRUNCATE
|
||||
/* The most compatible way to create a sparse file is to start with no length. */
|
||||
if (sparse_files > 0 && whole_file && fd >= 0 && vfs_ftruncate(fd, 0) == 0)
|
||||
if (sparse_files > 0 && whole_file && fd >= 0 && do_ftruncate(fd, 0) == 0)
|
||||
preallocated_len = 0;
|
||||
else
|
||||
#endif
|
||||
@@ -520,7 +526,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
}
|
||||
}
|
||||
offset = sum.flength;
|
||||
if (fd != -1 && (j = vfs_lseek(fd, offset, SEEK_SET)) != offset) {
|
||||
if (fd != -1 && (j = do_lseek(fd, offset, SEEK_SET)) != offset) {
|
||||
rsyserr(FERROR_XFER, errno, "lseek of %s returned %s, not %s",
|
||||
full_fname(fname), big_num(j), big_num(offset));
|
||||
exit_cleanup(RERR_FILEIO);
|
||||
@@ -644,7 +650,7 @@ static int receive_data(int f_in, char *fname_r, int fd_r, OFF_T size_r,
|
||||
* preallocate_files: total_size could have been an overestimate.
|
||||
* Cut off any extra preallocated zeros from dest file. */
|
||||
if ((inplace_sizing || preallocated_len > offset) && fd != -1 && !IS_DEVICE(file->mode)) {
|
||||
if (vfs_ftruncate(fd, offset) < 0)
|
||||
if (do_ftruncate(fd, offset) < 0)
|
||||
rsyserr(FERROR_XFER, errno, "ftruncate failed on %s", full_fname(fname));
|
||||
}
|
||||
#endif
|
||||
@@ -697,9 +703,9 @@ static void handle_delayed_updates(char *local_name)
|
||||
* walk so a symlinked partial-dir can't move a file out of
|
||||
* an excluded subtree. */
|
||||
int rret;
|
||||
/* partialptr is the operator-supplied --partial-dir source (owner
|
||||
* walk); fname is the transfer destination (secure receiver resolve). */
|
||||
rret = vfs_rename_at(partialptr, fname, VFS_OPERATOR_PATH, 0);
|
||||
operator_path_resolve = 1;
|
||||
rret = do_rename_at(partialptr, fname);
|
||||
operator_path_resolve = 0;
|
||||
if (rret < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"rename failed for %s (from %s)",
|
||||
@@ -1062,12 +1068,12 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
&& fnamecmp && *fnamecmp != '/') {
|
||||
/* The relative partial path contains peer-derived directory
|
||||
* components. It is not an operator-trusted path as a whole. */
|
||||
fd1 = vfs_resolve_open(NULL, fnamecmp, O_RDONLY, 0);
|
||||
} else if (!basedir && (bdfd = vfs_cached_dirfd(fnamecmp, file)) >= 0) {
|
||||
fd1 = secure_relative_open(NULL, fnamecmp, O_RDONLY, 0);
|
||||
} else if (!basedir && (bdfd = held_dfd_for(fnamecmp, file)) >= 0) {
|
||||
const char *slash;
|
||||
assert(fnamecmp != NULL); /* set on every path above */
|
||||
slash = strrchr(fnamecmp, '/');
|
||||
fd1 = vfs_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
|
||||
fd1 = do_open_atfd(bdfd, slash ? slash + 1 : fnamecmp, O_RDONLY, 0);
|
||||
} else {
|
||||
/* An operator-supplied basis -- a --partial-dir, or an
|
||||
* alt-dest basedir (--copy-dest/--compare-dest/--link-dest) --
|
||||
@@ -1078,9 +1084,10 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
* and the operator's own uid0/euid symlinks. A daemon keeps its
|
||||
* stronger confinement branch in secure_basis_open(), so only
|
||||
* route the alt-dest basedir read through the walk off-daemon. */
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0,
|
||||
((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR) ? VFS_OPERATOR_PATH : 0);
|
||||
|
||||
if ((basedir && !am_daemon) || fnamecmp_type == FNAMECMP_PARTIAL_DIR)
|
||||
operator_path_resolve = 1;
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
}
|
||||
if (fnamecmp_type == FNAMECMP_PARTIAL_DIR && fd1 == -1) {
|
||||
@@ -1103,7 +1110,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
if (fnamecmp != fname) {
|
||||
fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_FNAME;
|
||||
fd1 = vfs_open_nofollow(fnamecmp, O_RDONLY);
|
||||
fd1 = do_open_nofollow(fnamecmp, O_RDONLY);
|
||||
}
|
||||
|
||||
if (fd1 == -1 && basis_dir[0]) {
|
||||
@@ -1111,8 +1118,10 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
basedir = basis_dir[0];
|
||||
fnamecmp = fname;
|
||||
fnamecmp_type = FNAMECMP_BASIS_DIR_LOW;
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0,
|
||||
!am_daemon ? VFS_OPERATOR_PATH : 0);
|
||||
if (!am_daemon)
|
||||
operator_path_resolve = 1;
|
||||
fd1 = secure_basis_open(basedir, fnamecmp, O_RDONLY, 0);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,7 +1142,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
if (fd1 == -1) {
|
||||
st.st_mode = 0;
|
||||
st.st_size = 0;
|
||||
} else if (vfs_fstat(fd1,&st) != 0) {
|
||||
} else if (do_fstat(fd1,&st) != 0) {
|
||||
rsyserr(FERROR_XFER, errno, "fstat %s failed",
|
||||
full_fname(fnamecmp));
|
||||
discard_receive_data(f_in, file);
|
||||
@@ -1185,18 +1194,18 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
/* We now check to see if we are writing the file "inplace" */
|
||||
if (inplace || one_inplace) {
|
||||
fnametmp = one_inplace ? partialptr : fname;
|
||||
/* For any non-chrooted receiver (vfs_relpath_active()),
|
||||
/* For any non-chrooted receiver (secure_relpath_active()),
|
||||
* use secure open to prevent symlink race attacks where an
|
||||
* attacker could switch a directory to a symlink between
|
||||
* path validation and file open. */
|
||||
/* one_inplace stages into the operator/peer --partial-dir path:
|
||||
* resolve it with the ownership walk (exclude-aware) so it can't be
|
||||
* redirected through a symlink into an excluded subtree. */
|
||||
if (vfs_relpath_active())
|
||||
if (secure_relpath_active())
|
||||
fd2 = secure_recv_open(fnametmp, O_WRONLY|O_CREAT, 0600,
|
||||
one_inplace);
|
||||
else
|
||||
fd2 = vfs_open(fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
fd2 = do_open(fnametmp, O_WRONLY|O_CREAT, 0600);
|
||||
#ifdef linux
|
||||
if (fd2 == -1 && errno == EACCES) {
|
||||
/* Maybe the error was due to protected_regular setting? */
|
||||
@@ -1204,7 +1213,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
fd2 = secure_recv_open(fnametmp, O_WRONLY, 0600,
|
||||
one_inplace);
|
||||
else
|
||||
fd2 = vfs_open(fnametmp, O_WRONLY, 0600);
|
||||
fd2 = do_open(fnametmp, O_WRONLY, 0600);
|
||||
}
|
||||
#endif
|
||||
if (fd2 == -1 && errno == EACCES) {
|
||||
@@ -1283,7 +1292,9 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
/* Unlink the consumed --partial-dir basis through the
|
||||
* exclude-aware ownership walk (a symlinked partial-dir
|
||||
* must not delete a file in an excluded subtree). */
|
||||
vfs_unlink(VFS_AT_FDCWD, partialptr, VFS_OPERATOR_PATH);
|
||||
operator_path_resolve = 1;
|
||||
do_unlink_at(partialptr);
|
||||
operator_path_resolve = 0;
|
||||
}
|
||||
handle_partial_dir(partialptr, PDIR_DELETE);
|
||||
}
|
||||
@@ -1293,7 +1304,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
"Unable to create partial-dir for %s -- discarding %s.\n",
|
||||
local_name ? local_name : f_name(file, NULL),
|
||||
recv_ok ? "completed file" : "partial file");
|
||||
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
|
||||
do_unlink_at(fnametmp);
|
||||
recv_ok = -1;
|
||||
} else if (!finish_transfer(partialptr, fnametmp, fnamecmp, NULL,
|
||||
file, recv_ok, !partial_dir))
|
||||
@@ -1304,7 +1315,7 @@ int recv_files(int f_in, int f_out, char *local_name)
|
||||
} else
|
||||
partialptr = NULL;
|
||||
} else if (!one_inplace)
|
||||
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
|
||||
do_unlink_at(fnametmp);
|
||||
|
||||
cleanup_disable();
|
||||
|
||||
|
||||
@@ -226,9 +226,20 @@ if [[ "$1" == --HELPER ]]; then
|
||||
rsync_ssl_helper "${@}"
|
||||
fi
|
||||
|
||||
if [[ "$1" == --type=* ]]; then
|
||||
export RSYNC_SSL_TYPE="${1/--type=/}"
|
||||
shift
|
||||
fi
|
||||
args=()
|
||||
dash_dash_seen=false
|
||||
for arg in "$@"; do
|
||||
if [[ $dash_dash_seen == true ]]; then
|
||||
args+=("$arg")
|
||||
elif [[ "$arg" == "--" ]]; then
|
||||
args+=("$arg")
|
||||
dash_dash_seen=true
|
||||
elif [[ "$arg" == --type=* ]]; then
|
||||
export RSYNC_SSL_TYPE="${arg#--type=}"
|
||||
else
|
||||
args+=("$arg")
|
||||
fi
|
||||
done
|
||||
set -- "${args[@]}"
|
||||
|
||||
rsync_ssl_run "${@}"
|
||||
+5
-2
@@ -25,12 +25,15 @@ rsync version to be at least 3.2.0.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
If the **first** arg is a `--type=SSL_TYPE` option, the script will only use
|
||||
If an arg is a `--type=SSL_TYPE` option, the script will only use
|
||||
that particular program to open an ssl connection instead of trying to find an
|
||||
openssl or stunnel executable via a simple heuristic (assuming that the
|
||||
`RSYNC_SSL_TYPE` environment variable is not set as well -- see below). This
|
||||
option must specify one of `openssl` or `stunnel`. The equal sign is
|
||||
required for this particular option.
|
||||
required for this particular option. The wrapper's option scan stops at a
|
||||
`--` argument: the `--` and everything after it are passed through to rsync
|
||||
unchanged, so a `--type=...` token after a `--` is not consumed by the
|
||||
wrapper.
|
||||
|
||||
All the other options are passed through to the rsync command, so consult the
|
||||
**rsync**(1) manpage for more information on how it works.
|
||||
|
||||
+10
-1
@@ -1,3 +1,12 @@
|
||||
REGARDING OPENSSL AND XXHASH
|
||||
|
||||
In addition, as a special exception, the copyright holders give
|
||||
permission to dynamically link rsync with the OpenSSL and xxhash
|
||||
libraries when those libraries are being distributed in compliance
|
||||
with their license terms, and to distribute a dynamically linked
|
||||
combination of rsync and these libraries. This is also considered
|
||||
to be covered under the GPL's System Libraries exception.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
@@ -671,4 +680,4 @@ into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>>.
|
||||
<<a href="https://www.gnu.org/licenses/why-not-lgpl.html">https://www.gnu.org/licenses/why-not-lgpl.html</a>>.
|
||||
+47
-16
@@ -300,7 +300,8 @@ entirely. See the [SYMBOLIC LINKS](#) section for how these interact.
|
||||
Separately, the directory and file paths that *you* supply on the command line --
|
||||
[`--backup-dir`](#opt), [`--temp-dir`](#opt), [`--partial-dir`](#opt), the
|
||||
[`--link-dest`](#opt)/[`--compare-dest`](#opt)/[`--copy-dest`](#opt) basis directories,
|
||||
[`--log-file`](#opt), [`--files-from`](#opt)/`--include-from`/`--exclude-from`,
|
||||
[`--log-file`](#opt), [`--password-file`](#opt),
|
||||
[`--files-from`](#opt)/`--include-from`/`--exclude-from`,
|
||||
[`--filter`](#opt) merge files, [`--write-batch`](#opt)/[`--read-batch`](#opt),
|
||||
and the destination itself -- are resolved so that a symlink component is followed
|
||||
only when it is owned by you or by root; an attacker-planted symlink along one of
|
||||
@@ -412,6 +413,20 @@ when scripting rsync.
|
||||
WARNING: On some systems, environment variables are visible to all users. On
|
||||
those systems using [`--password-file`](#opt) is recommended.
|
||||
|
||||
If rsync was built with IDN support (look for "IDN" in `rsync --version`), the
|
||||
daemon host may contain non-ASCII characters: those labels are converted to
|
||||
their IDNA A-label ("Punycode") form before the name is looked up. The name is
|
||||
read using your locale's character encoding, so be sure your locale is set
|
||||
correctly. A name typed with combining characters is normalized on the way, so
|
||||
it is looked up the same as its precomposed spelling. Only the non-ASCII
|
||||
labels change, so an address literal, a name you punycoded yourself, and a name
|
||||
that is not a valid IDN are all looked up just as you typed them.
|
||||
|
||||
This applies to the host of a daemon connection only. The host of a plain
|
||||
remote-shell transfer (the single-colon syntax) is passed to your remote-shell
|
||||
program unchanged, since that name may well be an ssh_config "Host" alias
|
||||
rather than a DNS name.
|
||||
|
||||
You may establish the connection via a web proxy by setting the environment
|
||||
variable [`RSYNC_PROXY`](#) to a hostname:port pair pointing to your web proxy.
|
||||
Note that your web proxy's configuration must support proxy connections to port
|
||||
@@ -1741,20 +1756,20 @@ sign) if you want the local shell to expand it.
|
||||
0. `--devices`
|
||||
|
||||
This option causes rsync to transfer character and block device files to
|
||||
the remote system to recreate these devices. If the receiving rsync is not
|
||||
being run as the super-user, rsync silently skips creating the device files
|
||||
(see also the [`--super`](#opt) and [`--fake-super`](#opt) options).
|
||||
the remote system to recreate these devices. This option has no effect if
|
||||
the receiving rsync is not run as the super-user and neither
|
||||
[`--super`](#opt) nor [`--fake-super`](#opt) is in effect.
|
||||
|
||||
By default, rsync generates a "non-regular file" warning for each device
|
||||
file encountered when this option is not set. You can silence the warning
|
||||
by specifying [`--info=nonreg0`](#opt).
|
||||
When a device file is not created, rsync generates the usual "non-regular
|
||||
file" warning. You can silence the warning by specifying
|
||||
[`--info=nonreg0`](#opt).
|
||||
|
||||
0. `--specials`
|
||||
|
||||
This option causes rsync to transfer special files, such as named sockets
|
||||
and fifos. If the receiving rsync is not being run as the super-user,
|
||||
rsync silently skips creating the special files (see also the
|
||||
[`--super`](#opt) and [`--fake-super`](#opt) options).
|
||||
and fifos. Creating these files does not normally require super-user
|
||||
privileges. Use [`--drop-D`](#opt) to make the receiving rsync refuse to
|
||||
create them regardless of what the transfer requested.
|
||||
|
||||
By default, rsync generates a "non-regular file" warning for each special
|
||||
file encountered when this option is not set. You can silence the warning
|
||||
@@ -2325,12 +2340,22 @@ sign) if you want the local shell to expand it.
|
||||
See the [`--max-size`](#opt) option for a description of how SIZE can be
|
||||
specified. The default suffix if none is given is bytes.
|
||||
|
||||
Beginning in 3.2.7, a value of 0 was an easy way to specify SIZE_MAX (the
|
||||
largest limit possible). However, beginning with 3.5.0, a value of 0 is
|
||||
rejected as invalid for security reasons (a 0-byte cap could be used to
|
||||
disable the allocation limit, which could lead to a denial-of-service via
|
||||
memory exhaustion). Use an explicit very large value if you want a very
|
||||
high limit.
|
||||
A value of 0 is an easy way to say "the largest limit this build supports".
|
||||
It resolves to the same ceiling an explicit SIZE is checked against, so it
|
||||
is never a higher limit than one you could have typed out yourself.
|
||||
|
||||
Because the option is passed to the remote rsync as you wrote it, each side
|
||||
resolves a 0 against its own maximum. That makes 0 the only spelling that
|
||||
is correct for both ends of a transfer between hosts of different word
|
||||
sizes: a literal value large enough to be useful on a 64-bit client is
|
||||
rejected as too large by a 32-bit daemon.
|
||||
|
||||
A value of 0 was accepted beginning in 3.2.3 and rejected in 3.5.0; the
|
||||
release after 3.5.0 accepts it again.
|
||||
|
||||
A daemon administrator who does not want clients to change the configured
|
||||
allocation ceiling can set `refuse options = max-alloc` in the module's
|
||||
`rsyncd.conf`. This refuses every client-supplied value, including 0.
|
||||
|
||||
You can set a default value using the environment variable
|
||||
[`RSYNC_MAX_ALLOC`](#) using the same SIZE values as supported by this
|
||||
@@ -2610,6 +2635,12 @@ sign) if you want the local shell to expand it.
|
||||
options are parsed (e.g. [`-a`](#opt) works the same before or after
|
||||
`--files-from`, as does `--no-R` and all other options).
|
||||
|
||||
Listing individual files with `--files-from` does not make unlisted
|
||||
siblings eligible for deletion. The [`--delete`](#opt) option only removes
|
||||
entries from directories whose complete contents are being synchronised,
|
||||
so list the directory itself and enable recursion if that deletion scope is
|
||||
intended.
|
||||
|
||||
The filenames that are read from the FILE are all relative to the source
|
||||
directory: any leading slash is removed, and ".." components are resolved away so
|
||||
an entry cannot rise above the source directory -- e.g. "../foo" is taken as "foo"
|
||||
|
||||
@@ -37,6 +37,7 @@ extern int omit_dir_times;
|
||||
extern int omit_link_times;
|
||||
extern int am_root;
|
||||
extern int am_server;
|
||||
extern int operator_path_resolve;
|
||||
extern int am_daemon;
|
||||
extern int am_sender;
|
||||
extern int am_receiver;
|
||||
@@ -513,13 +514,6 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
int op_leaf_fd = -1; /* O_NOFOLLOW fd pinning a cross-tree operator leaf */
|
||||
int op_pin = 0; /* drive chmod/chown off op_leaf_fd for a cross-tree leaf */
|
||||
int op_refuse = 0; /* pin open hit the symlink-race signal: refuse, don't redirect */
|
||||
/* The ownership walk for the path-based chmod/chown fallbacks below. op_pin
|
||||
* covers a reg/dir/fifo leaf with a pinned fd, but a symlink or device leaf
|
||||
* never enters it, and a non-root operator can fail the pin open with a plain
|
||||
* EACCES and fall through -- both must still resolve the operator path via the
|
||||
* walk rather than a bare lchown()/chmod(). (vfs_chmod's operator branch skips
|
||||
* S_ISLNK itself, so a symlink-as-object keeps the lchmod/setattrlist path.) */
|
||||
int op_vfs = (flags & ATTRS_OPERATOR_PATH) ? VFS_OPERATOR_PATH : 0;
|
||||
#if defined SUPPORT_XATTRS || defined SUPPORT_ACLS
|
||||
int held_fd = -1; /* held O_NOFOLLOW fd for fd-based xattr/ACL ops, or -1 */
|
||||
int xattr_refuse = 0; /* no confined fd for a slashed path: skip path-based xattr/ACL */
|
||||
@@ -532,7 +526,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
/* Stat through the entry's held dir fd (like gen_entry_stat) so we
|
||||
* don't re-walk the full path here; link_stat_at folds in no
|
||||
* fake-super xattr, so only when am_root >= 0. */
|
||||
if (am_root >= 0 && (sdfd = vfs_cached_dirfd(fname, file)) >= 0) {
|
||||
if (am_root >= 0 && (sdfd = held_dfd_for(fname, file)) >= 0) {
|
||||
const char *sl = strrchr(fname, '/');
|
||||
sret = link_stat_at(sdfd, sl ? sl + 1 : fname, &sx2.st, 0);
|
||||
} else
|
||||
@@ -552,7 +546,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
* issue single-component *at() calls against it instead of re-resolving
|
||||
* the full path each time. -1 => fall back to the full-path wrappers
|
||||
* (cross-tree path such as --temp-dir/--backup-dir, or gated off). */
|
||||
dfd = vfs_cached_dirfd(fname, file);
|
||||
dfd = held_dfd_for(fname, file);
|
||||
if (dfd >= 0) {
|
||||
const char *slash = strrchr(fname, '/');
|
||||
leaf = slash ? slash + 1 : fname;
|
||||
@@ -575,26 +569,30 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
))
|
||||
held_fd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
|
||||
|
||||
/* If the held-fd pin above missed (no cached dir fd, or a raced leaf) but we
|
||||
* are a confined receiver on a non-operator path, re-pin the leaf through the
|
||||
* secure resolver so the xattr/ACL ops below drive fsetxattr off a confined fd
|
||||
* -- NOT a raw path-based lsetxattr, which re-resolves the parent and lets a
|
||||
* flipped dest/sub symlink land the xattr OUTSIDE the tree (copy-xattrs-
|
||||
* symlink-race). A confined receiver normally always has the cached pin; a
|
||||
* miss here is a raced parent/leaf. If the secure re-pin also fails (the
|
||||
* parent/leaf is a symlink), held_fd stays -1 and xattr_refuse below skips the
|
||||
* path-based ops rather than redirecting them. (chmod/chown/times stay safe
|
||||
* via their secure path wrappers; operator paths use op_pin/op_refuse.) */
|
||||
if (held_fd < 0 && !(flags & ATTRS_OPERATOR_PATH) && vfs_relpath_active()
|
||||
/* If the held-fd pin above missed (no cached dir fd -- a path deeper than the
|
||||
* dirfd cache, or a raced leaf) but we are a confined receiver on a
|
||||
* non-operator path, re-pin the leaf through the secure resolver so the
|
||||
* xattr/ACL ops below drive fsetxattr off a confined fd -- NOT a raw path-based
|
||||
* lsetxattr, which re-resolves the parent and lets a flipped dest/sub symlink
|
||||
* land the xattr OUTSIDE the tree (copy-xattrs-symlink-race). If the secure
|
||||
* re-pin also fails (a genuinely raced parent/leaf symlink), held_fd stays -1
|
||||
* and xattr_refuse skips the path-based ops rather than redirecting them.
|
||||
* (chmod/chown/times stay safe via their secure path wrappers; operator paths
|
||||
* use op_pin/op_refuse below.) */
|
||||
if (held_fd < 0 && !operator_path_resolve && secure_relpath_active()
|
||||
&& (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))
|
||||
&& (preserve_xattrs || am_root < 0
|
||||
# ifdef SUPPORT_ACLS
|
||||
|| (preserve_acls && am_root >= 0)
|
||||
# endif
|
||||
)) {
|
||||
held_fd = vfs_resolve_open(NULL, fname,
|
||||
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC
|
||||
| (S_ISDIR(sxp->st.st_mode) ? O_DIRECTORY : 0), 0);
|
||||
int odir = 0;
|
||||
# ifdef O_DIRECTORY
|
||||
if (S_ISDIR(sxp->st.st_mode))
|
||||
odir = O_DIRECTORY;
|
||||
# endif
|
||||
held_fd = secure_relative_open(NULL, fname,
|
||||
O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY | O_CLOEXEC | odir, 0);
|
||||
if (held_fd < 0 && strchr(fname, '/'))
|
||||
xattr_refuse = 1;
|
||||
}
|
||||
@@ -609,16 +607,16 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
* operator owner-walk resolver and drive fchmod/fchown off that fd. A raced
|
||||
* symlink leaf makes the open fail, leaving op_leaf_fd == -1: the metadata op
|
||||
* is then refused, never redirected. --insecure-links opts back out (the
|
||||
* resolver in vfs_open_at honours it), and a genuine symlink leaf (a symlink
|
||||
* resolver in do_open_at honours it), and a genuine symlink leaf (a symlink
|
||||
* backup) keeps the existing l-variant path. */
|
||||
/* Gate on the INTENDED type (new_mode), not the on-disk type (sxp->st): the
|
||||
* attacker controls the latter via the flip, and a dir component that has
|
||||
* just been flipped to a symlink must still take the pinned path so the
|
||||
* O_NOFOLLOW open refuses it -- otherwise the lchown would launder it. */
|
||||
op_pin = (flags & ATTRS_OPERATOR_PATH) && dfd < 0 && !vfs_symlink_optout_allowed()
|
||||
op_pin = operator_path_resolve && dfd < 0 && !symlink_optout_allowed()
|
||||
&& (S_ISREG(new_mode) || S_ISDIR(new_mode) || S_ISFIFO(new_mode));
|
||||
if (op_pin) {
|
||||
op_leaf_fd = vfs_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0, VFS_OPERATOR_PATH);
|
||||
op_leaf_fd = do_open_at(fname, O_RDONLY | O_NONBLOCK | O_NOCTTY | O_CLOEXEC, 0);
|
||||
/* When running as root (the uid-0 trust-laundering case) an O_RDONLY open
|
||||
* of a real owned reg/dir/fifo leaf never fails for permission reasons, so
|
||||
* ANY failure here means the leaf is being raced (a symlink refused by
|
||||
@@ -649,29 +647,6 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
if (daemon_chmod_modes && !S_ISLNK(new_mode))
|
||||
new_mode = tweak_mode(new_mode, daemon_chmod_modes);
|
||||
|
||||
#if (defined SUPPORT_XATTRS || defined SUPPORT_ACLS) && defined STRICT_CONFINEMENT
|
||||
/* Enforce the pin/re-pin invariant: for a confined, pinnable, non-operator
|
||||
* leaf with metadata work pending, the held-fd pin/re-pin above must have
|
||||
* produced a confined fd (held_fd >= 0) or set xattr_refuse. Reaching here
|
||||
* with neither means the xattr/ACL setters would take their raw path-based
|
||||
* branch (the copy-xattrs fallback class) -- abort so the suite catches the
|
||||
* regression. The clause mirrors the pin condition (excluding no-metadata-
|
||||
* work, symlink/operator/opt-out, etc.); it is intentionally a touch broader
|
||||
* than "the next setter definitely path-writes" (set_xattr is skipped when
|
||||
* fnamecmp == NULL; a native ACL may still take a dirfd+leaf route), but a
|
||||
* confined slashed path only reaches here once the invariant is already
|
||||
* broken, so it cannot false-abort a legitimate transfer. */
|
||||
if (held_fd < 0 && !op_refuse && !xattr_refuse && !(flags & ATTRS_OPERATOR_PATH)
|
||||
&& (S_ISREG(sxp->st.st_mode) || S_ISDIR(sxp->st.st_mode) || S_ISFIFO(sxp->st.st_mode))
|
||||
&& (preserve_xattrs || am_root < 0
|
||||
# ifdef SUPPORT_ACLS
|
||||
|| (preserve_acls && am_root >= 0)
|
||||
# endif
|
||||
)
|
||||
&& vfs_must_be_confined(fname, 0))
|
||||
vfs_strict_confine_fail(fname, "xattr/ACL set");
|
||||
#endif
|
||||
|
||||
#ifdef SUPPORT_ACLS
|
||||
if (preserve_acls && !S_ISLNK(file->mode) && !ACL_READY(*sxp) && !op_refuse && !xattr_refuse)
|
||||
get_acl_fdat(held_fd, dfd, leaf, fname, sxp);
|
||||
@@ -701,10 +676,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
if (am_root >= 0) {
|
||||
uid_t uid = change_uid ? (uid_t)F_OWNER(file) : sxp->st.st_uid;
|
||||
gid_t gid = change_gid ? (gid_t)F_GROUP(file) : sxp->st.st_gid;
|
||||
if ((op_leaf_fd >= 0 ? vfs_fchown(op_leaf_fd, uid, gid)
|
||||
if ((op_leaf_fd >= 0 ? do_fchown(op_leaf_fd, uid, gid)
|
||||
: op_refuse ? (errno = ELOOP, -1)
|
||||
: dfd >= 0 ? vfs_lchown(dfd, leaf, uid, gid, 0)
|
||||
: vfs_lchown(VFS_AT_FDCWD, fname, uid, gid, op_vfs)) != 0) {
|
||||
: dfd >= 0 ? do_lchown_atfd(dfd, leaf, uid, gid)
|
||||
: do_lchown_at(fname, uid, gid)) != 0) {
|
||||
/* We shouldn't have attempted to change uid
|
||||
* or gid unless have the privilege. */
|
||||
rsyserr(FERROR_XFER, errno, "%s %s failed",
|
||||
@@ -776,13 +751,13 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
if (crtimes_ndx && !(flags & ATTRS_SKIP_CRTIME)) {
|
||||
time_t file_crtime = F_CRTIME(file);
|
||||
if (sxp->crtime == 0)
|
||||
sxp->crtime = vfs_get_create_time(fname, &sxp->st);
|
||||
sxp->crtime = get_create_time(fname, &sxp->st);
|
||||
if (!same_time(sxp->crtime, 0L, file_crtime, 0L)) {
|
||||
if (
|
||||
#ifdef HAVE_GETATTRLIST
|
||||
vfs_setattrlist_crtime(fname, file_crtime) == 0
|
||||
do_setattrlist_crtime(fname, file_crtime) == 0
|
||||
#elif defined __CYGWIN__
|
||||
vfs_SetFileTime(fname, file_crtime) == 0
|
||||
do_SetFileTime(fname, file_crtime) == 0
|
||||
#else
|
||||
#error Unknown crtimes implementation
|
||||
#endif
|
||||
@@ -795,7 +770,7 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
int ret;
|
||||
#ifdef HAVE_FUTIMENS
|
||||
if (op_leaf_fd >= 0)
|
||||
ret = vfs_futimens(op_leaf_fd, &sx2.st);
|
||||
ret = do_futimens(op_leaf_fd, &sx2.st);
|
||||
else
|
||||
#endif
|
||||
if (op_refuse)
|
||||
@@ -831,10 +806,10 @@ int set_file_attrs(const char *fname, struct file_struct *file, stat_x *sxp,
|
||||
#ifdef HAVE_CHMOD
|
||||
if (!BITS_EQUAL(sxp->st.st_mode, new_mode, CHMOD_BITS)) {
|
||||
int ret = am_root < 0 ? 0
|
||||
: op_leaf_fd >= 0 ? vfs_fchmod(op_leaf_fd, new_mode)
|
||||
: op_leaf_fd >= 0 ? do_fchmod(op_leaf_fd, new_mode)
|
||||
: op_refuse ? (errno = ELOOP, -1)
|
||||
: dfd >= 0 && !S_ISLNK(new_mode) ? vfs_chmod(dfd, leaf, new_mode, 0)
|
||||
: vfs_chmod(VFS_AT_FDCWD, fname, new_mode, op_vfs);
|
||||
: dfd >= 0 && !S_ISLNK(new_mode) ? do_chmod_atfd(dfd, leaf, new_mode)
|
||||
: do_chmod_at(fname, new_mode);
|
||||
if (ret < 0) {
|
||||
rsyserr(FERROR_XFER, errno,
|
||||
"failed to set permissions on %s",
|
||||
@@ -932,8 +907,10 @@ int finish_transfer(const char *fname, const char *fnametmp,
|
||||
* dirfd, so resolve its metadata through the ownership walk (op_pin); a
|
||||
* flipped temp-dir parent then can't redirect the chmod/chown/times/etc.
|
||||
* (in-tree temps keep their held dirfd, so op_pin stays off there). */
|
||||
operator_path_resolve = 1;
|
||||
set_file_attrs(fnametmp, file, NULL, fnamecmp,
|
||||
ATTRS_OPERATOR_PATH | (ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME));
|
||||
ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);
|
||||
operator_path_resolve = 0;
|
||||
|
||||
/* move tmp file over real file */
|
||||
if (DEBUG_GTE(RECV, 1))
|
||||
@@ -945,7 +922,7 @@ int finish_transfer(const char *fname, const char *fnametmp,
|
||||
full_fname(fnametmp), fname);
|
||||
if (!partialptr || (ret == -2 && temp_copy_name)
|
||||
|| robust_rename(fnametmp, partialptr, NULL, file->mode, file) < 0)
|
||||
vfs_unlink(VFS_AT_FDCWD, fnametmp, 0);
|
||||
do_unlink_at(fnametmp);
|
||||
return 0;
|
||||
}
|
||||
if (ret == 0) {
|
||||
@@ -961,9 +938,7 @@ int finish_transfer(const char *fname, const char *fnametmp,
|
||||
ok_to_set_time ? ATTRS_ACCURATE_TIME : ATTRS_SKIP_MTIME | ATTRS_SKIP_ATIME | ATTRS_SKIP_CRTIME);
|
||||
|
||||
if (temp_copy_name) {
|
||||
/* temp_copy_name and fname both live in the dest tree here; flag 0 lets
|
||||
* vfs_twopath_side confine each side (absolute=owner-walk, relative=secure). */
|
||||
if (vfs_rename_at(fnametmp, fname, 0, 0) < 0) {
|
||||
if (do_rename_at(fnametmp, fname) < 0) {
|
||||
rsyserr(FERROR_XFER, errno, "rename %s -> \"%s\"",
|
||||
full_fname(fnametmp), fname);
|
||||
return 0;
|
||||
|
||||
@@ -223,7 +223,6 @@
|
||||
#define ATTRS_SKIP_MTIME (1<<1)
|
||||
#define ATTRS_ACCURATE_TIME (1<<2)
|
||||
#define ATTRS_SKIP_ATIME (1<<3)
|
||||
#define ATTRS_OPERATOR_PATH (1<<4) /* fname is a cross-tree operator path: pin its leaf (op_pin) */
|
||||
#define ATTRS_SKIP_CRTIME (1<<5)
|
||||
|
||||
#define MSG_FLUSH 2
|
||||
@@ -1238,13 +1237,12 @@ struct name_num_obj {
|
||||
|
||||
#ifndef __cplusplus
|
||||
#include "proto.h"
|
||||
#include "vfs/vfs.h"
|
||||
#endif
|
||||
|
||||
#ifndef SUPPORT_XATTRS
|
||||
#define x_stat(fn,fst,xst,vfsflags) vfs_stat(VFS_AT_FDCWD, fn, fst, vfsflags)
|
||||
#define x_lstat(fn,fst,xst,vfsflags) vfs_lstat(VFS_AT_FDCWD, fn, fst, vfsflags)
|
||||
#define x_fstat(fd,fst,xst) vfs_fstat(fd,fst)
|
||||
#define x_stat(fn,fst,xst) do_stat(fn,fst)
|
||||
#define x_lstat(fn,fst,xst) do_lstat(fn,fst)
|
||||
#define x_fstat(fd,fst,xst) do_fstat(fd,fst)
|
||||
#endif
|
||||
|
||||
/* We have replacement versions of these if they're missing. */
|
||||
@@ -1283,7 +1281,7 @@ extern int errno;
|
||||
#ifdef HAVE_READLINK
|
||||
#define SUPPORT_LINKS 1
|
||||
#if !defined NO_SYMLINK_XATTRS && !defined NO_SYMLINK_USER_XATTRS
|
||||
#define vfs_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz)
|
||||
#define do_readlink(path, buf, bufsiz) readlink(path, buf, bufsiz)
|
||||
#endif
|
||||
#endif
|
||||
#ifdef HAVE_LINK
|
||||
|
||||
@@ -853,6 +853,20 @@ in the values of parameters. See that section for details.
|
||||
- an '@' followed by a netgroup name, which will match if the reverse DNS
|
||||
of the connecting IP is in the specified netgroup.
|
||||
|
||||
If rsync was built with IDN support (look for "IDN" in `rsync --version`),
|
||||
a hostname pattern may contain non-ASCII characters: this file is read as
|
||||
UTF-8, and each non-ASCII label is converted to its IDNA A-label
|
||||
("Punycode") form before matching, since the name the daemon has for a
|
||||
client always reaches it from DNS as ASCII. A pattern written with
|
||||
combining characters is normalized on the way, so it matches the same as
|
||||
its precomposed spelling. Only those labels change, so an address, a
|
||||
mask, an already-punycoded name, and the wildcard characters are all
|
||||
matched exactly as written. A pattern that cannot be converted that way
|
||||
is matched as it stands, and thus matches nothing. That includes a
|
||||
pattern whose conversion would have introduced a character it was not
|
||||
written with, such as the U+FF0A FULLWIDTH ASTERISK that the IDNA mapping
|
||||
turns into a "*".
|
||||
|
||||
Note IPv6 link-local addresses can have a scope in the address
|
||||
specification:
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
|
||||
|
||||
/* "insecure links = yes" / --insecure-links: restore the 3.2.7 plain re-stat
|
||||
* by declining the confined parent (errno=0 makes the caller use do_lstat). */
|
||||
if (vfs_symlink_optout_allowed()) {
|
||||
if (symlink_optout_allowed()) {
|
||||
errno = 0;
|
||||
return -1;
|
||||
}
|
||||
@@ -127,21 +127,20 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
|
||||
#endif
|
||||
while (*rel == '/')
|
||||
rel++;
|
||||
return vfs_resolve_open("/", rel,
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
return secure_relative_dirfd("/", rel);
|
||||
}
|
||||
/* vfs_path_dirfd returns a cache-OWNED fd; the caller closes
|
||||
/* held_dir_path_fd returns a cache-OWNED fd; the caller closes
|
||||
* what we return, so hand back an owned dup and leave the cache's
|
||||
* dirfd intact. An uncacheable (very deep) dir declines with
|
||||
* errno 0 -- fall back to the full confined walk (an owned fd,
|
||||
* matching the sender's content open) so deep paths stay confined
|
||||
* too; a real error propagates. */
|
||||
dfd = vfs_path_dirfd(NULL, dir);
|
||||
dfd = held_dir_path_fd(NULL, dir);
|
||||
if (dfd >= 0)
|
||||
return dup(dfd);
|
||||
if (errno != 0)
|
||||
return -1;
|
||||
return vfs_resolve_open(NULL, dir, O_RDONLY | O_DIRECTORY, 0);
|
||||
return secure_relative_dirfd(NULL, dir);
|
||||
}
|
||||
errno = 0; /* top-level file: no parent component to confine */
|
||||
return -1;
|
||||
@@ -176,9 +175,9 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
|
||||
}
|
||||
memcpy(dir, relp, dlen);
|
||||
dir[dlen] = '\0';
|
||||
dfd = vfs_resolve_open(module_dir, dir, O_RDONLY | O_DIRECTORY, 0);
|
||||
dfd = secure_relative_dirfd(module_dir, dir);
|
||||
} else
|
||||
dfd = vfs_resolve_open(module_dir, "", O_RDONLY | O_DIRECTORY, 0);
|
||||
dfd = secure_relative_dirfd(module_dir, "");
|
||||
|
||||
/* The leaf is the same last component either way; take it from the caller's
|
||||
* persistent fname buffer, not the local secure_path. */
|
||||
@@ -195,20 +194,20 @@ static int secure_sender_parent_fd(struct file_struct *file, const char *fname,
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Go through the VFS wrapper rather than a raw unlinkat(): it carries the
|
||||
* dry_run no-op and the read-only/list-only refusal that the plain unlink path
|
||||
* applies, plus the missing-AT_FDCWD fallback. */
|
||||
/* Go through the do_*() wrapper rather than a raw unlinkat(): it carries the
|
||||
* dry_run no-op and the read-only/list-only refusal that do_unlink() applies
|
||||
* on the non-fd path, plus the missing-AT_FDCWD fallback. */
|
||||
static int secure_remove_source_file(int dfd, const char *bname)
|
||||
{
|
||||
return vfs_unlink(dfd, bname, 0);
|
||||
return do_unlink_atfd(dfd, bname, 0);
|
||||
}
|
||||
|
||||
/* Open `relpath` (relative to `anchor`: NULL=cwd, else an absolute trusted root)
|
||||
* with `flags`, opening the leaf via the shared held ancestor-dirfd stack
|
||||
* (vfs_path_dirfd) so a directory is walked once, not once per file. The leaf
|
||||
* semantics are identical to vfs_resolve_open() -- it always O_NOFOLLOWs a
|
||||
* (held_dir_path_fd) so a directory is walked once, not once per file. The leaf
|
||||
* semantics are identical to secure_relative_open() -- it always O_NOFOLLOWs a
|
||||
* file leaf and folds in O_NOATIME, both preserved here. An uncacheable path
|
||||
* (vfs_path_dirfd returns -1) falls back to the full confined walk. */
|
||||
* (held_dir_path_fd returns -1) falls back to the full confined walk. */
|
||||
static int sender_open_confined(const char *anchor, const char *relpath, int flags)
|
||||
{
|
||||
#ifdef AT_FDCWD
|
||||
@@ -237,14 +236,14 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla
|
||||
if (open_noatime)
|
||||
flags |= O_NOATIME;
|
||||
#endif
|
||||
dfd = vfs_path_dirfd(anchor, dir);
|
||||
dfd = held_dir_path_fd(anchor, dir);
|
||||
if (dfd < 0)
|
||||
return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0);
|
||||
return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);
|
||||
return openat(dfd, bname, flags | O_NOFOLLOW, 0);
|
||||
#else
|
||||
/* No *at() support: vfs_resolve_open is a plain open() here (no walk,
|
||||
/* No *at() support: secure_relative_open is a plain open() here (no walk,
|
||||
* so nothing to amortise); use it directly to keep the anchor semantics. */
|
||||
return vfs_resolve_open(anchor, relpath, flags | O_NOFOLLOW, 0);
|
||||
return secure_relative_open(anchor, relpath, flags | O_NOFOLLOW, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -253,7 +252,7 @@ static int sender_open_confined(const char *anchor, const char *relpath, int fla
|
||||
* O_NOFOLLOW that sender_open_confined() applies refuses an in-tree symlink the
|
||||
* operator explicitly asked to follow, so resolve the link ourselves: read it,
|
||||
* refuse an absolute or "../"-escaping target (a module escape), and re-resolve
|
||||
* the relative target through vfs_resolve_open() -- which follows in-tree
|
||||
* the relative target through secure_relative_open() -- which follows in-tree
|
||||
* links and rejects an escape above the anchor -- looping for a symlink chain.
|
||||
* The final open is still O_NOFOLLOW, so a raced flip at the resolved leaf is
|
||||
* refused. This keeps the module boundary while honouring --copy-links. */
|
||||
@@ -292,14 +291,12 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat
|
||||
* only this branch would hand it to strcmp(). */
|
||||
if (am_daemon && module_dirfd >= 0 && module_dir && anchor
|
||||
&& strcmp(anchor, module_dir) == 0)
|
||||
pdfd = vfs_resolve_open_at_beneath(module_dirfd, dir,
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
pdfd = secure_relative_dirfd_at_beneath(module_dirfd, dir);
|
||||
else
|
||||
pdfd = vfs_resolve_open(anchor, dir,
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
pdfd = secure_relative_dirfd(anchor, dir);
|
||||
if (pdfd < 0)
|
||||
return -1;
|
||||
n = vfs_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1);
|
||||
n = do_readlink_atfd(pdfd, bname, tgt, sizeof tgt - 1);
|
||||
e = errno;
|
||||
if (n < 0) {
|
||||
/* EINVAL: not a symlink -> the resolved target file. Open it
|
||||
@@ -327,7 +324,7 @@ static int sender_open_copylinks_confined(const char *anchor, const char *relpat
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
#else
|
||||
return vfs_resolve_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0);
|
||||
return secure_relative_open(anchor, relpath, O_RDONLY | O_NOFOLLOW, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -424,8 +421,8 @@ void successful_send(int ndx)
|
||||
}
|
||||
|
||||
if (dfd >= 0
|
||||
? (copy_links ? vfs_stat(dfd, bname, &st, 0) : vfs_lstat(dfd, bname, &st, 0)) < 0
|
||||
: (copy_links ? vfs_stat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK) : vfs_lstat(VFS_AT_FDCWD, fname, &st, VFS_ALLOW_SYMLINK)) < 0) {
|
||||
? (copy_links ? do_stat_atfd(dfd, bname, &st) : do_lstat_atfd(dfd, bname, &st)) < 0
|
||||
: (copy_links ? do_stat(fname, &st) : do_lstat(fname, &st)) < 0) {
|
||||
failed_op = "re-lstat";
|
||||
goto failed;
|
||||
}
|
||||
@@ -450,7 +447,7 @@ void successful_send(int ndx)
|
||||
return;
|
||||
}
|
||||
|
||||
if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : vfs_unlink(VFS_AT_FDCWD, fname, VFS_ALLOW_SYMLINK) < 0) {
|
||||
if (dfd >= 0 ? secure_remove_source_file(dfd, bname) < 0 : do_unlink(fname) < 0) {
|
||||
failed_op = "remove";
|
||||
failed:
|
||||
if (errno == ENOENT)
|
||||
@@ -647,13 +644,13 @@ void send_files(int f_in, int f_out)
|
||||
exit_cleanup(RERR_PROTOCOL);
|
||||
}
|
||||
|
||||
if (vfs_symlink_optout_allowed()) {
|
||||
if (symlink_optout_allowed()) {
|
||||
/* Module opted out of symlink confinement ("insecure links =
|
||||
* yes", admin-only) -- or a non-daemon --insecure-links: legacy
|
||||
* unconfined open, restoring the pre-hardening content read
|
||||
* (re-opening the escape for that module; documented). */
|
||||
fd = vfs_open_checklinks(fname);
|
||||
} else if (vfs_relpath_active()) {
|
||||
fd = do_open_checklinks(fname);
|
||||
} else if (secure_relpath_active()) {
|
||||
/* Open from module root to prevent TOCTOU race where
|
||||
* change_pathname's chdir follows a directory symlink.
|
||||
* Reconstruct the full path relative to module_dir
|
||||
@@ -671,7 +668,7 @@ void send_files(int f_in, int f_out)
|
||||
}
|
||||
/* A module with `path = /` makes F_PATHNAME absolute, so the
|
||||
* joined path starts with '/'; strip leading slashes to a
|
||||
* module-relative path that vfs_resolve_open accepts (#897). */
|
||||
* module-relative path that secure_relative_open accepts (#897). */
|
||||
relp = secure_path;
|
||||
while (*relp == '/')
|
||||
relp++;
|
||||
@@ -704,7 +701,7 @@ void send_files(int f_in, int f_out)
|
||||
} else
|
||||
fd = sender_open_confined(NULL, fname, O_RDONLY);
|
||||
} else {
|
||||
fd = vfs_open_checklinks(fname);
|
||||
fd = do_open_checklinks(fname);
|
||||
}
|
||||
if (fd == -1) {
|
||||
if (errno == ENOENT) {
|
||||
@@ -725,7 +722,7 @@ void send_files(int f_in, int f_out)
|
||||
}
|
||||
|
||||
/* map the local file */
|
||||
if (vfs_fstat(fd, &st) != 0) {
|
||||
if (do_fstat(fd, &st) != 0) {
|
||||
io_error |= IOERR_GENERAL;
|
||||
rsyserr(FERROR_XFER, errno, "fstat failed");
|
||||
free_sums(s);
|
||||
|
||||
@@ -343,6 +343,14 @@ int open_socket_out(char *host, int port, const char *bind_addr, int af_hint)
|
||||
int proxied = 0;
|
||||
char buffer[1024];
|
||||
char *proxy_user = NULL, *proxy_pass = NULL;
|
||||
#ifdef SUPPORT_IDN
|
||||
char idn_host[1024];
|
||||
|
||||
/* The resolver only speaks ASCII, so an IDN host goes out as A-labels.
|
||||
* An all-ASCII host is passed along untouched. */
|
||||
if (idn_to_ascii(host, 1, idn_host, sizeof idn_host))
|
||||
host = idn_host;
|
||||
#endif
|
||||
|
||||
/* if we have a RSYNC_PROXY env variable then redirect our
|
||||
* connection via a web proxy at the given address. */
|
||||
|
||||
@@ -57,7 +57,14 @@ def main():
|
||||
for fn in files:
|
||||
if args.prefix:
|
||||
fn = args.prefix + fn
|
||||
mtime = os.lstat(fn).st_mtime
|
||||
try:
|
||||
mtime = os.lstat(fn).st_mtime
|
||||
except FileNotFoundError:
|
||||
# Tracked in git but absent from the tree we are stamping.
|
||||
# export-ignore in .gitattributes keeps rsync-web/ and the
|
||||
# old_versions/ binaries out of "git archive", so they have
|
||||
# no file here to give a commit time to.
|
||||
continue
|
||||
if args.list:
|
||||
print_line(fn, mtime, commit_time)
|
||||
elif mtime != commit_time:
|
||||
|
||||
@@ -9,3 +9,4 @@ sudo apt install -y libxxhash-dev
|
||||
sudo apt install -y libzstd-dev
|
||||
sudo apt install -y liblz4-dev
|
||||
sudo apt install -y libssl-dev
|
||||
sudo apt install -y libidn2-dev
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Test harness for vfs_chmod(). Confirms the symlink-TOCTOU
|
||||
* Test harness for do_chmod_at(). Confirms the symlink-TOCTOU
|
||||
* primitive used by CVE-2026-29518 (and its incomplete-fix follow-up
|
||||
* for chmod) is closed by vfs_chmod(): a parent directory component
|
||||
* for chmod) is closed by do_chmod_at(): a parent directory component
|
||||
* being a symlink that escapes the receiver's confinement must be
|
||||
* rejected, while a parent symlink that resolves *within* the tree
|
||||
* must still work (so legitimate dir-symlinks are not regressed).
|
||||
@@ -31,7 +31,7 @@ short info_levels[COUNT_INFO], debug_levels[COUNT_DEBUG];
|
||||
static int errs = 0;
|
||||
|
||||
|
||||
/* Does vfs_chmod()'s leaf handling refuse to follow a symlink at the final
|
||||
/* Does do_chmod_at()'s leaf handling refuse to follow a symlink at the final
|
||||
* component? Yes wherever AT_SYMLINK_NOFOLLOW exists; otherwise the wrapper
|
||||
* falls back to a following fchmodat() (documented limitation). Mirrors the
|
||||
* #ifdef ladder in do_fchmodat_nofollow. */
|
||||
@@ -85,9 +85,9 @@ int main(int argc, char **argv)
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Simulate the daemon-without-chroot deployment that vfs_chmod()
|
||||
/* Simulate the daemon-without-chroot deployment that do_chmod_at()
|
||||
* defends. With am_daemon=0 or am_chrooted=1 the wrapper falls
|
||||
* through to plain vfs_chmod() and the symlink-race test would be
|
||||
* through to plain do_chmod() and the symlink-race test would be
|
||||
* meaningless. */
|
||||
am_daemon = 1;
|
||||
am_chrooted = 0;
|
||||
@@ -112,26 +112,26 @@ int main(int argc, char **argv)
|
||||
* Solaris, older Cygwin, HPE NonStop, pre-5.6 Linux) -- which now follows
|
||||
* an in-tree directory symlink whose target is relative and ".."-free.
|
||||
* Escapes are still rejected on both paths (Scenario B). */
|
||||
int rc = vfs_chmod(VFS_AT_FDCWD, "inside_link/sentinel", 0640, 0);
|
||||
int rc = do_chmod_at("inside_link/sentinel", 0640);
|
||||
check("A: legit dir-symlink within tree (followed)",
|
||||
rc, 1, "realdir/sentinel", 0640);
|
||||
|
||||
/* Scenario B: parent symlink escapes the tree -- chmod must be
|
||||
* rejected and the outside file's mode must be unchanged. */
|
||||
rc = vfs_chmod(VFS_AT_FDCWD, "escape_link/sentinel", 0666, 0);
|
||||
rc = do_chmod_at("escape_link/sentinel", 0666);
|
||||
check("B: parent symlink escapes tree (the attack)",
|
||||
rc, 0, "../trap/sentinel", 0600);
|
||||
|
||||
/* Scenario C: plain relative path with no symlink components,
|
||||
* regression check that the safe wrapper doesn't break the
|
||||
* normal case. */
|
||||
rc = vfs_chmod(VFS_AT_FDCWD, "realdir/sentinel", 0644, 0);
|
||||
rc = do_chmod_at("realdir/sentinel", 0644);
|
||||
check("C: plain relative path (regression check)",
|
||||
rc, 1, "realdir/sentinel", 0644);
|
||||
|
||||
/* Scenario D: top-level file, no parent directory component.
|
||||
* Falls back to vfs_chmod(); should succeed. */
|
||||
rc = vfs_chmod(VFS_AT_FDCWD, "topfile", 0640, 0);
|
||||
* Falls back to do_chmod(); should succeed. */
|
||||
rc = do_chmod_at("topfile", 0640);
|
||||
check("D: top-level file, no parent component",
|
||||
rc, 1, "topfile", 0640);
|
||||
|
||||
@@ -141,12 +141,12 @@ int main(int argc, char **argv)
|
||||
* (refused on Linux, lchmod-the-symlink on *BSD/macOS), so assert only that
|
||||
* the outside target's mode is unchanged. */
|
||||
if (leaf_chmod_nofollow_supported()) {
|
||||
rc = vfs_chmod(VFS_AT_FDCWD, "realdir/leaflink", 0666, 0);
|
||||
rc = do_chmod_at("realdir/leaflink", 0666);
|
||||
check("E: leaf component is an escaping symlink (must not be followed)",
|
||||
rc, -1, "../trap/sentinel", 0600);
|
||||
} else {
|
||||
fprintf(stderr, "INFO: leaf-nofollow chmod unsupported here; "
|
||||
"vfs_chmod follows a leaf symlink (documented limitation), "
|
||||
"do_chmod_at follows a leaf symlink (documented limitation), "
|
||||
"skipping scenario E\n");
|
||||
}
|
||||
|
||||
|
||||
+7
-63
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Test harness for vfs_rename_at(): a mixed top-level/slashed rename must still
|
||||
* resolve the slashed side's parent under vfs_resolve_open() rather than
|
||||
* Test harness for do_rename_at(): a mixed top-level/slashed rename must still
|
||||
* resolve the slashed side's parent under secure_relative_open() rather than
|
||||
* fall back to plain rename(). Not linked into rsync. GPL version 2.
|
||||
*/
|
||||
|
||||
@@ -23,21 +23,21 @@ static int errs = 0;
|
||||
|
||||
#ifdef AT_FDCWD
|
||||
/* The 3.4.3 bug: if either side has no slash the whole op fell back to plain
|
||||
* rename(), leaving the slashed side's parent outside vfs_resolve_open(). */
|
||||
* rename(), leaving the slashed side's parent outside secure_relative_open(). */
|
||||
static int vulnerable_mixed_rename_at(const char *old_path, const char *new_path)
|
||||
{
|
||||
const char *old_slash, *new_slash;
|
||||
|
||||
if (!old_path || !*old_path || *old_path == '/'
|
||||
|| !new_path || !*new_path || *new_path == '/')
|
||||
return vfs_rename(old_path, new_path);
|
||||
return do_rename(old_path, new_path);
|
||||
|
||||
old_slash = strrchr(old_path, '/');
|
||||
new_slash = strrchr(new_path, '/');
|
||||
if (!old_slash || !new_slash)
|
||||
return vfs_rename(old_path, new_path);
|
||||
return do_rename(old_path, new_path);
|
||||
|
||||
return vfs_rename_at(old_path, new_path, 0, 0);
|
||||
return do_rename_at(old_path, new_path);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -64,7 +64,7 @@ static void check_rename(const char *label, const char *old_path,
|
||||
int saved_errno;
|
||||
|
||||
errno = 0;
|
||||
rc = vfs_rename_at(old_path, new_path, 0, 0);
|
||||
rc = do_rename_at(old_path, new_path);
|
||||
saved_errno = errno;
|
||||
got_ok = rc == 0;
|
||||
|
||||
@@ -79,30 +79,6 @@ static void check_rename(const char *label, const char *old_path,
|
||||
label, old_path, new_path, expect_ok ? "succeeded" : "rejected");
|
||||
}
|
||||
|
||||
/* Like check_rename() but with explicit per-operand policy flags, for the
|
||||
* two-path per-side split (PR #30). */
|
||||
static void check_rename_flags(const char *label, const char *old_path,
|
||||
const char *new_path, int old_flags, int new_flags,
|
||||
int expect_ok)
|
||||
{
|
||||
int rc, got_ok, saved_errno;
|
||||
|
||||
errno = 0;
|
||||
rc = vfs_rename_at(old_path, new_path, old_flags, new_flags);
|
||||
saved_errno = errno;
|
||||
got_ok = rc == 0;
|
||||
|
||||
if (got_ok != expect_ok) {
|
||||
fprintf(stderr, "FAIL [%s]: rename %s -> %s (of=%d nf=%d) rc=%d errno=%d (%s), expected %s\n",
|
||||
label, old_path, new_path, old_flags, new_flags, rc, saved_errno,
|
||||
strerror(saved_errno), expect_ok ? "success" : "rejection");
|
||||
errs++;
|
||||
return;
|
||||
}
|
||||
fprintf(stderr, "OK [%s]: rename %s -> %s %s\n",
|
||||
label, old_path, new_path, expect_ok ? "succeeded" : "rejected");
|
||||
}
|
||||
|
||||
static void check_vulnerable_rename(const char *label, const char *old_path,
|
||||
const char *new_path)
|
||||
{
|
||||
@@ -223,38 +199,6 @@ int main(int argc, char **argv)
|
||||
check_exists("F source consumed", "top-old", 0);
|
||||
check_exists("F destination created", "top-new", 1);
|
||||
|
||||
/* Per-operand policy split (PR #30): the NEW side's policy must be independent
|
||||
* of the OLD side's. oplink is a caller-owned (uid0/euid) symlink that ESCAPES
|
||||
* the tree (-> ../trap): the ownership walk (operator policy) follows the
|
||||
* operator's own symlink, but the secure receiver resolve (transfer, flag 0)
|
||||
* refuses it because it leaves the cwd anchor. PS-refuse and PS-follow rename
|
||||
* to the SAME oplink/ path with the SAME operator old side, differing ONLY in
|
||||
* the new-side flag -- so the per-side flag, not a whole-call flag, decides.
|
||||
* The old single-flag API applied VFS_OPERATOR_PATH to both operands, so it
|
||||
* would have followed oplink in PS-refuse too (PS-refuse is RED on that code).
|
||||
* Run non-daemon (the regime where an operator basis/backup path legitimately
|
||||
* carries the operator's own uid0/euid symlinks). */
|
||||
{
|
||||
struct stat lst;
|
||||
if (lstat("oplink", &lst) == 0 && S_ISLNK(lst.st_mode)) {
|
||||
int save_daemon = am_daemon;
|
||||
am_daemon = 0;
|
||||
|
||||
check_rename_flags("PS-refuse: new side transfer refuses an escaping owned symlink",
|
||||
"realdir/perside-src2", "oplink/tr-out",
|
||||
VFS_OPERATOR_PATH, 0, 0);
|
||||
check_exists("PS-refuse out-of-tree dest absent", "../trap/tr-out", 0);
|
||||
check_exists("PS-refuse source preserved", "realdir/perside-src2", 1);
|
||||
|
||||
check_rename_flags("PS-follow: new side operator follows the same owned symlink",
|
||||
"realdir/perside-src3", "oplink/op-out",
|
||||
VFS_OPERATOR_PATH, VFS_OPERATOR_PATH, 1);
|
||||
check_exists("PS-follow out-of-tree dest created (operator's own symlink)", "../trap/op-out", 1);
|
||||
|
||||
am_daemon = save_daemon;
|
||||
}
|
||||
}
|
||||
|
||||
if (errs)
|
||||
fprintf(stderr, "%d failure(s)\n", errs);
|
||||
return errs ? 1 : 0;
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Test harness for vfs_resolve_open()'s front-door input
|
||||
* Test harness for secure_relative_open()'s front-door input
|
||||
* validation. Codex audit Finding 5 noted that the existing check
|
||||
*
|
||||
* if (strncmp(relpath, "../", 3) == 0 || strstr(relpath, "/../"))
|
||||
@@ -14,7 +14,7 @@
|
||||
* pre-5.6 Linux does not, so the validation must happen at the
|
||||
* front door.
|
||||
*
|
||||
* This helper invokes vfs_resolve_open() with each suspect
|
||||
* This helper invokes secure_relative_open() with each suspect
|
||||
* input and checks both the failure (rc < 0) and the errno
|
||||
* (EINVAL means "rejected at the front door"). Pre-fix, the kernel
|
||||
* may reject with a different errno (EXDEV from RESOLVE_BENEATH);
|
||||
@@ -47,7 +47,7 @@ static void check_relpath(const char *relpath)
|
||||
int saved_errno;
|
||||
|
||||
errno = 0;
|
||||
fd = vfs_resolve_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0);
|
||||
fd = secure_relative_open(NULL, relpath, O_RDONLY | O_DIRECTORY, 0);
|
||||
saved_errno = errno;
|
||||
|
||||
if (fd >= 0) {
|
||||
@@ -76,7 +76,7 @@ static void check_basedir(const char *basedir)
|
||||
int saved_errno;
|
||||
|
||||
errno = 0;
|
||||
fd = vfs_resolve_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0);
|
||||
fd = secure_relative_open(basedir, "ok", O_RDONLY | O_DIRECTORY, 0);
|
||||
saved_errno = errno;
|
||||
|
||||
if (fd >= 0) {
|
||||
@@ -111,7 +111,7 @@ static void check_beneath_dotdot(void)
|
||||
return;
|
||||
}
|
||||
|
||||
fd = vfs_resolve_open_at_beneath(anchor, "alias/../subdir",
|
||||
fd = secure_relative_open_at_beneath(anchor, "alias/../subdir",
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
if (fd < 0 || fstat(fd, &fst) < 0 || fst.st_dev != ast.st_dev
|
||||
|| fst.st_ino == ast.st_ino) {
|
||||
@@ -138,7 +138,7 @@ static void check_beneath_dotdot(void)
|
||||
for (ci = 0; ci < sizeof dotdot_cases / sizeof *dotdot_cases; ci++) {
|
||||
int dfd;
|
||||
errno = 0;
|
||||
dfd = vfs_resolve_open_at_beneath(anchor, "..",
|
||||
dfd = secure_relative_open_at_beneath(anchor, "..",
|
||||
dotdot_cases[ci].flags, 0);
|
||||
if (dfd >= 0) {
|
||||
STRUCT_STAT dst;
|
||||
@@ -159,7 +159,7 @@ static void check_beneath_dotdot(void)
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
fd = vfs_resolve_open_at_beneath(anchor, "../outside",
|
||||
fd = secure_relative_open_at_beneath(anchor, "../outside",
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
if (fd >= 0 || errno != ELOOP) {
|
||||
fprintf(stderr, "FAIL [beneath escape]: rc=%d errno=%d, expected -1/ELOOP\n",
|
||||
@@ -184,7 +184,7 @@ int main(int argc, char **argv)
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* vfs_resolve_open's daemon-only confinement protections only
|
||||
/* secure_relative_open's daemon-only confinement protections only
|
||||
* fire when am_daemon && !am_chrooted (the threat model is the
|
||||
* daemon-no-chroot deployment), but the front-door input
|
||||
* validation runs unconditionally. We set am_daemon anyway so the
|
||||
@@ -196,7 +196,7 @@ int main(int argc, char **argv)
|
||||
symlink("subdir", "alias");
|
||||
|
||||
/* Each of these relpaths must be rejected with EINVAL at the
|
||||
* vfs_resolve_open() front door. ".." is the actual one-level
|
||||
* secure_relative_open() front door. ".." is the actual one-level
|
||||
* escape; the others ("subdir/..", "subdir/../subdir") resolve
|
||||
* back to the start dir on systems that allow them, but we still
|
||||
* reject them as defence-in-depth: a path containing a ".." token
|
||||
|
||||
@@ -42,14 +42,14 @@ size_t max_alloc = (size_t)-1; /* test helpers are not memory-constrained;
|
||||
* 0 here makes every my_alloc()/my_strdup() in
|
||||
* util2.c trip the "exceeded --max-alloc=0"
|
||||
* check, which any helper exercising the
|
||||
* per-component fallback of vfs_resolve_open()
|
||||
* per-component fallback of secure_relative_open()
|
||||
* hits at its first my_strdup() call. */
|
||||
char *partial_dir;
|
||||
char *module_dir;
|
||||
int module_dirfd = -1;
|
||||
char *confine_root;
|
||||
unsigned int confine_rootlen = 0;
|
||||
/* vfs.curr_dir[]/vfs.curr_dir_len (read by vfs_resolve_open) are defined in
|
||||
/* curr_dir[]/curr_dir_len (read by secure_relative_open) are defined in
|
||||
* syscall.c, which every helper links -- no stub needed here. */
|
||||
filter_rule_list daemon_filter_list;
|
||||
|
||||
|
||||
+22
-22
@@ -1,8 +1,8 @@
|
||||
/*
|
||||
* Test harness for the fake-super branches of vfs_symlink_at()/vfs_mknod_at().
|
||||
* Test harness for the fake-super branches of do_symlink_at()/do_mknod_at().
|
||||
* Fake-super stores a symlink/device as a placeholder file, so the create
|
||||
* resolves the final component; the no-slash branch used to fall back to
|
||||
* vfs_symlink()/vfs_mknod(), whose plain open() followed a planted basename
|
||||
* do_symlink()/do_mknod(), whose plain open() followed a planted basename
|
||||
* symlink and escaped the module. Checks the fixed wrappers refuse it;
|
||||
* --poc shows the old fallback escaping. Not linked into rsync. GPL version 2.
|
||||
*/
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* The symlink placeholder (and thus this escape) exists only where symlink
|
||||
* xattrs are unavailable -- the same guard vfs_symlink() uses. Elsewhere
|
||||
* xattrs are unavailable -- the same guard do_symlink() uses. Elsewhere
|
||||
* symlink() fails EEXIST on a planted link, so only the device path applies. */
|
||||
#if defined SUPPORT_LINKS && (defined NO_SYMLINK_XATTRS || defined NO_SYMLINK_USER_XATTRS)
|
||||
#define TEST_SYMLINK_PLACEHOLDER 1
|
||||
@@ -89,8 +89,8 @@ int main(int argc, char **argv)
|
||||
const char *moddir;
|
||||
|
||||
# if !defined(HAVE_MKNODAT) && !defined(TEST_SYMLINK_PLACEHOLDER)
|
||||
/* Nothing left to assert: the vfs_mknod() checks need mknodat(), and
|
||||
* the vfs_symlink() ones are not compiled here. Skip rather than
|
||||
/* Nothing left to assert: the do_mknod_at() checks need mknodat(), and
|
||||
* the do_symlink_at() ones are not compiled here. Skip rather than
|
||||
* pass vacuously. */
|
||||
(void)argc; (void)argv;
|
||||
fprintf(stderr, "SKIP: no mknodat() and no symlink placeholders -- "
|
||||
@@ -118,39 +118,39 @@ int main(int argc, char **argv)
|
||||
am_root = -1; /* fake-super: symlinks/devices stored as files */
|
||||
|
||||
if (poc) {
|
||||
/* Pre-fix fallback: a no-slash path went to vfs_symlink()/vfs_mknod(),
|
||||
/* Pre-fix fallback: a no-slash path went to do_symlink()/do_mknod(),
|
||||
* which open() the basename without O_NOFOLLOW. */
|
||||
#ifdef TEST_SYMLINK_PLACEHOLDER
|
||||
vfs_symlink("VULN_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", VFS_ALLOW_SYMLINK);
|
||||
check_clobbered("poc vfs_symlink bare", "../outside/secret_sym",
|
||||
do_symlink("VULN_SYM_PAYLOAD", "sympath");
|
||||
check_clobbered("poc do_symlink bare", "../outside/secret_sym",
|
||||
"VULN_SYM_PAYLOAD");
|
||||
#endif
|
||||
vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, VFS_ALLOW_SYMLINK);
|
||||
check_clobbered("poc vfs_mknod bare", "../outside/secret_nod", "");
|
||||
do_mknod("nodpath", S_IFCHR | 0600, 0);
|
||||
check_clobbered("poc do_mknod bare", "../outside/secret_nod", "");
|
||||
return errs ? 1 : 0;
|
||||
}
|
||||
|
||||
/* Fixed wrappers: a bare-path basename symlink must not be followed;
|
||||
* the victim outside the module stays untouched. */
|
||||
#ifdef TEST_SYMLINK_PLACEHOLDER
|
||||
vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sympath", 0);
|
||||
check_preserved("vfs_symlink bare", "../outside/secret_sym", "VICTIM_SYM");
|
||||
do_symlink_at("FIXED_SYM_PAYLOAD", "sympath");
|
||||
check_preserved("do_symlink_at bare", "../outside/secret_sym", "VICTIM_SYM");
|
||||
|
||||
/* Slashed path for parity (already protected before the fix). */
|
||||
vfs_symlink("FIXED_SYM_PAYLOAD", VFS_AT_FDCWD, "sub/sympath2", 0);
|
||||
check_preserved("vfs_symlink slashed", "../outside/secret_sym2", "VICTIM_SYM2");
|
||||
do_symlink_at("FIXED_SYM_PAYLOAD", "sub/sympath2");
|
||||
check_preserved("do_symlink_at slashed", "../outside/secret_sym2", "VICTIM_SYM2");
|
||||
#endif
|
||||
|
||||
# ifdef HAVE_MKNODAT
|
||||
/* Without mknodat() the secure vfs_mknod() IS the plain mknod(): the
|
||||
* confinement is compiled out by design (SECURITY.md), so these would
|
||||
* assert a property the build deliberately does not have. The
|
||||
* vfs_symlink() checks above do not depend on it and still run. */
|
||||
vfs_mknod(VFS_AT_FDCWD, "nodpath", S_IFCHR | 0600, 0, 0);
|
||||
check_preserved("vfs_mknod bare", "../outside/secret_nod", "VICTIM_NOD");
|
||||
/* Without mknodat() do_mknod_at() IS do_mknod(): the confinement is
|
||||
* compiled out by design (SECURITY.md), so these would assert a
|
||||
* property the build deliberately does not have. The do_symlink_at()
|
||||
* checks above do not depend on it and still run. */
|
||||
do_mknod_at("nodpath", S_IFCHR | 0600, 0);
|
||||
check_preserved("do_mknod_at bare", "../outside/secret_nod", "VICTIM_NOD");
|
||||
|
||||
vfs_mknod(VFS_AT_FDCWD, "sub/nodpath2", S_IFCHR | 0600, 0, 0);
|
||||
check_preserved("vfs_mknod slashed", "../outside/secret_nod2", "VICTIM_NOD2");
|
||||
do_mknod_at("sub/nodpath2", S_IFCHR | 0600, 0);
|
||||
check_preserved("do_mknod_at slashed", "../outside/secret_nod2", "VICTIM_NOD2");
|
||||
# endif
|
||||
|
||||
if (errs)
|
||||
|
||||
@@ -16,30 +16,16 @@
|
||||
# *symlink* components. The fix sanitizes the wire xname itself (for basis
|
||||
# types only, leaving the hard-link "=> target" xname alone).
|
||||
#
|
||||
# Test: build an instrumented daemon-sender (env-gated sender.c edit that, when
|
||||
# Test: build an instrumented rsync (env-gated sender.c edit that, when
|
||||
# RSYNC_MAL_XNAME is set, injects ITEM_XNAME_FOLLOWS|ITEM_BASIS_TYPE_FOLLOWS +
|
||||
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer),
|
||||
# run it via RSYNC_CONNECT_PROG with the production rsync as the receiver pulling
|
||||
# with --link-dest, and observe where the receiver opens the basis.
|
||||
#
|
||||
# Two FIFOs, each with a helper blocked in open(O_WRONLY) that drops a flag when
|
||||
# some reader opens it, tell RED from GREEN without hanging the receiver (it
|
||||
# reads EOF and finishes):
|
||||
# * ESCAPE base/secret reached only by an unsanitized "../secret"
|
||||
# * DECOY linkdest/secret where the SANITIZED "secret" lands
|
||||
# Injected xname is "../secret":
|
||||
# - vulnerable receiver opens ESCAPE -> escape flag -> FAIL (traversal)
|
||||
# - fixed receiver sanitizes to "secret", opens DECOY -> decoy flag -> PASS
|
||||
# (the decoy flag also proves the crafted xname actually crossed the wire,
|
||||
# so a stale/failed injection build can't false-PASS as "confined")
|
||||
# - neither flag -> the injection never took effect -> FAIL (vacuous)
|
||||
# fnamecmp_type=FNAMECMP_FUZZY+1 (== basis_dir[0]) + xname onto each transfer).
|
||||
# An env-gated receiver.c edit records the exact basedir and relpath passed to
|
||||
# secure_basis_open(). This observes the security decision directly without
|
||||
# relying on timing-sensitive FIFO rendezvous behaviour across operating systems.
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, build_patched_rsync, forced_protocol, makepath, rmtree,
|
||||
@@ -53,11 +39,7 @@ from rsyncfns import (
|
||||
_proto = forced_protocol()
|
||||
if _proto is not None and _proto < 29:
|
||||
test_skipped("basis-xname-traversal: xname/item flags need protocol >= 29")
|
||||
if not hasattr(os, 'mkfifo'):
|
||||
test_skipped("basis-xname-traversal: os.mkfifo unavailable on this platform")
|
||||
|
||||
|
||||
# -- Build the instrumented sender (shared helper: Cygwin skip, CCACHE_DISABLE,
|
||||
# -- Build the instrumented peer (shared helper: Cygwin skip, CCACHE_DISABLE,
|
||||
# forced rebuild of the patched unit) -------------------------------------
|
||||
PATCH_OLD = ("\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
|
||||
"\t\twrite_sum_head(f_xfer, s);")
|
||||
@@ -68,14 +50,32 @@ PATCH_NEW = ("\t\tif (getenv(\"RSYNC_MAL_XNAME\")) { /* basis-xname-traversal Po
|
||||
"\t\t}\n"
|
||||
"\t\twrite_ndx_and_attrs(f_out, ndx, iflags, fname, file, fnamecmp_type, xname, xlen);\n"
|
||||
"\t\twrite_sum_head(f_xfer, s);")
|
||||
mal_rsync = build_patched_rsync('mal-xname-rsync', [('sender.c', PATCH_OLD, PATCH_NEW)])
|
||||
TRACE_OLD = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
|
||||
"{\n"
|
||||
"\textern int am_daemon, am_chrooted;")
|
||||
TRACE_NEW = ("static int secure_basis_open(const char *basedir, const char *relpath, int flags, mode_t mode)\n"
|
||||
"{\n"
|
||||
"\tconst char *trace_path = getenv(\"RSYNC_BASIS_TRACE\");\n"
|
||||
"\tif (trace_path) {\n"
|
||||
"\t\tFILE *trace = fopen(trace_path, \"a\");\n"
|
||||
"\t\tif (trace) {\n"
|
||||
"\t\t\tfprintf(trace, \"%s\\t%s\\n\", basedir ? basedir : \"\", relpath);\n"
|
||||
"\t\t\tfclose(trace);\n"
|
||||
"\t\t}\n"
|
||||
"\t}\n"
|
||||
"\textern int am_daemon, am_chrooted;")
|
||||
mal_rsync = build_patched_rsync(
|
||||
'mal-xname-rsync',
|
||||
[('sender.c', PATCH_OLD, PATCH_NEW),
|
||||
('receiver.c', TRACE_OLD, TRACE_NEW)],
|
||||
)
|
||||
|
||||
|
||||
# -- Workspace ----------------------------------------------------------------
|
||||
# base/serversrc/file the file the instrumented daemon offers
|
||||
# base/linkdest/ the client's --link-dest (basis_dir[0])
|
||||
# base/linkdest/secret DECOY fifo -- where a sanitized "secret" resolves
|
||||
# base/secret ESCAPE fifo -- where an unsanitized "../secret" lands
|
||||
# base/linkdest/secret where a sanitized "secret" resolves
|
||||
# base/secret where an unsanitized "../secret" resolves
|
||||
# base/dest/ the client's destination
|
||||
base = SCRATCHDIR / 'xname-race'
|
||||
rmtree(base)
|
||||
@@ -84,133 +84,57 @@ linkdest = base / 'linkdest'
|
||||
dest = base / 'dest'
|
||||
escape = base / 'secret' # linkdest/../secret
|
||||
decoy = linkdest / 'secret' # linkdest/secret
|
||||
esc_flag = base / 'escape.flag'
|
||||
dec_flag = base / 'decoy.flag'
|
||||
trace_file = base / 'basis.trace'
|
||||
makepath(serversrc)
|
||||
makepath(linkdest)
|
||||
makepath(dest)
|
||||
(serversrc / 'file').write_text("from the server\n")
|
||||
escape.write_text("escaped basis\n")
|
||||
decoy.write_text("confined basis\n")
|
||||
|
||||
|
||||
# A helper that blocks in open(fifo, O_WRONLY) until some reader opens the FIFO,
|
||||
# then records the flag. Terminated below if no reader ever appears.
|
||||
WRITER = ("import os,sys\n"
|
||||
"open(sys.argv[3],'w').close()\n" # ready: about to block in open()
|
||||
"fd=os.open(sys.argv[1],os.O_WRONLY)\n"
|
||||
"open(sys.argv[2],'w').close()\n"
|
||||
"os.close(fd)\n")
|
||||
|
||||
|
||||
def spawn(fifo, flag):
|
||||
ready = Path(str(flag) + '.ready')
|
||||
if ready.exists():
|
||||
ready.unlink()
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, '-c', WRITER, str(fifo), str(flag), str(ready)])
|
||||
# Wait until the helper is actually at its blocking open(). Starting the
|
||||
# transfer before that lets the receiver come and go while nothing is
|
||||
# watching the FIFO, and the run reports a vacuous result -- which is what
|
||||
# made this test flaky on the slower fleet VMs.
|
||||
deadline = time.time() + 30
|
||||
while not ready.exists() and proc.poll() is None and time.time() < deadline:
|
||||
time.sleep(0.02)
|
||||
return proc
|
||||
|
||||
|
||||
def settle(w):
|
||||
"""Give a rendezvoused helper a bounded chance to record its flag; a still-
|
||||
blocked one just times out. (Closes the terminate-before-flag race.)"""
|
||||
try:
|
||||
w.wait(timeout=15)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
|
||||
def reap(w):
|
||||
if w.poll() is None:
|
||||
w.terminate()
|
||||
try:
|
||||
w.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
w.kill()
|
||||
w.wait()
|
||||
|
||||
|
||||
def attempt():
|
||||
"""One injection run. Returns the receiver's CompletedProcess.
|
||||
|
||||
Re-creates the FIFOs and flags each time so a retry starts clean.
|
||||
"""
|
||||
for f in (escape, decoy, esc_flag, dec_flag):
|
||||
if os.path.lexists(f):
|
||||
os.unlink(f)
|
||||
rmtree(dest)
|
||||
makepath(dest)
|
||||
os.mkfifo(escape)
|
||||
os.mkfifo(decoy)
|
||||
esc_w = spawn(escape, esc_flag)
|
||||
dec_w = spawn(decoy, dec_flag)
|
||||
proc = None
|
||||
try:
|
||||
conf = write_daemon_conf(
|
||||
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
|
||||
name='mal-xname-rsyncd.conf')
|
||||
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
|
||||
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
|
||||
proc = subprocess.run(
|
||||
rsync_argv('-a', f'--link-dest={linkdest}',
|
||||
'rsync://localhost/m/file', str(dest) + '/'),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
|
||||
settle(esc_w)
|
||||
settle(dec_w)
|
||||
finally:
|
||||
os.environ.pop('RSYNC_MAL_XNAME', None)
|
||||
os.environ.pop('RSYNC_CONNECT_PROG', None)
|
||||
reap(esc_w)
|
||||
reap(dec_w)
|
||||
for f in (escape, decoy):
|
||||
if os.path.lexists(f):
|
||||
os.unlink(f)
|
||||
return proc
|
||||
|
||||
|
||||
# A run where NEITHER fifo was opened proves nothing: the injection did not
|
||||
# take effect, so there was no traversal attempt to confine. That is a setup
|
||||
# failure, not a security signal, and on the slower fleet VMs it happens often
|
||||
# enough to make the test unusable -- so retry it. An ESCAPE is never retried:
|
||||
# the loop stops the moment the escape flag appears.
|
||||
attempts = 0
|
||||
for _try in range(6):
|
||||
attempts += 1
|
||||
proc = attempt()
|
||||
if esc_flag.is_file() or dec_flag.is_file():
|
||||
break
|
||||
conf = write_daemon_conf(
|
||||
[('m', {'path': str(serversrc), 'read only': 'yes', 'use chroot': 'no'})],
|
||||
name='mal-xname-rsyncd.conf')
|
||||
os.environ['RSYNC_CONNECT_PROG'] = f'{shlex.quote(str(mal_rsync))} --config={shlex.quote(str(conf))} --daemon'
|
||||
os.environ['RSYNC_MAL_XNAME'] = '../secret' # from basis_dir[0] == linkdest
|
||||
os.environ['RSYNC_BASIS_TRACE'] = str(trace_file)
|
||||
try:
|
||||
argv = rsync_argv('-a', f'--link-dest={linkdest}',
|
||||
'rsync://localhost/m/file', str(dest) + '/')
|
||||
argv[0] = str(mal_rsync)
|
||||
proc = subprocess.run(
|
||||
argv,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=120)
|
||||
finally:
|
||||
os.environ.pop('RSYNC_BASIS_TRACE', None)
|
||||
os.environ.pop('RSYNC_MAL_XNAME', None)
|
||||
os.environ.pop('RSYNC_CONNECT_PROG', None)
|
||||
|
||||
|
||||
# -- Oracle -------------------------------------------------------------------
|
||||
out_tail = '\n'.join((proc.stdout if proc else '').splitlines()[-20:])
|
||||
out_tail = '\n'.join(proc.stdout.splitlines()[-20:])
|
||||
trace = trace_file.read_text().splitlines() if trace_file.is_file() else []
|
||||
escaped = f'{linkdest}\t../secret'
|
||||
confined = f'{linkdest}\tsecret'
|
||||
|
||||
if esc_flag.is_file():
|
||||
if escaped in trace:
|
||||
test_fail(
|
||||
"malicious server traversed the client's filesystem via the alt-dest "
|
||||
f"xname: the receiver opened {escape} (one level above the --link-dest "
|
||||
f"xname: the receiver attempted {escape} (one level above the --link-dest "
|
||||
"dir) as the delta basis. A server-supplied xname of '../secret' was "
|
||||
"not sanitized on the client (sanitize_paths==0 off-daemon). Fix: "
|
||||
"sanitize a basis-type xname in read_ndx_and_attrs(). Receiver output "
|
||||
f"tail:\n{out_tail}")
|
||||
|
||||
# The decoy flag proves the crafted xname reached the receiver AND was confined
|
||||
# to the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
|
||||
# absence means the injection never took effect (e.g. a stale patched build),
|
||||
# so a clear escape flag alone would be a vacuous pass.
|
||||
if not dec_flag.is_file():
|
||||
# The trace proves the crafted xname reached the receiver and was confined to
|
||||
# the basedir (sanitized "../secret" -> "secret" -> linkdest/secret). Its
|
||||
# absence means the injection never took effect (e.g. a stale patched build).
|
||||
if confined not in trace:
|
||||
test_fail(
|
||||
"the crafted xname never reached the receiver's basis open (neither the "
|
||||
"escape nor the decoy FIFO was opened) -- the instrumented-sender "
|
||||
f"injection did not take effect, so this run is vacuous after "
|
||||
f"{attempts} attempt(s). This is a harness failure, NOT a traversal: "
|
||||
"an escape is reported separately and is never retried. Receiver rc="
|
||||
f"{proc.returncode if proc else 'n/a'}. Output tail:\n{out_tail}")
|
||||
"the crafted xname never reached the receiver's confined basis open; "
|
||||
"the instrumented injection did not take effect, so this run is "
|
||||
f"vacuous. Trace={trace!r}. Receiver rc={proc.returncode}. "
|
||||
f"Output tail:\n{out_tail}")
|
||||
|
||||
if proc.returncode != 0:
|
||||
test_fail(
|
||||
|
||||
@@ -209,7 +209,7 @@ proc = subprocess.run(
|
||||
# node fails first with ENXIO, which is an equally valid refusal at the open.
|
||||
# A clean (returncode 0) run would be the real failure -- rsync accepting the
|
||||
# device as a batch file.
|
||||
refused = ('is not a regular file' in proc.stderr
|
||||
refused = ('is neither a regular file nor a FIFO' in proc.stderr
|
||||
or ('open error' in proc.stderr and proc.returncode != 0))
|
||||
if not refused:
|
||||
test_fail(
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
# --contimeout is documented as a "daemon connection timeout". The guard that
|
||||
# rejects it only looked at whether connect_timeout was set, so a daemon
|
||||
# connection made through a remote shell (e.g. rsync-ssl, which runs rsync with
|
||||
# --rsh pointing at its helper) was rejected with the same syntax error as a
|
||||
# plain non-daemon remote-shell transfer. Only reject the option when there is
|
||||
# no daemon connection at all: a daemon reached via --rsh (daemon_connection ==
|
||||
# 1) is still a daemon connection, and rsync now also times that connection's
|
||||
# establishment phase with --contimeout the same way it times a socket connect.
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from rsyncfns import SCRATCHDIR, SRCDIR, rsync_argv, rmtree, test_fail
|
||||
|
||||
RERR_CONTIMEOUT = 35
|
||||
|
||||
base = SCRATCHDIR / 'contimeout-rsh'
|
||||
rmtree(base)
|
||||
base.mkdir(parents=True)
|
||||
|
||||
|
||||
def run(*args):
|
||||
return subprocess.run(rsync_argv(*args), capture_output=True, text=True)
|
||||
|
||||
|
||||
rejected_marker = "may only be used when connecting to an rsync daemon"
|
||||
|
||||
# A remote-shell command that fails immediately: the option guard runs before
|
||||
# rsync ever tries to exec it, so it only has to exist as a plausible --rsh
|
||||
# target to put rsync into its daemon-via-rsh connection mode.
|
||||
rsh_prog = str(SRCDIR / 'support' / 'lsh.sh')
|
||||
|
||||
# --- Daemon via --rsh must accept --contimeout (rsync-ssl's shape of call).
|
||||
proc = run('--contimeout=5', '--rsh=' + rsh_prog,
|
||||
'-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest'))
|
||||
if rejected_marker in (proc.stderr or ''):
|
||||
test_fail(f"--contimeout was rejected for a daemon-via-rsh connection:\n{proc.stderr}")
|
||||
|
||||
# --- A plain remote-shell (non-daemon) destination must still be rejected.
|
||||
proc = run('--contimeout=5', '-av', str(base / 'src'), 'localhost:' + str(base / 'dst'))
|
||||
if rejected_marker not in (proc.stderr or ''):
|
||||
test_fail("--contimeout was not rejected for a non-daemon remote shell:\n" +
|
||||
(proc.stderr or '') + (proc.stdout or ''))
|
||||
|
||||
# --- A daemon-via-rsh connection that never establishes must time out with the
|
||||
# daemon-connection timeout exit code. The fake helper sleeps instead of
|
||||
# connecting, so the only thing that can end the run is --contimeout firing.
|
||||
fake_rsh = base / 'hang-rsh'
|
||||
# The helper inherits rsync's stderr; redirect it so an orphaned "sleep" does
|
||||
# not keep the harness's captured-pipe open after rsync has already exited.
|
||||
fake_rsh.write_text("#!/bin/sh\nexec 2>/dev/null\nsleep 60\n")
|
||||
fake_rsh.chmod(0o755)
|
||||
|
||||
start = time.monotonic()
|
||||
proc = run('--contimeout=1', '--rsh=' + str(fake_rsh),
|
||||
'-av', 'rsync://127.0.0.1:9/mod/', str(base / 'dest2'))
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
if proc.returncode != RERR_CONTIMEOUT:
|
||||
test_fail(f"--contimeout did not abort the hung connection with exit "
|
||||
f"{RERR_CONTIMEOUT}; got {proc.returncode}:\n{proc.stderr}")
|
||||
if elapsed >= 15:
|
||||
test_fail(f"--contimeout=1 took {elapsed:.1f}s; the timeout did not bound "
|
||||
"the connection establishment phase")
|
||||
|
||||
print("contimeout-rsh: --contimeout is accepted for a daemon-via-rsh "
|
||||
"connection, rejected for a non-daemon remote shell, and times out a "
|
||||
"connection that never establishes")
|
||||
Executable
+169
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Daemon coverage: IDN hosts allow / hosts deny matching (access.c).
|
||||
|
||||
A daemon gets its peer's name from DNS as ASCII, so an rsyncd.conf entry
|
||||
written with non-ASCII characters is folded to its IDNA A-label form before
|
||||
being matched. The IDNA mapping also folds some non-ASCII characters onto
|
||||
ASCII ones (U+FF0A FULLWIDTH ASTERISK becomes '*'), so the checks below cover
|
||||
both directions: a Unicode name that has to match, and the tokens that must
|
||||
stay denied rather than turn into a wildcard or an address/mask. Punycode,
|
||||
mixed case and an unconvertible name are covered too.
|
||||
|
||||
The peer name isn't assumed: a throwaway daemon comes up first to log the name
|
||||
this host's resolver gives it, and the real config is written around that.
|
||||
"forward lookup" is off throughout, which pins the match on the reverse-DNS
|
||||
name and keeps a denied module from waiting out a resolver timeout on a name
|
||||
that deliberately doesn't exist.
|
||||
|
||||
Like daemon-access-ip_test.py this needs a real TCP peer (--use-tcp), and the
|
||||
config sets no global hosts allow so each module's own patterns decide.
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, SCRATCHDIR,
|
||||
claim_ports, make_tree, require_tcp, rmtree, rsync_argv, start_rsyncd,
|
||||
start_test_daemon, test_fail, test_skipped,
|
||||
)
|
||||
|
||||
PROBE_PORT = 12896
|
||||
DAEMON_PORT = 12898
|
||||
require_tcp("hosts allow/deny hostname matching needs a real TCP peer")
|
||||
|
||||
if '"IDN": true' not in subprocess.run(rsync_argv('-VV'), capture_output=True,
|
||||
text=True).stdout:
|
||||
test_skipped("rsync built without IDN support")
|
||||
|
||||
src = FROMDIR
|
||||
rmtree(src)
|
||||
make_tree(src, depth=2)
|
||||
|
||||
|
||||
def write_conf(path, modules, log, pidfile):
|
||||
lines = [
|
||||
'# autogenerated by daemon-access-idn_test.py',
|
||||
f'pid file = {pidfile}',
|
||||
'use chroot = no',
|
||||
'forward lookup = no',
|
||||
f'log file = {log}',
|
||||
'',
|
||||
]
|
||||
for mod, params in modules:
|
||||
lines.append(f'[{mod}]')
|
||||
lines.append(f'\tpath = {src}')
|
||||
lines.append('\tread only = yes')
|
||||
lines += [f'\t{k} = {v}' for k, v in params.items()]
|
||||
lines.append('')
|
||||
# rsyncd.conf is read as UTF-8 by the daemon whatever the test's locale is.
|
||||
path.write_text('\n'.join(lines) + '\n', encoding='utf-8')
|
||||
return path
|
||||
|
||||
|
||||
# --- find out what this host's resolver calls the loopback peer -------------
|
||||
# A throwaway daemon with one wide-open module: connect once, read the name it
|
||||
# logged for us, then shut it down before the real config goes up.
|
||||
probe_log = SCRATCHDIR / 'rsyncd-idn-probe.log'
|
||||
probe_conf = write_conf(SCRATCHDIR / 'access-idn-probe.conf', [('probe', {})],
|
||||
probe_log, SCRATCHDIR / 'rsyncd-idn-probe.pid')
|
||||
claim_ports(PROBE_PORT)
|
||||
probe = start_rsyncd(probe_conf, PROBE_PORT)
|
||||
try:
|
||||
subprocess.run(rsync_argv('-r', f'rsync://localhost:{PROBE_PORT}/probe/'),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
finally:
|
||||
probe.terminate()
|
||||
probe.wait(timeout=5)
|
||||
|
||||
m = re.search(r'connect from (\S+) \(', probe_log.read_text(errors='replace'))
|
||||
if not m:
|
||||
test_fail(f"no 'connect from' line in the probe daemon log {probe_log}")
|
||||
peer = m.group(1)
|
||||
print(f"daemon sees its peer as {peer!r}")
|
||||
|
||||
|
||||
def fullwidth(name):
|
||||
"""The fullwidth-forms spelling of an ASCII name.
|
||||
|
||||
IDNA (UTS #46) maps U+FF01..U+FF5E back onto ASCII, so this gives us a
|
||||
genuinely non-ASCII name that folds to a peer name we can predict without
|
||||
controlling DNS. Label separators stay ASCII dots.
|
||||
"""
|
||||
return ''.join(chr(ord(c) - 0x21 + 0xFF01) if '!' <= c <= '~' and c != '.'
|
||||
else c for c in name)
|
||||
|
||||
|
||||
if fullwidth(peer) == peer:
|
||||
test_skipped(f"peer name {peer!r} has no ASCII to respell in fullwidth")
|
||||
|
||||
ZWSP = '' # maps to nothing, leaving no label at all
|
||||
FW_STAR = '*' # FULLWIDTH ASTERISK, which IDNA maps to '*'
|
||||
FW_SLASH = '/' # FULLWIDTH SOLIDUS, which IDNA maps to '/'
|
||||
|
||||
modules = [
|
||||
('ascii-name', {'hosts allow': peer}),
|
||||
('ascii-upper', {'hosts allow': peer.upper()}),
|
||||
('ascii-wild', {'hosts allow': peer[:1] + '*'}),
|
||||
# The same name in fullwidth forms, which only matches once the token has
|
||||
# been folded to A-labels.
|
||||
('idn-name', {'hosts allow': fullwidth(peer)}),
|
||||
('idn-mixedcase', {'hosts allow': fullwidth(peer.upper())}),
|
||||
('idn-deny', {'hosts deny': fullwidth(peer)}),
|
||||
# A real IDN that is not the peer, plus its punycode spelling: both must
|
||||
# stay denied, and neither may be mistaken for a wildcard.
|
||||
('idn-other', {'hosts allow': 'čičku.example'}),
|
||||
('idn-puny', {'hosts allow': 'xn--iku-eqab.example'}),
|
||||
# idn-other's token respelled with combining carons: an equivalent name
|
||||
# under Unicode, so it has to be treated the same way. (A decomposed
|
||||
# token that *matches* isn't constructible here -- the peer name comes
|
||||
# from DNS and is ASCII -- so idn_test checks the two spellings convert
|
||||
# alike, and this checks the daemon agrees they don't match.)
|
||||
('idn-nfd', {'hosts allow': 'c\u030ci' 'c\u030cku.example'}),
|
||||
# Tokens whose IDNA mapping yields ASCII the author never wrote. Left
|
||||
# unconverted they match nothing; converted blindly, the first two would
|
||||
# allow every host and the third would read as an address/mask.
|
||||
('wide-star', {'hosts allow': FW_STAR}),
|
||||
('wide-star-dom', {'hosts allow': FW_STAR + '.example'}),
|
||||
('wide-mask', {'hosts allow': '127.0.0.0' + FW_SLASH + '8'}),
|
||||
# An IDN that can't be converted at all (its label maps to nothing).
|
||||
('bad-idn', {'hosts allow': ZWSP + '.example'}),
|
||||
]
|
||||
|
||||
conf = write_conf(SCRATCHDIR / 'access-idn.conf', modules,
|
||||
SCRATCHDIR / 'rsyncd.log', SCRATCHDIR / 'rsyncd.pid')
|
||||
url = start_test_daemon(conf, DAEMON_PORT)
|
||||
|
||||
|
||||
def connect(mod):
|
||||
"""Return rsync's exit code for listing the module over the daemon."""
|
||||
return subprocess.run(rsync_argv('-r', f'{url}{mod}/'),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
||||
text=True).returncode
|
||||
|
||||
|
||||
def allowed(mod, why):
|
||||
if connect(mod) != 0:
|
||||
test_fail(f"connection to {mod} should be ALLOWED ({why}) but was refused")
|
||||
|
||||
|
||||
def denied(mod, why):
|
||||
if connect(mod) == 0:
|
||||
test_fail(f"connection to {mod} should be DENIED ({why}) but succeeded")
|
||||
|
||||
|
||||
allowed('ascii-name', "the peer's own name in a hosts allow")
|
||||
allowed('ascii-upper', "hostname matching is case-insensitive")
|
||||
allowed('ascii-wild', "an ASCII wildcard still matches")
|
||||
allowed('idn-name', f"fullwidth {peer!r} folds to the peer's name")
|
||||
allowed('idn-mixedcase', "IDNA case-folds the token")
|
||||
denied('idn-deny', "hosts deny sees the folded token too")
|
||||
denied('idn-other', "a different IDN must not match the peer")
|
||||
denied('idn-puny', "an A-label for a different host must not match the peer")
|
||||
denied('idn-nfd', "a decomposed spelling of that name must not match either")
|
||||
denied('wide-star', "U+FF0A must not become a '*' that allows every host")
|
||||
denied('wide-star-dom', "U+FF0A must not become a wildcard label")
|
||||
denied('wide-mask', "U+FF0F must not become an address/mask separator")
|
||||
denied('bad-idn', "an unconvertible IDN must not match anything")
|
||||
|
||||
print("daemon-access-idn: IDN hosts allow/deny matching + no wildcard widening")
|
||||
@@ -19,9 +19,7 @@ from rsyncfns import (
|
||||
SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,
|
||||
)
|
||||
|
||||
# Not 13000-13060: ASUS Armoury Crate on the Cygwin CI host parks localhost
|
||||
# listeners there (13010 among them).
|
||||
DAEMON_PORT = 12931
|
||||
DAEMON_PORT = 13010
|
||||
|
||||
# (module name, exclude pattern, expect the pushed file to land)
|
||||
CASES = [
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Daemon-mode: the server must reject a wire-supplied --max-alloc=0.
|
||||
|
||||
max-alloc-zero-rejected_test.py only proves the *local* client refuses
|
||||
--max-alloc=0. That alone doesn't protect a daemon: a modified or older client
|
||||
still forwards --max-alloc=0 on the wire, and an unpatched daemon honours it and
|
||||
disables its my_alloc() allocation cap (the defence behind CVE-2024-12084 and
|
||||
friends). This test drives an older rsync client -- which lacks the reject-zero
|
||||
check and so forwards the option -- against the current rsync daemon, and
|
||||
asserts the *daemon* refuses it.
|
||||
|
||||
It uses the in-tree old_versions/rsync_3.2.7 as the client (3.2.7 predates the
|
||||
reject-zero fix, so it forwards --max-alloc=0 on the wire). If that binary is
|
||||
missing or can't run here (e.g. a non-Linux host that can't run the static
|
||||
archive) the test skips.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import (
|
||||
FROMDIR, RSYNC, SCRATCHDIR,
|
||||
makepath, rmtree, start_test_daemon, test_fail, test_skipped,
|
||||
write_daemon_conf,
|
||||
)
|
||||
|
||||
DAEMON_PORT = 12932
|
||||
REJECT_MSG = 'max-alloc must be greater than zero'
|
||||
|
||||
OLD_CLIENT = Path(__file__).resolve().parents[1] / 'old_versions' / 'rsync_3.2.7'
|
||||
|
||||
if not OLD_CLIENT.exists():
|
||||
test_skipped(f"{OLD_CLIENT} not present")
|
||||
|
||||
# Confirm the static binary actually runs as rsync on this OS/arch before we
|
||||
# depend on it: exec of a foreign-arch/OS binary raises OSError, while one that
|
||||
# loads but can't run won't print the rsync banner. (3.2.7 predates the
|
||||
# reject-zero fix, so once it runs it forwards --max-alloc=0 on the wire.)
|
||||
try:
|
||||
probe = subprocess.run([str(OLD_CLIENT), '--version'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True)
|
||||
except OSError as e:
|
||||
test_skipped(f"cannot run {OLD_CLIENT.name} on this OS/arch: {e}")
|
||||
if probe.returncode != 0 or 'version 3.2.7' not in probe.stdout:
|
||||
test_skipped(f"{OLD_CLIENT.name} does not run as rsync on this OS/arch")
|
||||
|
||||
# Module served by the *current* (patched) daemon.
|
||||
src = FROMDIR
|
||||
rmtree(src)
|
||||
makepath(src)
|
||||
(src / 'file.txt').write_text('hello\n')
|
||||
|
||||
conf = write_daemon_conf([('mod', {'path': str(src), 'read only': 'yes'})])
|
||||
url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)
|
||||
|
||||
dest = SCRATCHDIR / 'out.txt'
|
||||
|
||||
|
||||
def run_client(*extra):
|
||||
argv = [str(OLD_CLIENT), *extra, f'{url}mod/file.txt', str(dest)]
|
||||
return subprocess.run(argv, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE, text=True)
|
||||
|
||||
|
||||
# Positive control: the old client and current daemon transfer fine without the
|
||||
# option, so the failure below is specifically the daemon refusing the option.
|
||||
dest.unlink(missing_ok=True)
|
||||
ctrl = run_client()
|
||||
if ctrl.returncode != 0:
|
||||
test_fail(f"old client could not talk to the current daemon:\n{ctrl.stderr}")
|
||||
|
||||
# The attack: a forwarded --max-alloc=0 must be refused by the daemon.
|
||||
dest.unlink(missing_ok=True)
|
||||
proc = run_client('--max-alloc=0')
|
||||
if proc.returncode == 0:
|
||||
test_fail("daemon accepted a wire-supplied --max-alloc=0")
|
||||
if REJECT_MSG not in proc.stderr:
|
||||
test_fail("daemon did not reject --max-alloc=0 with the expected message; "
|
||||
f"stderr:\n{proc.stderr}")
|
||||
|
||||
print("daemon-max-alloc-zero: daemon refuses a wire-supplied --max-alloc=0 "
|
||||
f"(client {OLD_CLIENT.name})")
|
||||
@@ -227,10 +227,9 @@
|
||||
"scratchbase": "/Volumes/RsyncHFS",
|
||||
"expect_skip_omit": [
|
||||
"backup-crossdev-copy",
|
||||
"chmod-temp-dir",
|
||||
"operator-path-backup-chown"
|
||||
"chmod-temp-dir"
|
||||
],
|
||||
"_skip_comment": "The three omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)"
|
||||
"_skip_comment": "The two omitted entries are macOS-wide expected-skips that this target RUNS: the separate volume supplies the cross-device conditions mac2 lacks. (itemize used to XFAIL here, before --link-dest learned to fall back when the filesystem cannot hard-link a symlink.)"
|
||||
},
|
||||
{
|
||||
"_comment": "The x86-64 Mac (macOS 10.13). The ONLY target that can build the x86-64 md5 assembly -- mac2 is arm64, where configure refuses --enable-md5-asm outright. MacPorts supplies autotools, python3 and the crypto/hash libs the stock 10.13 image lacks, and is not on the non-interactive ssh PATH, so put it there for the whole run. This target keeps the STOCK Apple compiler (clang 10, the 10.13 ceiling), which is what caught #161; it cannot build --enable-roll-simd, because a clang that old rejects configure's target(\"default\") multiversioning probe. That is a compiler-VERSION limit, not a Mach-O one -- see mac-x86-asm below, which builds the same source with MacPorts clang 19 and all three optimizations on.",
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
# Verify that rsync converts an IDN (internationalized domain name) host to
|
||||
# its IDNA A-label (Punycode) form, and that it leaves an ASCII host name
|
||||
# alone. Only the labels that are not ASCII get rewritten, so an address
|
||||
# literal, an already-punycoded name, and a name that isn't a valid IDN all
|
||||
# reach the resolver as typed. A name typed with combining marks is normalized
|
||||
# on the way, so it converts the same as its precomposed spelling.
|
||||
#
|
||||
# Two daemon connection methods carry the host name out of rsync, so both are
|
||||
# checked:
|
||||
# * daemon over a remote shell (what rsync-ssl does): the host is handed to
|
||||
# the --rsh helper.
|
||||
# * direct daemon socket: observed through a dummy HTTP proxy (RSYNC_PROXY) on
|
||||
# loopback, so this part only runs under --use-tcp.
|
||||
# A plain remote-shell transfer (host:path) is intentionally left alone, since
|
||||
# that name belongs to the user's ssh.
|
||||
#
|
||||
# The daemon side of IDN -- hosts allow/deny matching -- is daemon-access-idn.
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from rsyncfns import (
|
||||
RSYNC, SCRATCHDIR, USE_TCP, claim_ports, run_rsync,
|
||||
test_fail, test_skipped,
|
||||
)
|
||||
|
||||
|
||||
if '"IDN": true' not in run_rsync('-VV', check=True, capture_output=True).stdout:
|
||||
test_skipped("rsync built without IDN support")
|
||||
|
||||
|
||||
def find_utf8_locale():
|
||||
try:
|
||||
out = subprocess.check_output(['locale', '-a'], text=True,
|
||||
stderr=subprocess.DEVNULL)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return None
|
||||
avail = out.split()
|
||||
for want in ('C.UTF-8', 'C.utf8', 'en_US.UTF-8', 'en_US.utf8'):
|
||||
if want in avail:
|
||||
return want
|
||||
for loc in avail:
|
||||
if loc.lower().replace('-', '').endswith('utf8'):
|
||||
return loc
|
||||
return None
|
||||
|
||||
|
||||
utf8_locale = find_utf8_locale()
|
||||
if not utf8_locale:
|
||||
test_skipped("no UTF-8 locale available to encode the IDN host")
|
||||
|
||||
idn_host = "\u010ci\u010dku.example"
|
||||
ascii_host = "xn--iku-eqab.example"
|
||||
# The same name with each caron letter spelled as a plain "c" plus a combining
|
||||
# caron (U+030C). Unicode calls the two spellings equivalent, so both have to
|
||||
# come out as the same A-label; libidn2 is what normalizes them.
|
||||
nfd_host = "c\u030ci" "c\u030cku.example"
|
||||
|
||||
env = os.environ.copy()
|
||||
env['LC_ALL'] = utf8_locale
|
||||
out_dir = (str(SCRATCHDIR / 'out') + '/').encode()
|
||||
|
||||
|
||||
def run_idn(url, *extra, extra_env=None):
|
||||
# A bytes argv keeps the UTF-8 host intact regardless of Python's
|
||||
# filesystem encoding.
|
||||
e = dict(env)
|
||||
if extra_env:
|
||||
e.update(extra_env)
|
||||
argv = [a.encode() for a in shlex.split(RSYNC)]
|
||||
argv += [a.encode() for a in extra]
|
||||
argv += [url.encode('utf-8'), out_dir]
|
||||
return subprocess.run(argv, capture_output=True, env=e, timeout=30)
|
||||
|
||||
|
||||
# --- daemon over a remote shell (the rsync-ssl mechanism) ------------------
|
||||
helper = SCRATCHDIR / 'idn-rsh.sh'
|
||||
helper.write_text('#!/bin/sh\nprintf %s "$1" > "$IDN_RSH_OUT"\nexit 1\n')
|
||||
helper.chmod(0o755)
|
||||
|
||||
hostfile = SCRATCHDIR / 'idn-rsh-host'
|
||||
|
||||
|
||||
def rsh_host(url_host):
|
||||
"""The host name rsync hands the --rsh helper for rsync://<url_host>/."""
|
||||
if hostfile.exists():
|
||||
hostfile.unlink()
|
||||
run_idn(f"rsync://{url_host}/module/", f"--rsh={helper}",
|
||||
extra_env={'IDN_RSH_OUT': str(hostfile)})
|
||||
if not hostfile.exists():
|
||||
test_fail(f"the --rsh helper never ran for {url_host!r}")
|
||||
return hostfile.read_bytes().decode('utf-8', 'surrogateescape')
|
||||
|
||||
|
||||
def check_rsh(url_host, want, what):
|
||||
got = rsh_host(url_host)
|
||||
if got != want:
|
||||
test_fail(f"daemon-over-rsh sent host {got!r} for {what} "
|
||||
f"({url_host!r}), expected {want!r}")
|
||||
print(f"OK: {what} -> {got}")
|
||||
|
||||
|
||||
# A U-label becomes its A-label, case-folded by the IDNA mapping. An ASCII
|
||||
# label is handed on byte for byte, case included, since DNS doesn't care.
|
||||
check_rsh(idn_host, ascii_host, "a Unicode host")
|
||||
check_rsh(nfd_host, ascii_host, "a decomposed Unicode host")
|
||||
check_rsh("C\u030cI" "C\u030cKU.Example", "xn--iku-eqab.Example",
|
||||
"a decomposed mixed-case Unicode host")
|
||||
check_rsh("ČIČKU.Example", "xn--iku-eqab.Example",
|
||||
"a mixed-case Unicode host")
|
||||
check_rsh(ascii_host, ascii_host, "an already-punycoded host")
|
||||
check_rsh("XN--IKU-EQAB.Example", "XN--IKU-EQAB.Example",
|
||||
"a mixed-case punycoded host")
|
||||
# A name that isn't a valid IDN goes out as-is instead of being rewritten into
|
||||
# some other name (the U+200B one would map to ".example"), so the resolver
|
||||
# fails on it just as it did before.
|
||||
check_rsh("xn--0.example", "xn--0.example", "an undecodable A-label")
|
||||
check_rsh("ـx.example", "ـx.example", "a label with a disallowed character")
|
||||
check_rsh(".example", ".example", "a label that maps to nothing")
|
||||
|
||||
|
||||
# --- direct daemon socket, observed via a dummy proxy -----------------------
|
||||
if not USE_TCP:
|
||||
print("direct-socket proxy check needs --use-tcp; skipping that part")
|
||||
sys.exit(0)
|
||||
|
||||
PROXY_PORT = 13335
|
||||
claim_ports(PROXY_PORT)
|
||||
|
||||
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
listener.bind(('127.0.0.1', PROXY_PORT))
|
||||
listener.listen(1)
|
||||
|
||||
captured = {}
|
||||
|
||||
|
||||
def serve_one():
|
||||
conn, _ = listener.accept()
|
||||
conn.settimeout(5)
|
||||
data = b""
|
||||
try:
|
||||
while b"\r\n\r\n" not in data and len(data) < 65536:
|
||||
chunk = conn.recv(8192)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
except socket.timeout:
|
||||
pass
|
||||
captured['request'] = data
|
||||
try:
|
||||
conn.sendall(b"HTTP/1.0 403 Forbidden\r\n\r\n")
|
||||
conn.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
conn.close()
|
||||
|
||||
|
||||
t = threading.Thread(target=serve_one)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
proc = run_idn(f"rsync://{idn_host}:873/whatever/",
|
||||
extra_env={'RSYNC_PROXY': f'127.0.0.1:{PROXY_PORT}'})
|
||||
|
||||
t.join(timeout=15)
|
||||
listener.close()
|
||||
|
||||
if proc.returncode >= 128:
|
||||
sys.stderr.write(proc.stderr.decode('latin1'))
|
||||
test_fail(f"rsync killed by signal (status={proc.returncode})")
|
||||
|
||||
request = captured.get('request', b'')
|
||||
if not request:
|
||||
test_fail("dummy proxy received no CONNECT request from rsync")
|
||||
|
||||
if ascii_host.encode() not in request:
|
||||
sys.stderr.write("proxy received: %r\n" % request.split(b"\r\n", 1)[0])
|
||||
test_fail(f"expected A-label {ascii_host} in the proxy CONNECT request")
|
||||
|
||||
print(f"OK: direct-socket CONNECT host sent as {ascii_host}")
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import filecmp
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from rsyncfns import SCRATCHDIR, SRCDIR, TOOLDIR, test_fail
|
||||
|
||||
|
||||
make_vars = {}
|
||||
for line in (TOOLDIR / 'Makefile').read_text().splitlines():
|
||||
name, separator, value = line.partition('=')
|
||||
if separator and name in ('prefix', 'exec_prefix', 'bindir',
|
||||
'EXEEXT', 'STRIP'):
|
||||
make_vars[name] = value.strip()
|
||||
|
||||
if not make_vars.get('STRIP'):
|
||||
test_fail('configured Makefile does not define STRIP')
|
||||
|
||||
source_rsync = TOOLDIR / f"rsync{make_vars.get('EXEEXT', '')}"
|
||||
if not source_rsync.is_file():
|
||||
test_fail('cannot find the built rsync binary')
|
||||
|
||||
builddir = SCRATCHDIR / 'build'
|
||||
builddir.mkdir()
|
||||
makefile_text = (TOOLDIR / 'Makefile').read_text()
|
||||
if 'install: all\n' not in makefile_text:
|
||||
test_fail('cannot isolate the install target from the shared build')
|
||||
if 'Makefile: Makefile.in config.status configure.sh config.h.in\n' not in makefile_text:
|
||||
test_fail('cannot isolate Makefile regeneration from the shared build')
|
||||
(builddir / 'Makefile').write_text(
|
||||
makefile_text
|
||||
.replace('install: all\n', 'install:\n', 1)
|
||||
.replace('Makefile: Makefile.in config.status configure.sh config.h.in\n',
|
||||
'Makefile:\n', 1)
|
||||
)
|
||||
|
||||
for name in (source_rsync.name, 'install-sh', 'rsync-ssl', 'rrsync', 'rsync.1',
|
||||
'rsync-ssl.1', 'rsyncd.conf.5', 'rrsync.1'):
|
||||
source = TOOLDIR / name
|
||||
if not source.is_file():
|
||||
source = SRCDIR / name
|
||||
if source.is_file():
|
||||
shutil.copy2(source, builddir / name)
|
||||
|
||||
|
||||
def expand_make_value(value):
|
||||
for _ in range(10):
|
||||
expanded = re.sub(
|
||||
r'\$\{([^}]+)\}|\$\(([^)]+)\)',
|
||||
lambda match: make_vars.get(match.group(1) or match.group(2),
|
||||
match.group(0)),
|
||||
value,
|
||||
)
|
||||
if expanded == value:
|
||||
return expanded
|
||||
value = expanded
|
||||
test_fail(f'cannot expand Makefile value {value!r}')
|
||||
|
||||
|
||||
bindir = expand_make_value(make_vars.get('bindir', ''))
|
||||
if not bindir or '$' in bindir:
|
||||
test_fail(f'cannot determine configured bindir from {bindir!r}')
|
||||
|
||||
make = shlex.split(os.environ.get('MAKE', 'make'))
|
||||
tools = SCRATCHDIR / 'tools'
|
||||
tools.mkdir()
|
||||
|
||||
for strip_name in ('strip', 'aarch64-linux-gnu-strip'):
|
||||
destdir = SCRATCHDIR / 'roots' / strip_name
|
||||
strip_log = SCRATCHDIR / f'{strip_name}.log'
|
||||
strip = tools / strip_name
|
||||
strip.write_text('#!/bin/sh\nprintf \'%s\\n\' "$@" >"$STRIP_LOG"\n')
|
||||
strip.chmod(0o755)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['STRIP_LOG'] = str(strip_log)
|
||||
proc = subprocess.run(
|
||||
[*make, f'DESTDIR={destdir}', f'STRIP={strip}', 'install-strip'],
|
||||
cwd=builddir, env=env, capture_output=True, text=True,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
test_fail(f'install-strip failed with {strip_name}:\n'
|
||||
f'{proc.stdout}{proc.stderr}')
|
||||
|
||||
installed = destdir / bindir.lstrip('/') / source_rsync.name
|
||||
if not installed.is_file():
|
||||
test_fail(f'install-strip did not install {installed}')
|
||||
if not strip_log.is_file():
|
||||
test_fail(f'install-strip did not call {strip_name}')
|
||||
if strip_log.read_text().splitlines() != [str(installed)]:
|
||||
test_fail(f'{strip_name} was not called with {installed}')
|
||||
if not filecmp.cmp(source_rsync, installed, shallow=False):
|
||||
test_fail(f'{strip_name} unexpectedly changed the installed test binary')
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
from rsyncfns import SCRATCHDIR, rsync_argv
|
||||
from rsyncfns import expect_fail
|
||||
|
||||
expect_fail(
|
||||
rsync_argv('--max-alloc=0', str(SCRATCHDIR / 'missing-src'), str(SCRATCHDIR / 'missing-dst')),
|
||||
'max-alloc must be greater than zero',
|
||||
)
|
||||
print("max-alloc-zero-rejected: --max-alloc=0 is rejected")
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
"""``--max-alloc=0`` means "the largest limit this build supports".
|
||||
|
||||
Three things are asserted, and the second is the reason 0 is worth keeping as a
|
||||
spelling at all:
|
||||
|
||||
1. 0 is accepted, and a transfer using it works.
|
||||
|
||||
2. 0 reaches the peer as the literal "0", not as a resolved number. Each side
|
||||
then resolves it against its own SIZE_MAX. That is what makes 0 the only
|
||||
value correct for both ends of a mixed-word-size pairing: the ceiling is
|
||||
SIZE_MAX/2, so any number large enough to be worth setting on a 64-bit
|
||||
client (over 2047M) is refused as "too large" by a 32-bit daemon.
|
||||
|
||||
3. The parser's upper bound is still enforced. Accepting 0 again must not
|
||||
bring back the unbounded ``size *= atof(size_arg)`` that was fixed in
|
||||
3.5.0, so an out-of-range value is still rejected rather than wrapping.
|
||||
|
||||
The forwarding check in (2) deliberately inspects the argv the remote shell is
|
||||
handed rather than a transfer outcome: a resolved number also copies files
|
||||
happily on a same-word-size pair, so an outcome-based assertion would pass on
|
||||
exactly the configuration this behaviour does not matter for.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, SRCDIR, expect_fail, rsh_cmd, rmtree, rsync_argv,
|
||||
rsync_path_arg, run_rsync, test_fail,
|
||||
)
|
||||
|
||||
base = SCRATCHDIR / 'max-alloc-zero'
|
||||
rmtree(base)
|
||||
src = base / 'from'
|
||||
dst = base / 'to'
|
||||
src.mkdir(parents=True)
|
||||
dst.mkdir(parents=True)
|
||||
(src / 'file.txt').write_text('hello\n')
|
||||
|
||||
# --- 1. 0 is accepted -------------------------------------------------------
|
||||
|
||||
run_rsync('-r', '--max-alloc=0', f'{src}/', f'{dst}/')
|
||||
if (dst / 'file.txt').read_text() != 'hello\n':
|
||||
test_fail('--max-alloc=0 did not copy the file')
|
||||
|
||||
# --- 2. 0 goes on the wire un-normalized ------------------------------------
|
||||
|
||||
argv_log = base / 'server-argv'
|
||||
wrapper = base / 'log-rsh.sh'
|
||||
wrapper.write_text(
|
||||
'#!/bin/sh\n'
|
||||
'# Log the command line built for the peer, then behave like lsh.sh.\n'
|
||||
f'printf \'%s\\n\' "$*" >> {shlex.quote(str(argv_log))}\n'
|
||||
f'exec {shlex.quote(str(SRCDIR / "support" / "lsh.sh"))} "$@"\n'
|
||||
)
|
||||
wrapper.chmod(0o755)
|
||||
|
||||
rmtree(dst)
|
||||
dst.mkdir()
|
||||
os.environ['RSYNC_RSH'] = rsh_cmd(str(wrapper))
|
||||
run_rsync('-r', '--max-alloc=0', f'--rsync-path={rsync_path_arg()}',
|
||||
f'localhost:{src}/', f'{dst}/')
|
||||
del os.environ['RSYNC_RSH']
|
||||
|
||||
if (dst / 'file.txt').read_text() != 'hello\n':
|
||||
test_fail('--max-alloc=0 did not copy the file over the remote shell')
|
||||
|
||||
logged = argv_log.read_text() if argv_log.exists() else ''
|
||||
if not logged:
|
||||
test_fail('the remote-shell wrapper logged no command line')
|
||||
if '--max-alloc=0' not in f' {logged} '.replace('\n', ' '):
|
||||
test_fail('--max-alloc=0 was not forwarded verbatim; the peer was sent:\n'
|
||||
f'{logged}'
|
||||
'\nA resolved number here would be rejected as "too large" by a '
|
||||
'peer with a smaller SIZE_MAX.')
|
||||
|
||||
# --- 3. the upper bound still holds -----------------------------------------
|
||||
|
||||
# 8192P is one step past SIZE_ARG_MAX (SIZE_MAX/2) on a 64-bit build; on a
|
||||
# 32-bit one the P multiplier alone already exceeds it. Either way: too large.
|
||||
expect_fail(rsync_argv('--max-alloc=8192P', f'{src}/', f'{dst}/'), 'is too large')
|
||||
|
||||
# And the min-value message must keep advertising a spelling that works.
|
||||
expect_fail(rsync_argv('--max-alloc=1', f'{src}/', f'{dst}/'),
|
||||
'or 0 for unlimited')
|
||||
|
||||
print('max-alloc-zero: 0 is accepted, forwarded verbatim, and the bound holds')
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# --backup-dir parent-component symlink-race confinement for the OWNERSHIP set,
|
||||
# not the create. operator-path-backup-symlink covers the create side (a backup
|
||||
# symlink must not be written outside the backup tree); this covers what
|
||||
# set_file_attrs() does to the item afterwards.
|
||||
#
|
||||
# make_backup() recreates the item at the backup name and then calls
|
||||
# set_file_attrs(buf, ..., ATTRS_OPERATOR_PATH). A regular/dir/fifo leaf is
|
||||
# pinned by op_pin and its metadata driven off that fd, but a SYMLINK leaf never
|
||||
# enters op_pin (there is no O_NOFOLLOW open of a symlink), so the chown falls
|
||||
# through to the path-based wrapper on the full operator path. Unless that
|
||||
# wrapper resolves through the ownership walk, a parent component flipped to an
|
||||
# attacker-owned symlink redirects the lchown out of the backup tree and retags
|
||||
# a victim inode as the attacker's -- an ownership-transfer primitive, and the
|
||||
# trust laundering that then defeats the walk on any later pass.
|
||||
#
|
||||
# Reaching the recreate path at all needs the backup dir on ANOTHER filesystem:
|
||||
# on one filesystem make_backup() hard-links or renames the item across and
|
||||
# never calls set_file_attrs(). So the whole fixture lives on tmpfs.
|
||||
#
|
||||
# A statically planted symlink is not enough either -- rsync's own backup-dir
|
||||
# validation deletes a non-directory component before using it -- so the plant
|
||||
# has to be a live flip, as in the sibling test.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, race_budget, find_attacker_uid, rmtree, makepath,
|
||||
start_c_flipper, stop_flipper, test_fail, test_skipped,
|
||||
)
|
||||
|
||||
if os.geteuid() != 0:
|
||||
test_skipped("requires root to own a symlink by a foreign uid and to chown backups")
|
||||
|
||||
ATT_UID = find_attacker_uid()
|
||||
if ATT_UID is None:
|
||||
test_skipped("no untrusted-uid user available for cross-uid plant")
|
||||
|
||||
# The backup dir must be on a different st_dev from the destination, or
|
||||
# make_backup() renames into it and the set_file_attrs() path never runs.
|
||||
dest_dev = os.stat(SCRATCHDIR).st_dev
|
||||
TMPFS = None
|
||||
for cand in ('/dev/shm', '/run/shm', os.environ.get('TMPDIR', '/tmp')):
|
||||
try:
|
||||
if os.stat(cand).st_dev != dest_dev and os.access(cand, os.W_OK):
|
||||
TMPFS = cand
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if TMPFS is None:
|
||||
test_skipped("no writable cross-device dir (tmpfs) for the --backup-dir EXDEV path")
|
||||
|
||||
# Many files widen the per-file backup window so the flipper has more chances to
|
||||
# land the swap between the recreate and the chown.
|
||||
NFILES = 95
|
||||
|
||||
base = SCRATCHDIR / 'bdir-chown-race'
|
||||
src = base / 'src'
|
||||
dest = base / 'dest'
|
||||
|
||||
bakroot = os.path.join(TMPFS, 'rsync-bakchown-race')
|
||||
backup = os.path.join(bakroot, 'backup')
|
||||
outside = os.path.join(bakroot, 'outside')
|
||||
sub = os.path.join(backup, 'sub')
|
||||
sublink = os.path.join(backup, '.sublink')
|
||||
|
||||
|
||||
def build():
|
||||
"""Reset the workspace. Call only while the flipper is stopped."""
|
||||
rmtree(base)
|
||||
subprocess.run(['rm', '-rf', bakroot], check=False)
|
||||
makepath(src / 'sub', dest / 'sub')
|
||||
os.makedirs(outside, exist_ok=True)
|
||||
os.makedirs(backup, exist_ok=True)
|
||||
|
||||
# Distinct source and destination symlink values so each transfer replaces
|
||||
# the destination symlink and thus backs the old one up. The destination
|
||||
# symlinks are attacker-owned, so restoring their ownership onto the backup
|
||||
# copy REQUIRES an lchown -- without that there is no chown to redirect and
|
||||
# the test would pass vacuously.
|
||||
for i in range(NFILES):
|
||||
(src / 'sub' / f'f{i}').symlink_to('test')
|
||||
d = dest / 'sub' / f'f{i}'
|
||||
d.symlink_to('test2')
|
||||
os.lchown(d, ATT_UID, ATT_UID)
|
||||
|
||||
# Victims: root-owned regular files carrying the names the backup would use
|
||||
# if a flipped `sub` redirected the operator path into outside/.
|
||||
for i in range(NFILES):
|
||||
v = os.path.join(outside, f'f{i}')
|
||||
with open(v, 'w') as fh:
|
||||
fh.write('victim\n')
|
||||
os.chown(v, 0, 0)
|
||||
|
||||
# The attacker-owned parent-swap target.
|
||||
os.symlink(outside, sublink)
|
||||
os.lchown(sublink, ATT_UID, ATT_UID)
|
||||
os.makedirs(sub, exist_ok=True)
|
||||
|
||||
|
||||
def push():
|
||||
"""Blocking local rsync push that backs up the old destination symlinks."""
|
||||
return subprocess.run(
|
||||
['./rsync', '-a', '-b', f'--backup-dir={backup}', f'{src}/', f'{dest}/'],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||
)
|
||||
|
||||
|
||||
def retagged():
|
||||
"""Name of a victim in outside/ that stopped being root-owned, or ''.
|
||||
|
||||
A swap killed mid-rename can leave outside/ momentarily odd; anything we
|
||||
cannot stat is simply not evidence of a win."""
|
||||
try:
|
||||
with os.scandir(outside) as it:
|
||||
for e in it:
|
||||
try:
|
||||
st = e.stat(follow_symlinks=False)
|
||||
except OSError:
|
||||
continue
|
||||
if st.st_uid != 0 or st.st_gid != 0:
|
||||
return e.name
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return ''
|
||||
return ''
|
||||
|
||||
|
||||
# ---- POSITIVE CONTROL ------------------------------------------------------
|
||||
# A clean run must (a) back the old destination symlink into the backup tree via
|
||||
# the cross-device recreate path and (b) carry the attacker ownership onto that
|
||||
# backup copy -- which is the lchown this test is about. Without both, the race
|
||||
# below would be asserting on a code path that never executes.
|
||||
build()
|
||||
proc = push()
|
||||
if proc.returncode != 0:
|
||||
test_fail(f"positive control: clean --backup-dir run failed (rc={proc.returncode}):\n{proc.stdout or ''}")
|
||||
|
||||
bak0 = os.path.join(sub, 'f0')
|
||||
if not os.path.islink(bak0) or os.readlink(bak0) != 'test2':
|
||||
test_fail(f"positive control: the old destination symlink was not backed up into {sub}; "
|
||||
"the cross-device recreate path was not exercised")
|
||||
st = os.lstat(bak0)
|
||||
if st.st_uid != ATT_UID:
|
||||
test_fail(f"positive control: backup copy {bak0} is uid {st.st_uid}, expected the "
|
||||
f"attacker uid {ATT_UID}; set_file_attrs() did not lchown the backup, so "
|
||||
"this test would pass vacuously")
|
||||
if retagged():
|
||||
test_fail("positive control: a victim in outside/ changed ownership during a no-flipper run")
|
||||
|
||||
|
||||
# ---- THE LIVE RACE ---------------------------------------------------------
|
||||
# Flip backup/sub between the real backup directory and the attacker-owned
|
||||
# symlink to outside/ under a live transfer. The ownership walk must refuse the
|
||||
# foreign-owned component, so no victim in outside/ is ever retagged.
|
||||
|
||||
deadline = time.monotonic() + race_budget(10.0)
|
||||
flip = None
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
# Reset only while the flipper is quiet, so build()'s rmtree/mkdir
|
||||
# cannot race the swapper and drop artifacts in outside/.
|
||||
if flip is not None:
|
||||
stop_flipper(flip)
|
||||
flip = None
|
||||
build()
|
||||
flip = start_c_flipper(sub, sublink)
|
||||
push()
|
||||
|
||||
victim = retagged()
|
||||
if victim:
|
||||
test_fail(
|
||||
"--backup-dir parent symlink race: victim "
|
||||
f"{os.path.join(outside, victim)} was retagged away from root; rsync "
|
||||
"chowned through the flipped attacker-owned backup/sub component "
|
||||
"instead of refusing it."
|
||||
)
|
||||
finally:
|
||||
if flip is not None:
|
||||
stop_flipper(flip)
|
||||
subprocess.run(['rm', '-rf', bakroot], check=False)
|
||||
|
||||
print("operator-path-backup-chown: backup ownership confined under parent-swap race")
|
||||
@@ -37,6 +37,17 @@ if _proto is not None and _proto < 30:
|
||||
|
||||
hook_code = r'''
|
||||
#define _GNU_SOURCE
|
||||
/* Build the hook itself WITHOUT large-file redirection, whatever the compiler
|
||||
* defaults to. Debian's armhf/hppa/powerpc gcc predefines
|
||||
* -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64 (check with "gcc -v -E -"), and under
|
||||
* those macros glibc's __REDIRECT renames the DEFINITIONS below -- open()
|
||||
* becomes open64(), fstatat() becomes __fstatat64_time64() -- which then
|
||||
* collide with the explicit large-file wrappers further down ("symbol `open64'
|
||||
* is already defined"). Undefining them here keeps each name declared exactly
|
||||
* once, so the hook always exports both spellings and interposes whichever set
|
||||
* the rsync under test was linked against. */
|
||||
#undef _FILE_OFFSET_BITS
|
||||
#undef _TIME_BITS
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
@@ -56,6 +67,9 @@ static int (*real_open)(const char *, int, ...);
|
||||
static int (*real_openat)(int, const char *, int, ...);
|
||||
static int (*real_fstatat)(int, const char *, struct stat *, int);
|
||||
static int (*real_fxstatat)(int, int, const char *, struct stat *, int);
|
||||
static int (*real_fstatat64)(int, const char *, struct stat64 *, int);
|
||||
static int (*real_fxstatat64)(int, int, const char *, struct stat64 *, int);
|
||||
static int (*real_fstatat64_time64)(int, const char *, struct stat64 *, int);
|
||||
|
||||
/* Resolve on demand rather than trusting our constructor to have run. A
|
||||
* preloaded open() interposes for the whole process the moment the loader maps
|
||||
@@ -77,6 +91,10 @@ static void hook_resolve(void)
|
||||
if (!real_openat) real_openat = dlsym(RTLD_NEXT, "openat");
|
||||
if (!real_fstatat) real_fstatat = dlsym(RTLD_NEXT, "fstatat");
|
||||
if (!real_fxstatat) real_fxstatat = dlsym(RTLD_NEXT, "__fxstatat");
|
||||
if (!real_fstatat64) real_fstatat64 = dlsym(RTLD_NEXT, "fstatat64");
|
||||
if (!real_fxstatat64) real_fxstatat64 = dlsym(RTLD_NEXT, "__fxstatat64");
|
||||
if (!real_fstatat64_time64)
|
||||
real_fstatat64_time64 = dlsym(RTLD_NEXT, "__fstatat64_time64");
|
||||
hook_resolving = 0;
|
||||
}
|
||||
|
||||
@@ -151,6 +169,10 @@ static int swap_and_deny(void)
|
||||
# define HOOK_TAKES_MODE(f) ((f) & O_CREAT)
|
||||
#endif
|
||||
|
||||
#ifndef O_LARGEFILE
|
||||
# define O_LARGEFILE 0
|
||||
#endif
|
||||
|
||||
static int is_victim_write(const char *path, int flags)
|
||||
{
|
||||
return path && strcmp(path, "victim") == 0
|
||||
@@ -231,6 +253,49 @@ int open(const char *path, int flags, ...)
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* --- large-file spellings -------------------------------------------------
|
||||
* Which names the RECEIVER calls is settled by its own build: where off_t is
|
||||
* not already 64 bits, configure's AC_SYS_LARGEFILE adds -D_FILE_OFFSET_BITS=64
|
||||
* (i386, alpha, ...), and distro CPPFLAGS add it -- along with -D_TIME_BITS=64
|
||||
* -- on the 64-bit time_t ports, so glibc redirects each open()/openat()/
|
||||
* fstatat() call to open64()/openat64()/fstatat64()/__fstatat64_time64().
|
||||
* "objdump -T rsync | grep UND" says which set a given build imports.
|
||||
*
|
||||
* Which names THIS HOOK exports is a different question with a different
|
||||
* answer, settled by whatever the "cc" below it defaults to -- see the #undef
|
||||
* at the top. Nothing keeps the two in step, so define every spelling and let
|
||||
* the loader match them up. With only the unsuffixed ones the receiver's opens
|
||||
* sail straight past the hook, no EACCES is ever injected, and the test reports
|
||||
* "positive control failed" having exercised nothing at all.
|
||||
*
|
||||
* O_LARGEFILE is the only thing open64() adds over open(), so the wrappers
|
||||
* below can hand the call to the unsuffixed interposer above. */
|
||||
int open64(const char *path, int flags, ...)
|
||||
{
|
||||
mode_t mode = 0;
|
||||
|
||||
if (HOOK_TAKES_MODE(flags)) {
|
||||
va_list ap;
|
||||
va_start(ap, flags);
|
||||
mode = (mode_t)va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
return open(path, flags | O_LARGEFILE, mode);
|
||||
}
|
||||
|
||||
int openat64(int dfd, const char *path, int flags, ...)
|
||||
{
|
||||
mode_t mode = 0;
|
||||
|
||||
if (HOOK_TAKES_MODE(flags)) {
|
||||
va_list ap;
|
||||
va_start(ap, flags);
|
||||
mode = (mode_t)va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
return openat(dfd, path, flags | O_LARGEFILE, mode);
|
||||
}
|
||||
|
||||
/* ona_open() decides via fstatat(..., AT_SYMLINK_NOFOLLOW) and refuses a
|
||||
* component owned by neither root nor the euid. Model the attacker as a
|
||||
* different uid so a retry that kept the ownership walk refuses the swap. */
|
||||
@@ -276,6 +341,68 @@ int __fxstatat(int ver, int dfd, const char *path, struct stat *st, int flags)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* The stat family's large-file spellings. st_mode and st_uid sit ahead of the
|
||||
* timestamps in every glibc struct stat layout, so the time32/time64 variants
|
||||
* of the buffer are interchangeable for the two fields touched here. */
|
||||
static void model_foreign_owner64(int rc, const char *path, struct stat64 *st)
|
||||
{
|
||||
if (rc == 0 && swapped && path && strcmp(path, "pdir") == 0
|
||||
&& S_ISLNK(st->st_mode)) {
|
||||
st->st_uid = geteuid() + 1;
|
||||
mark(getenv("RSYNC_PARTIAL_RETRY_FOREIGN_MARKER"));
|
||||
}
|
||||
}
|
||||
|
||||
int fstatat64(int dfd, const char *path, struct stat64 *st, int flags)
|
||||
{
|
||||
int rc, saved_errno;
|
||||
|
||||
hook_resolve();
|
||||
if (!real_fstatat64) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
rc = real_fstatat64(dfd, path, st, flags);
|
||||
saved_errno = errno;
|
||||
model_foreign_owner64(rc, path, st);
|
||||
errno = saved_errno;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int __fxstatat64(int ver, int dfd, const char *path, struct stat64 *st, int flags)
|
||||
{
|
||||
int rc, saved_errno;
|
||||
|
||||
hook_resolve();
|
||||
if (!real_fxstatat64) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
rc = real_fxstatat64(ver, dfd, path, st, flags);
|
||||
saved_errno = errno;
|
||||
model_foreign_owner64(rc, path, st);
|
||||
errno = saved_errno;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* A 32-bit port built with -D_TIME_BITS=64 (Debian's armhf/armel/hppa/powerpc,
|
||||
* ...) reaches fstatat() under this third name. */
|
||||
int __fstatat64_time64(int dfd, const char *path, struct stat64 *st, int flags)
|
||||
{
|
||||
int rc, saved_errno;
|
||||
|
||||
hook_resolve();
|
||||
if (!real_fstatat64_time64) {
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
rc = real_fstatat64_time64(dfd, path, st, flags);
|
||||
saved_errno = errno;
|
||||
model_foreign_owner64(rc, path, st);
|
||||
errno = saved_errno;
|
||||
return rc;
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void hook_loaded(void)
|
||||
{
|
||||
hook_resolve();
|
||||
|
||||
@@ -38,12 +38,33 @@ if run_rsync('-a', '--preallocate', f'{src}/', f'{TODIR}/',
|
||||
check=False, capture_output=True).returncode != 0:
|
||||
test_skipped("--preallocate not supported on this platform")
|
||||
|
||||
def fs_can_punch_holes():
|
||||
"""True only where the kernel can deallocate blocks via FALLOC_FL_PUNCH_HOLE
|
||||
-- the mechanism do_punch_hole uses for --sparse. A filesystem may report
|
||||
seek-based sparseness yet still keep every block on a punch (e.g. where
|
||||
rsync's punch falls back to writing zeros), so probe the real capability and
|
||||
assert the hole only where it actually frees blocks."""
|
||||
def punch_frees(offset, length, size):
|
||||
"""True where punching [offset, offset+length) out of a `size`-byte file
|
||||
really deallocates blocks -- the mechanism do_punch_hole uses for --sparse.
|
||||
|
||||
Two separate things can leave st_blocks untouched, so each assertion below
|
||||
probes the exact shape it relies on. A filesystem may report seek-based
|
||||
sparseness yet still keep every block on a punch (e.g. where rsync's punch
|
||||
falls back to writing zeros), which a whole-file probe catches. And a punch
|
||||
only frees storage in whole allocation units, which are not always 4 KiB: a
|
||||
tmpfs frees whole pages, 16 KiB on loongarch/loong64 (and 64 KiB on a
|
||||
64k-page ppc64el or arm64 kernel), and a filesystem may be formatted with a
|
||||
block size above the page size. An interior run spanning no whole unit is
|
||||
zeroed rather than deallocated, so st_blocks does not move and an assertion
|
||||
phrased in st_blocks would report a hole-punching regression that is really
|
||||
just the filesystem's granularity.
|
||||
|
||||
fallocate64() rather than fallocate(): where off_t is 32 bits the latter
|
||||
takes 32-bit offsets, so ctypes' 64-bit arguments do not line up with what
|
||||
it reads (on i386 it takes the high half of `offset` as its `length`) and
|
||||
every probe fails with EINVAL -- which is why every assertion below has
|
||||
silently done nothing on all the 32-bit ports. fallocate64() takes off64_t
|
||||
everywhere and is a plain alias of fallocate() where off_t is already 64
|
||||
bits wide.
|
||||
|
||||
The probe data has to be incompressible: a filesystem that compresses
|
||||
(btrfs with compress=) stores a run of one repeated byte in almost no
|
||||
blocks, leaving a successful punch with nothing to free."""
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
KEEP_SIZE, PUNCH_HOLE = 0x01, 0x02
|
||||
@@ -52,12 +73,12 @@ def fs_can_punch_holes():
|
||||
try:
|
||||
libc = ctypes.CDLL(ctypes.util.find_library('c') or 'libc.so.6',
|
||||
use_errno=True)
|
||||
libc.fallocate.argtypes = [ctypes.c_int, ctypes.c_int,
|
||||
ctypes.c_longlong, ctypes.c_longlong]
|
||||
libc.fallocate64.argtypes = [ctypes.c_int, ctypes.c_int,
|
||||
ctypes.c_longlong, ctypes.c_longlong]
|
||||
fd = os.open(p, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o644)
|
||||
os.write(fd, b'\xff' * 65536)
|
||||
os.write(fd, os.urandom(size))
|
||||
before = os.fstat(fd).st_blocks
|
||||
ret = libc.fallocate(fd, PUNCH_HOLE | KEEP_SIZE, 0, 65536)
|
||||
ret = libc.fallocate64(fd, PUNCH_HOLE | KEEP_SIZE, offset, length)
|
||||
return ret == 0 and os.fstat(fd).st_blocks < before
|
||||
except (OSError, AttributeError, ValueError):
|
||||
return False
|
||||
@@ -70,7 +91,7 @@ def fs_can_punch_holes():
|
||||
pass
|
||||
|
||||
|
||||
can_punch = fs_can_punch_holes()
|
||||
can_punch = punch_frees(0, 65536, 65536)
|
||||
|
||||
|
||||
def seed_plain(size=1_000_000):
|
||||
@@ -139,6 +160,13 @@ with open(src / deep, 'wb') as source, open(TODIR / deep, 'wb') as dest:
|
||||
source.write(block)
|
||||
dest.write(block)
|
||||
|
||||
# Only assert the interior punch where the filesystem can free a 24 KiB run
|
||||
# sitting 4 KiB into a 32 KiB block -- the exact shape written just above.
|
||||
can_punch_interior = can_punch and punch_frees(4096, 24576, 32768)
|
||||
if can_punch and not can_punch_interior:
|
||||
print("preallocate: interior-hole assertion skipped: this filesystem's "
|
||||
"allocation unit cannot free a 24 KiB run inside a 32 KiB block")
|
||||
|
||||
matched_size = os.path.getsize(TODIR / deep)
|
||||
matched_before = allocated(TODIR / deep)
|
||||
run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file',
|
||||
@@ -146,7 +174,7 @@ run_rsync('-a', '--ignore-times', '--inplace', '--sparse', '--no-whole-file',
|
||||
assert_same(TODIR / deep, src / deep,
|
||||
label='--inplace --sparse matched-block content')
|
||||
matched_after = allocated(TODIR / deep)
|
||||
if (can_punch and matched_before >= matched_size
|
||||
if (can_punch_interior and matched_before >= matched_size
|
||||
and matched_after * 2 >= matched_before):
|
||||
test_fail(f"--inplace --sparse left matching interior zero runs allocated: "
|
||||
f"{matched_after} of {matched_before} bytes remain allocated "
|
||||
|
||||
@@ -49,11 +49,17 @@ if not _chown_5001(workdir / 'dst'):
|
||||
if not os.environ.get('RSYNC_UNSHARED'):
|
||||
unshare = shutil.which('unshare')
|
||||
if unshare is not None:
|
||||
probe = subprocess.run(
|
||||
[unshare, '--user', '--map-root-user',
|
||||
'--map-users', '5001:100000:1', 'true'],
|
||||
capture_output=True,
|
||||
)
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
[unshare, '--user', '--map-root-user',
|
||||
'--map-users', '5001:100000:1', 'true'],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=5,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
test_skipped("Can't chown (unshare probe timed out)")
|
||||
if probe.returncode == 0:
|
||||
print("Re-running under unshare with UID mapping...")
|
||||
env = os.environ.copy()
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Process substitution /dev/fd/ write pipe pseudo-paths for --log-file must not crash and must successfully write logs, but must be rejected if confined root."""
|
||||
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped,
|
||||
)
|
||||
if not sys.platform.startswith('linux'):
|
||||
test_skipped('Kernel pseudo-path string is a Linux-specific procfs feature')
|
||||
raise SystemExit(0)
|
||||
|
||||
# We require bash specifically because standard POSIX /bin/sh does not
|
||||
# guarantee support for >(...) process substitution syntax.
|
||||
bash = shutil.which('bash')
|
||||
if bash is None:
|
||||
test_skipped('bash is unavailable, cannot test process substitution')
|
||||
|
||||
# Verify the host bash actually supports process substitution
|
||||
probe = subprocess.run(
|
||||
[bash, '-c', 'echo "probe" > >(cat > /dev/null)'],
|
||||
capture_output=True
|
||||
)
|
||||
if probe.returncode != 0:
|
||||
test_skipped('bash process substitution is not supported on this system')
|
||||
|
||||
base = Path(SCRATCHDIR / 'rsync-pseudo-path').resolve()
|
||||
src = base / 'src'
|
||||
dest = base / 'dest'
|
||||
log_out = base / 'test_log.txt'
|
||||
log_out_confined = base / 'test_log_confined.txt'
|
||||
makepath(src, dest)
|
||||
|
||||
(src / 'transfer_me.txt').write_text('sync this\n')
|
||||
|
||||
rsync_base_cmd = shlex.join(rsync_argv('-a'))
|
||||
src_path = shlex.quote(str(src) + '/')
|
||||
dest_path = shlex.quote(str(dest) + '/')
|
||||
|
||||
log_path = shlex.quote(str(log_out))
|
||||
log_path_confined = shlex.quote(str(log_out_confined))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TEST 1: Unconfined process substitution (Should Succeed)
|
||||
# -------------------------------------------------------------------------
|
||||
bash_script = f"{rsync_base_cmd} -v --log-file=>(cat > {log_path}) {src_path} {dest_path}"
|
||||
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[bash, '-c', bash_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
rmtree(base)
|
||||
test_fail('process substitution test timed out')
|
||||
|
||||
ctx = f'rc={proc.returncode}, stderr={proc.stderr.strip()!r}'
|
||||
|
||||
if proc.returncode != 0:
|
||||
rmtree(base)
|
||||
test_fail(f'rsync crashed writing to a pseudo-path log pipe ({ctx})')
|
||||
|
||||
if not (dest / 'transfer_me.txt').is_file():
|
||||
rmtree(base)
|
||||
test_fail(f'rsync failed to transfer the allowed file ({ctx})')
|
||||
|
||||
if not log_out.exists() or log_out.stat().st_size == 0:
|
||||
rmtree(base)
|
||||
test_fail(f'rsync survived, but failed to write data to the log pipe ({ctx})')
|
||||
|
||||
log_data = log_out.read_text()
|
||||
if "transfer_me.txt" not in log_data:
|
||||
rmtree(base)
|
||||
test_fail(f'Log pipe received data, but is missing expected output: {log_data[:100]}')
|
||||
|
||||
print('Test 1 Passed: rsync successfully wrote logs to a process substitution pseudo-path')
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TEST 2: Confined Root (Should Reject Pseudo-path)
|
||||
# -------------------------------------------------------------------------
|
||||
bash_script_confined = f"{rsync_base_cmd} --confine-root={dest_path} -v --log-file=>(cat > {log_path_confined}) {src_path} {dest_path}"
|
||||
|
||||
try:
|
||||
proc_confined = subprocess.run(
|
||||
[bash, '-c', bash_script_confined],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
rmtree(base)
|
||||
test_fail('confined process substitution test timed out')
|
||||
|
||||
ctx_confined = f'rc={proc_confined.returncode}, stderr={proc_confined.stderr.strip()!r}'
|
||||
|
||||
# Rsync considers log-file failure a warning, so it still exits 0.
|
||||
stderr_lower = proc_confined.stderr.lower()
|
||||
if "no such file or directory" in stderr_lower and "failed to open" in stderr_lower:
|
||||
if log_out_confined.exists() and log_out_confined.stat().st_size > 0:
|
||||
rmtree(base)
|
||||
test_fail(f'rsync printed an error but still wrote the confined log! ({ctx_confined})')
|
||||
print('Test 2 Passed: rsync correctly rejected the pseudo-path when confine_root was active')
|
||||
else:
|
||||
rmtree(base)
|
||||
test_fail(f'rsync failed to reject the pseudo-path or had an unexpected error ({ctx_confined})')
|
||||
|
||||
# A pseudo-path is valid only when its descriptor number is the final component.
|
||||
rmtree(dest)
|
||||
makepath(dest)
|
||||
trailing_script = (
|
||||
f'pipe_path=<(printf "transfer_me.txt\\n"); '
|
||||
f'{rsync_base_cmd} --exclude-from="$pipe_path/trailing" {src_path} {dest_path}'
|
||||
)
|
||||
try:
|
||||
proc_trailing = subprocess.run(
|
||||
[bash, '-c', trailing_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
rmtree(base)
|
||||
test_fail('trailing-component pseudo-path test timed out')
|
||||
|
||||
if proc_trailing.returncode == 0:
|
||||
rmtree(base)
|
||||
test_fail('/dev/fd/N/trailing unexpectedly opened descriptor N')
|
||||
if (dest / 'transfer_me.txt').exists():
|
||||
rmtree(base)
|
||||
test_fail('transfer continued after accepting a trailing pseudo-path component')
|
||||
|
||||
rmtree(base)
|
||||
raise SystemExit(0)
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""--read-batch process substitution /dev/fd/ pipe must not crash with strict file-type checks."""
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import SCRATCHDIR, makepath, rmtree, rsync_argv, test_fail, test_skipped
|
||||
|
||||
# We require bash specifically because standard POSIX /bin/sh does not
|
||||
# guarantee support for <(...) process substitution syntax.
|
||||
if not sys.platform.startswith('linux'):
|
||||
test_skipped('This test requires Linux platform')
|
||||
|
||||
bash = shutil.which('bash')
|
||||
if bash is None:
|
||||
test_skipped('bash is unavailable, cannot test process substitution')
|
||||
|
||||
# Verify the host bash actually supports process substitution
|
||||
probe = subprocess.run(
|
||||
[bash, '-c', 'cat <(echo "probe")'],
|
||||
capture_output=True)
|
||||
|
||||
if probe.returncode != 0:
|
||||
test_skipped('bash process substitution is not supported on this system')
|
||||
|
||||
base = Path(SCRATCHDIR / 'rsync-batch-fifo')
|
||||
src = base / 'src'
|
||||
dest = base / 'dest'
|
||||
batch_file = base / 'update.batch'
|
||||
makepath(src, dest)
|
||||
|
||||
# 1. Create dummy data
|
||||
(src / 'payload.txt').write_text('batch payload data\n')
|
||||
|
||||
# 2. Generate a valid batch file so `cat` actually has a real file to read.
|
||||
# Note: This operation also copies the file to `dest` as a side effect.
|
||||
subprocess.run([*rsync_argv('-a', f'--write-batch={batch_file}'), f'{src}/', f'{dest}/'], check=True)
|
||||
|
||||
# must wipe and recreate the destination directory so the test can
|
||||
# properly prove that --read-batch recreates the files from scratch.
|
||||
rmtree(dest)
|
||||
makepath(dest)
|
||||
|
||||
# 3. Now we can test reading it via bash process substitution
|
||||
rsync_base_cmd = shlex.join(rsync_argv('-a'))
|
||||
batch_path = shlex.quote(str(batch_file))
|
||||
dest_path = shlex.quote(str(dest) + '/')
|
||||
|
||||
# Construct the bash command: rsync -a --read-batch=<(cat /path/to/batch) /dest/
|
||||
bash_script = f"{rsync_base_cmd} --read-batch=<(cat {batch_path}) {dest_path}"
|
||||
|
||||
try:
|
||||
proc_read = subprocess.run(
|
||||
[bash, '-c', bash_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
rmtree(base)
|
||||
test_fail('process substitution batch test timed out')
|
||||
|
||||
ctx = f'rc={proc_read.returncode}, stderr={proc_read.stderr.strip()!r}'
|
||||
|
||||
# Evaluate result against the strict S_ISREG check bug
|
||||
if proc_read.returncode != 0:
|
||||
rmtree(base)
|
||||
test_fail(f'rsync crashed reading batch file from pipe ({ctx})')
|
||||
|
||||
if not (dest / 'payload.txt').is_file():
|
||||
rmtree(base)
|
||||
test_fail(f'rsync exited successfully but payload is missing in target ({ctx})')
|
||||
|
||||
rmtree(base)
|
||||
print('rsync successfully parsed batch stream via process substitution pseudo-path')
|
||||
raise SystemExit(0)
|
||||
@@ -31,14 +31,6 @@ trap_outside.mkdir(parents=True)
|
||||
(mod / 'top-old').write_text("top-old\n")
|
||||
os.symlink('../trap', mod / 'escape_link')
|
||||
|
||||
# Per-operand policy split (PR #30): a caller-owned symlink that ESCAPES the tree
|
||||
# (-> ../trap). The ownership walk (operator) follows the operator's own symlink;
|
||||
# the secure receiver resolve (transfer, flag 0) refuses it. The harness
|
||||
# PS-refuse/PS-follow checks rename to oplink/ under each new-side policy.
|
||||
os.symlink('../trap', mod / 'oplink')
|
||||
for n in ('perside-src2', 'perside-src3'):
|
||||
(mod / 'realdir' / n).write_text(n + "\n")
|
||||
|
||||
proc = subprocess.run([str(TOOLDIR / 't_rename_secure'), str(mod)])
|
||||
if proc.returncode == 77:
|
||||
test_skipped("t_rename_secure skipped")
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A confined fd pin must remain usable inside a Linux user namespace."""
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import makepath, rmtree, rsync_argv, test_fail, test_skipped
|
||||
|
||||
|
||||
if not sys.platform.startswith('linux'):
|
||||
test_skipped('rrsync-userns-procfs is Linux-specific')
|
||||
|
||||
if not os.environ.get('RSYNC_USERNS_PROCFS'):
|
||||
unshare = shutil.which('unshare')
|
||||
if unshare is None:
|
||||
test_skipped('unshare is unavailable')
|
||||
env = os.environ.copy()
|
||||
env['RSYNC_USERNS_PROCFS'] = '1'
|
||||
launch_dir = Path(tempfile.mkdtemp(prefix='rsync-userns-launch-'))
|
||||
launch_dir.chmod(0o755)
|
||||
testdir = Path(__file__).resolve().parent
|
||||
child_test = launch_dir / Path(__file__).name
|
||||
for source in (Path(__file__), testdir / 'rsyncfns.py',
|
||||
testdir / 'exitcodes.py'):
|
||||
shutil.copy2(source, launch_dir / source.name)
|
||||
rsync_cmd = shlex.split(env['RSYNC'])
|
||||
for i, arg in enumerate(rsync_cmd):
|
||||
if Path(arg).name in ('rsync', 'rsync.exe') and Path(arg).is_file():
|
||||
staged_rsync = launch_dir / Path(arg).name
|
||||
shutil.copy2(arg, staged_rsync)
|
||||
staged_rsync.chmod(0o755)
|
||||
rsync_cmd[i] = str(staged_rsync)
|
||||
break
|
||||
else:
|
||||
rmtree(launch_dir)
|
||||
test_fail(f'cannot locate the rsync executable in {env["RSYNC"]!r}')
|
||||
env['RSYNC'] = shlex.join(rsync_cmd)
|
||||
launcher = []
|
||||
if os.geteuid() == 0:
|
||||
setpriv = shutil.which('setpriv')
|
||||
if setpriv is None:
|
||||
test_skipped('setpriv is unavailable for the root-run testsuite')
|
||||
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
|
||||
unshare_argv = [unshare, '--user', '--map-root-user', '--mount', '--pid',
|
||||
'--fork', '--mount-proc']
|
||||
probe = subprocess.run(
|
||||
launcher + unshare_argv + ['true'],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if probe.returncode != 0:
|
||||
rmtree(launch_dir)
|
||||
print(f'user namespaces unavailable (rc={probe.returncode})')
|
||||
raise SystemExit(0)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
launcher + unshare_argv
|
||||
+ [sys.executable, str(child_test)],
|
||||
env=env,
|
||||
timeout=30,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
test_fail('user-namespace regression test timed out')
|
||||
finally:
|
||||
rmtree(launch_dir)
|
||||
if proc.returncode != 0:
|
||||
test_fail(f'user-namespace regression test failed (rc={proc.returncode})')
|
||||
print('rrsync fd pin works inside a user namespace')
|
||||
raise SystemExit(0)
|
||||
|
||||
proc_uid = os.lstat('/proc/self').st_uid
|
||||
if proc_uid in (0, os.geteuid()):
|
||||
test_skipped('/proc/self does not expose an overflow uid in this namespace')
|
||||
|
||||
base = Path(tempfile.mkdtemp(prefix='rsync-userns-procfs-'))
|
||||
src = base / 'src'
|
||||
dest = base / 'dest'
|
||||
outside = base / 'outside'
|
||||
makepath(src, dest, outside)
|
||||
(src / 'file').write_text('content\n')
|
||||
|
||||
fd_roots = ['/proc/self/fd']
|
||||
if Path('/dev/fd').exists():
|
||||
fd_roots.append('/dev/fd')
|
||||
|
||||
dest_fd = os.open(dest, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
for index, fd_root in enumerate(fd_roots):
|
||||
log_file = dest / f'rsync-{index}.log'
|
||||
proc = subprocess.run(
|
||||
rsync_argv('-a', f'--confine-root={dest}',
|
||||
f'--log-file={fd_root}/{dest_fd}/{log_file.name}',
|
||||
str(src) + '/', str(dest) + '/'),
|
||||
pass_fds=(dest_fd,),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
ctx = (f'fd_root={fd_root!r}, rc={proc.returncode}, '
|
||||
f'stderr={proc.stderr.strip()[:300]!r}')
|
||||
if proc.returncode != 0:
|
||||
test_fail(f'confined transfer through an fd pin failed ({ctx})')
|
||||
if not log_file.is_file():
|
||||
test_fail(f'confined log path through an fd pin was rejected ({ctx})')
|
||||
if (dest / 'file').read_text() != 'content\n':
|
||||
test_fail(f'confined transfer did not deliver the file ({ctx})')
|
||||
finally:
|
||||
os.close(dest_fd)
|
||||
|
||||
outside_list = outside / 'files-from'
|
||||
outside_list.write_text('file\n')
|
||||
for fd_root in fd_roots:
|
||||
outside_fd = os.open(outside_list, os.O_RDONLY)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
rsync_argv('-a', f'--confine-root={dest}',
|
||||
f'--files-from={fd_root}/{outside_fd}',
|
||||
str(src) + '/', str(dest) + '/'),
|
||||
pass_fds=(outside_fd,),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
finally:
|
||||
os.close(outside_fd)
|
||||
if proc.returncode == 0 or 'failed to open files-from file' not in proc.stderr:
|
||||
test_fail(f'outside {fd_root} pin was not observably refused: '
|
||||
f'rc={proc.returncode}, stderr={proc.stderr!r}')
|
||||
rmtree(base)
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
# rsync-ssl only recognized --type=SSL_TYPE as the FIRST argument, so
|
||||
# "rsync-ssl --dry-run --type=stunnel host::mod" passed the option through to
|
||||
# the underlying rsync, which rejected it with "--type=stunnel: unknown option".
|
||||
# Fix: scan the whole argument list for --type=..., export RSYNC_SSL_TYPE, and
|
||||
# drop the option before handing the remaining args to rsync.
|
||||
# A `--` arg stops the wrapper-option scan: `--` and everything after it are
|
||||
# passed through to rsync verbatim, so an operand such as `--type=stunnel`
|
||||
# that was protected from option parsing is not consumed by the wrapper.
|
||||
#
|
||||
# A fake rsync in PATH records the args it receives and the RSYNC_SSL_TYPE it
|
||||
# observes; rsync-ssl is run in its normal (non-HELPER) mode with --type= in
|
||||
# various positions. The recorded args must contain every other option but
|
||||
# never a --type= token, RSYNC_SSL_TYPE must match what the wrapper consumed,
|
||||
# and rsync-ssl must exit successfully.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from rsyncfns import SCRATCHDIR, SRCDIR, rmtree, test_fail
|
||||
|
||||
base = SCRATCHDIR / 'rsync-ssl-type-opt'
|
||||
rmtree(base)
|
||||
base.mkdir(parents=True)
|
||||
|
||||
args_capture = base / 'rsync_args'
|
||||
type_capture = base / 'rsync_ssl_type'
|
||||
fakebin = base / 'bin'
|
||||
fakebin.mkdir(parents=True)
|
||||
fake_rsync = fakebin / 'rsync'
|
||||
fake_rsync.write_text(
|
||||
f"#!/usr/bin/env bash\n"
|
||||
f"printf '%s\\n' \"$@\" > {args_capture}\n"
|
||||
f"printf '%s\\n' \"${{RSYNC_SSL_TYPE-UNSET}}\" > {type_capture}\n"
|
||||
f"exit 0\n")
|
||||
fake_rsync.chmod(0o755)
|
||||
|
||||
env = {**os.environ, 'PATH': str(fakebin) + os.pathsep + os.environ.get('PATH', '')}
|
||||
for v in ('RSYNC_SSL_TYPE', 'RSYNC_SSL_OPENSSL', 'RSYNC_SSL_STUNNEL'):
|
||||
env.pop(v, None)
|
||||
|
||||
|
||||
def run(args, expect_type):
|
||||
for capture in (args_capture, type_capture):
|
||||
if capture.exists():
|
||||
capture.unlink()
|
||||
proc = subprocess.run(['bash', str(SRCDIR / 'rsync-ssl')] + args, env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if proc.returncode != 0:
|
||||
test_fail(f"rsync-ssl exited {proc.returncode} for args {args!r}:\n{proc.stdout}")
|
||||
got_args = args_capture.read_text().splitlines() if args_capture.exists() else []
|
||||
got_type = type_capture.read_text().strip() if type_capture.exists() else 'UNSET'
|
||||
if got_type != expect_type:
|
||||
test_fail(f"RSYNC_SSL_TYPE is {got_type!r}, expected {expect_type!r} "
|
||||
f"for args {args!r}:\n{proc.stdout}")
|
||||
return got_args
|
||||
|
||||
|
||||
# --- The reported failure: --type= in the middle of the rsync args.
|
||||
got = run(['--dry-run', '--type=stunnel', 'host::mod'], 'stunnel')
|
||||
if any(a.startswith('--type=') for a in got):
|
||||
test_fail(f"--type= was passed through to rsync instead of being consumed:\n{got}")
|
||||
for want in ('--dry-run', 'host::mod'):
|
||||
if want not in got:
|
||||
test_fail(f"missing rsync arg {want!r} after --type= handling:\n{got}")
|
||||
if not any(a.startswith('--rsh=') for a in got):
|
||||
test_fail(f"missing the --rsh= helper option:\n{got}")
|
||||
|
||||
# --- First, last, and no --type= keep working.
|
||||
for pos_args, expect_type in ((['--type=stunnel', '--dry-run', 'host::mod'], 'stunnel'),
|
||||
(['-av', 'host::mod', '--type=openssl'], 'openssl'),
|
||||
(['-av', 'host::mod'], 'UNSET')):
|
||||
got = run(pos_args, expect_type)
|
||||
if any(a.startswith('--type=') for a in got):
|
||||
test_fail(f"--type= was passed through for args {pos_args!r}:\n{got}")
|
||||
for want in pos_args:
|
||||
if want.startswith('--type='):
|
||||
continue
|
||||
if want not in got:
|
||||
test_fail(f"missing rsync arg {want!r} for args {pos_args!r}:\n{got}")
|
||||
|
||||
# --- `--` stops the wrapper-option scan: the protected operand is preserved.
|
||||
rsh_arg = "--rsh='{}' --HELPER".format(SRCDIR / 'rsync-ssl')
|
||||
got = run(['--', '--type=stunnel', 'host::mod'], 'UNSET')
|
||||
if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:
|
||||
test_fail(f"args after -- must be preserved verbatim (no --type= consumed):\n{got}")
|
||||
|
||||
# --- A wrapper option before `--` is still consumed; the protected one is not.
|
||||
got = run(['--type=openssl', '--', '--type=stunnel', 'host::mod'], 'openssl')
|
||||
if got != [rsh_arg, '--', '--type=stunnel', 'host::mod']:
|
||||
test_fail(f"--type= before -- is consumed, args after -- are preserved:\n{got}")
|
||||
|
||||
print("rsync-ssl-type-option: --type=SSL_TYPE is consumed in any argument "
|
||||
"position (until a -- stops the wrapper-option scan), exported as "
|
||||
"RSYNC_SSL_TYPE, and rsync-ssl exits successfully")
|
||||
@@ -35,7 +35,6 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
from exitcodes import Exit # re-exported: tests may `from rsyncfns import Exit`
|
||||
@@ -2194,12 +2193,7 @@ def setup_chroot_inner(name):
|
||||
('mod', {'path': str(outer) + '/./inner', 'read only': 'no',
|
||||
'use chroot': 'yes', 'munge symlinks': 'no'}),
|
||||
], name=f'{name}.conf')
|
||||
# crc32, not hash(): str hash is per-process randomized (PYTHONHASHSEED),
|
||||
# so the port would wander run to run -- and the old 12940+200 span reached
|
||||
# into 13000+, where desktop bloatware (e.g. ASUS Armoury Crate on the
|
||||
# Cygwin CI host) parks localhost listeners. 12800-12859 is otherwise
|
||||
# unused by the suite.
|
||||
url = start_test_daemon(conf, 12800 + (zlib.crc32(name.encode()) % 60))
|
||||
url = start_test_daemon(conf, 12940 + (abs(hash(name)) % 200))
|
||||
return base, inner, outside, src, url
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The receiver must traverse a searchable but unreadable destination parent.
|
||||
|
||||
Android exposes /sdcard through such a path, so the race-safe destination walk
|
||||
must use directory descriptors that require search permission only.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import SCRATCHDIR, rmtree, rsync_argv, test_fail, test_skipped
|
||||
|
||||
if not sys.platform.startswith('linux'):
|
||||
test_skipped('search-only-destination is Linux-specific')
|
||||
|
||||
launcher = []
|
||||
if os.geteuid() == 0:
|
||||
setpriv = shutil.which('setpriv')
|
||||
if setpriv is None:
|
||||
test_skipped('setpriv is unavailable for the root-run testsuite')
|
||||
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
|
||||
|
||||
external_base = os.geteuid() == 0
|
||||
if external_base:
|
||||
base = Path(tempfile.mkdtemp(prefix='rsync-search-only-'))
|
||||
base.chmod(0o755)
|
||||
else:
|
||||
base = SCRATCHDIR / 'search-only-destination'
|
||||
src = base / 'src'
|
||||
parent = base / 'search-only'
|
||||
dest = parent / 'dest'
|
||||
rmtree(base)
|
||||
src.mkdir(parents=True)
|
||||
dest.mkdir(parents=True)
|
||||
(src / 'probe').write_text('search-only destination\n')
|
||||
|
||||
if os.geteuid() == 0:
|
||||
for path in (src, src / 'probe', dest):
|
||||
os.chown(path, 65534, 65534)
|
||||
|
||||
try:
|
||||
parent.chmod(0o111)
|
||||
try:
|
||||
probe = subprocess.run(
|
||||
launcher + ['test', '-r', str(parent)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if probe.returncode == 0:
|
||||
test_skipped('filesystem does not enforce the search-only test mode')
|
||||
if probe.returncode != 1:
|
||||
test_fail(f'search-only permission probe failed with exit {probe.returncode}')
|
||||
|
||||
proc = subprocess.run(
|
||||
launcher + rsync_argv('-a', f'{src}/', f'{dest}/'),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
finally:
|
||||
parent.chmod(0o755)
|
||||
copied = (dest / 'probe').read_text() if (dest / 'probe').is_file() else None
|
||||
finally:
|
||||
if external_base:
|
||||
rmtree(base)
|
||||
|
||||
if proc.returncode != 0:
|
||||
test_fail(
|
||||
'receiver could not enter a destination below a searchable, unreadable '
|
||||
f'parent (exit {proc.returncode}): {proc.stderr.strip()}'
|
||||
)
|
||||
if copied != 'search-only destination\n':
|
||||
test_fail('receiver did not copy into the search-only destination')
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Known-name operations must not require permission to list parent dirs.
|
||||
|
||||
The confined resolver holds directory descriptors to prevent symlink races.
|
||||
On Linux, those traversal and *at() anchor descriptors can use O_PATH: opening
|
||||
a known file beneath a searchable directory, or creating one beneath a
|
||||
writable/searchable directory, does not require directory read permission.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from rsyncfns import (
|
||||
SCRATCHDIR, forced_protocol, rmtree, rsync_argv, test_fail, test_skipped,
|
||||
)
|
||||
|
||||
if not sys.platform.startswith('linux'):
|
||||
test_skipped('search-only held-dirfd coverage is Linux-specific')
|
||||
|
||||
launcher = []
|
||||
if os.geteuid() == 0:
|
||||
setpriv = shutil.which('setpriv')
|
||||
if setpriv is None:
|
||||
test_skipped('setpriv is unavailable for the root-run testsuite')
|
||||
launcher = [setpriv, '--reuid=65534', '--regid=65534', '--clear-groups']
|
||||
|
||||
external_base = os.geteuid() == 0
|
||||
base = (
|
||||
Path(tempfile.mkdtemp(prefix='rsync-search-only-held-dirfd-'))
|
||||
if external_base
|
||||
else SCRATCHDIR / 'search-only-held-dirfd'
|
||||
)
|
||||
rmtree(base)
|
||||
|
||||
src = base / 'src'
|
||||
xonly = src / 'xonly'
|
||||
readable = xonly / 'readable'
|
||||
nested_src = src / 'nested'
|
||||
exact_dest = base / 'exact-dest'
|
||||
tree_dest = base / 'tree-dest'
|
||||
unreadable_dest = base / 'unreadable-dest'
|
||||
write_only_dest = base / 'write-only-dest'
|
||||
nested_dest = base / 'nested-dest'
|
||||
nested_parent = nested_dest / 'nested'
|
||||
for path in (
|
||||
readable,
|
||||
nested_src,
|
||||
exact_dest,
|
||||
tree_dest,
|
||||
unreadable_dest,
|
||||
write_only_dest,
|
||||
nested_parent,
|
||||
):
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(xonly / 'exact').write_text('known file beneath search-only parent\n')
|
||||
(readable / 'nested').write_text('enumerated below search-only ancestor\n')
|
||||
incoming = src / 'incoming'
|
||||
incoming.write_text('created beneath write-search-only destination\n')
|
||||
(nested_src / 'known').write_text(
|
||||
'created beneath nested write-search-only parent\n'
|
||||
)
|
||||
|
||||
if os.geteuid() == 0:
|
||||
for root, dirs, files in os.walk(base):
|
||||
os.chown(root, 65534, 65534)
|
||||
for name in dirs + files:
|
||||
os.chown(Path(root) / name, 65534, 65534)
|
||||
|
||||
|
||||
def permission_probe(path, flag, expected, label):
|
||||
proc = subprocess.run(
|
||||
launcher + ['test', flag, str(path)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if proc.returncode != expected:
|
||||
test_skipped(
|
||||
f'filesystem does not enforce {label}: test {flag} returned '
|
||||
f'{proc.returncode}, expected {expected}'
|
||||
)
|
||||
|
||||
|
||||
failures = []
|
||||
try:
|
||||
xonly.chmod(0o111)
|
||||
write_only_dest.chmod(0o333)
|
||||
nested_parent.chmod(0o333)
|
||||
|
||||
permission_probe(xonly, '-r', 1, 'search-only mode')
|
||||
permission_probe(xonly, '-x', 0, 'search-only mode')
|
||||
permission_probe(write_only_dest, '-r', 1, 'write-search-only mode')
|
||||
permission_probe(write_only_dest, '-w', 0, 'write-search-only mode')
|
||||
permission_probe(write_only_dest, '-x', 0, 'write-search-only mode')
|
||||
permission_probe(nested_parent, '-r', 1, 'nested write-search-only mode')
|
||||
permission_probe(nested_parent, '-w', 0, 'nested write-search-only mode')
|
||||
permission_probe(nested_parent, '-x', 0, 'nested write-search-only mode')
|
||||
|
||||
# Keep received implied dirs usable on systems without a safe fchmodat2.
|
||||
# The source remains mode 0111, so sender traversal coverage is unchanged.
|
||||
exact = subprocess.run(
|
||||
launcher + rsync_argv(
|
||||
'-aR', '--chmod=Du+rw', 'xonly/exact', f'{exact_dest}/',
|
||||
),
|
||||
cwd=src,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
exact_path = exact_dest / 'xonly' / 'exact'
|
||||
exact_content = exact_path.read_text() if exact_path.is_file() else None
|
||||
if exact.returncode != 0 or exact_content != (
|
||||
'known file beneath search-only parent\n'
|
||||
):
|
||||
failures.append(
|
||||
'exact -R source beneath mode 0111 failed: '
|
||||
f'rc={exact.returncode}, stderr={exact.stderr.strip()!r}, '
|
||||
f'content={exact_content!r}'
|
||||
)
|
||||
|
||||
tree = subprocess.run(
|
||||
launcher + rsync_argv(
|
||||
'-aR', '--chmod=Du+rw', 'xonly/readable/', f'{tree_dest}/',
|
||||
),
|
||||
cwd=src,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
tree_path = tree_dest / 'xonly' / 'readable' / 'nested'
|
||||
tree_content = tree_path.read_text() if tree_path.is_file() else None
|
||||
if tree.returncode != 0 or tree_content != (
|
||||
'enumerated below search-only ancestor\n'
|
||||
):
|
||||
failures.append(
|
||||
'readable directory beneath mode 0111 ancestor failed: '
|
||||
f'rc={tree.returncode}, stderr={tree.stderr.strip()!r}, '
|
||||
f'content={tree_content!r}'
|
||||
)
|
||||
|
||||
unreadable = subprocess.run(
|
||||
launcher + rsync_argv(
|
||||
'-a', 'xonly/', f'{unreadable_dest}/',
|
||||
),
|
||||
cwd=src,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
if unreadable.returncode == 0:
|
||||
failures.append(
|
||||
'mode 0111 source directory was enumerable without read permission'
|
||||
)
|
||||
|
||||
receiver = subprocess.run(
|
||||
launcher + rsync_argv(
|
||||
'-t', str(incoming), f'{write_only_dest}/',
|
||||
),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
received = write_only_dest / 'incoming'
|
||||
received_content = received.read_text() if received.is_file() else None
|
||||
if receiver.returncode != 0 or received_content != (
|
||||
'created beneath write-search-only destination\n'
|
||||
):
|
||||
failures.append(
|
||||
'known-file creation beneath mode 0333 destination failed: '
|
||||
f'rc={receiver.returncode}, stderr={receiver.stderr.strip()!r}, '
|
||||
f'content={received_content!r}'
|
||||
)
|
||||
|
||||
# Protocol 29 rejects this nested -R shape before the resolver is reached.
|
||||
proto = forced_protocol()
|
||||
if proto is None or proto >= 30:
|
||||
nested_receiver = subprocess.run(
|
||||
launcher + rsync_argv(
|
||||
'-tR', '--no-implied-dirs', 'nested/known', f'{nested_dest}/',
|
||||
),
|
||||
cwd=src,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
nested_received = nested_parent / 'known'
|
||||
nested_content = (
|
||||
nested_received.read_text() if nested_received.is_file() else None
|
||||
)
|
||||
if nested_receiver.returncode != 0 or nested_content != (
|
||||
'created beneath nested write-search-only parent\n'
|
||||
):
|
||||
failures.append(
|
||||
'known-file creation beneath nested mode 0333 destination '
|
||||
f'failed: rc={nested_receiver.returncode}, '
|
||||
f'stderr={nested_receiver.stderr.strip()!r}, '
|
||||
f'content={nested_content!r}'
|
||||
)
|
||||
finally:
|
||||
xonly.chmod(0o755)
|
||||
write_only_dest.chmod(0o755)
|
||||
nested_parent.chmod(0o755)
|
||||
for dest in (exact_dest, tree_dest):
|
||||
copied_xonly = dest / 'xonly'
|
||||
if copied_xonly.is_dir():
|
||||
copied_xonly.chmod(0o755)
|
||||
if external_base:
|
||||
rmtree(base)
|
||||
|
||||
if failures:
|
||||
test_fail('\n'.join(failures))
|
||||
@@ -48,6 +48,10 @@ try:
|
||||
os.utime(inside_file, (st.st_atime, st.st_mtime))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except PermissionError:
|
||||
# Cygwin can report EACCES while the flipper swaps this path.
|
||||
if not _CYGWIN:
|
||||
raise
|
||||
subprocess.run(
|
||||
rsync_argv('-a', '--remove-source-files', f'{url}src/real/file', str(dest) + '/'),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)
|
||||
|
||||
@@ -23,6 +23,7 @@ different tests merge cleanly.
|
||||
| file | contents |
|
||||
| --- | --- |
|
||||
| `common.txt` | skipped on every platform that runs the oracle — mostly `require_tcp` / `require_asan` tests, which the default stdio-pipe `make check` cannot satisfy |
|
||||
| `almalinux-8.txt` | AlmaLinux 8 container additions |
|
||||
| `linux.txt` | Linux-only additions |
|
||||
| `macos.txt` | macOS-only additions |
|
||||
| `cygwin.txt` | Cygwin-only additions |
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Tests expected to SKIP. One name per line, '#' starts a comment; the file
|
||||
# must stay sorted and duplicate-free (runtests.py enforces both). Referenced
|
||||
# from a workflow as RSYNC_EXPECT_SKIPPED=@testsuite/skiplist/<file>[,@...].
|
||||
# See testsuite/skiplist/README.md.
|
||||
#
|
||||
# AlmaLinux 8 container additions to common.txt and linux.txt.
|
||||
|
||||
pseudo-paths # Bash process substitution is unavailable in the AlmaLinux 8 container
|
||||
read-batch-pipe
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
checksum-zero-blocklen # the pure-Python receiver needs a real TCP socket; run with --use-tcp
|
||||
chroot-basis-forge-inner-module # the pure-Python sender needs a real TCP daemon; run with --use-tcp
|
||||
daemon-access-idn # hosts allow/deny hostname matching needs a real TCP peer
|
||||
daemon-access-ip # hosts allow/deny address matching needs a real TCP peer
|
||||
daemon-argv-limit # raw malicious daemon client needs a real TCP daemon; run with --use-tcp
|
||||
daemon-chroot # daemon chroot path needs the real start_daemon socket flow
|
||||
|
||||
@@ -25,7 +25,6 @@ copy-xattrs-symlink-race
|
||||
daemon-auth-group
|
||||
daemon-chroot-munge-default
|
||||
daemon-config-symlink
|
||||
daemon-max-alloc-zero
|
||||
daemon-module-chdir-symlink
|
||||
daemon-module-private-parent
|
||||
daemon-secrets-file-symlink
|
||||
@@ -45,7 +44,6 @@ msg-io-timeout-overflow
|
||||
nondaemon-symlink-race
|
||||
nonroot-restrictive-perms
|
||||
open-noatime
|
||||
operator-path-backup-chown
|
||||
operator-path-backup-rmdir
|
||||
operator-path-backup-symlink
|
||||
operator-path-insecure-links-daemon
|
||||
@@ -53,15 +51,19 @@ partial-protected-regular-retry-linux
|
||||
partial-protected-regular-retry-policy # deterministic partial EACCES recovery uses dyld interposing
|
||||
password-file-symlink
|
||||
protected-regular
|
||||
pseudo-paths
|
||||
read-batch-pipe
|
||||
rename-mixed-parent-transfer
|
||||
rrsync-sender-leaf-flip
|
||||
rrsync-sender-parent-pin
|
||||
rrsync-symlink
|
||||
rrsync-userns-procfs
|
||||
search-only-destination
|
||||
search-only-held-dirfd
|
||||
sender-remove-source-root-anchor
|
||||
simd-checksum
|
||||
source-change-size-continues
|
||||
symlink-dest-backupdir
|
||||
symlink-exclude-xattr
|
||||
symlink-race-dest
|
||||
symlink-race-relative-dest
|
||||
temp-dir-symlink-injection
|
||||
@@ -14,19 +14,22 @@ backup-crossdev-copy
|
||||
chmod-temp-dir
|
||||
copy-xattrs-symlink-race
|
||||
daemon-auth-group
|
||||
daemon-max-alloc-zero
|
||||
dir-sgid
|
||||
fake-super-acl-xattr
|
||||
link-dest-symlink-enotsup # the ENOTSUP hard-link hook is an LD_PRELOAD, Linux-only
|
||||
open-noatime
|
||||
operator-path-backup-chown
|
||||
partial-protected-regular-retry-linux
|
||||
preallocate
|
||||
protected-regular
|
||||
pseudo-paths # dynamically skips on runners lacking bash process substitution
|
||||
read-batch-pipe
|
||||
readonly-partial-abort-mode-regression #
|
||||
rrsync-sender-leaf-flip
|
||||
rrsync-sender-parent-pin
|
||||
rrsync-symlink
|
||||
rrsync-userns-procfs
|
||||
search-only-destination
|
||||
search-only-held-dirfd
|
||||
sender-remove-source-root-anchor
|
||||
simd-checksum
|
||||
source-change-size-continues
|
||||
|
||||
@@ -50,8 +50,20 @@ dest.mkdir(parents=True)
|
||||
os.symlink(outside, dest / 'sub') # attacker-owned dest component
|
||||
os.lchown(dest / 'sub', ATT_UID, ATT_UID)
|
||||
|
||||
subprocess.run(rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
proc = subprocess.run(
|
||||
rsync_argv('-a', f'{src}/sub/', f'{dest}/sub/'),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if proc.returncode == 0:
|
||||
test_fail("attacker-owned destination symlink was not rejected")
|
||||
if "refusing to follow a symlink owned by an untrusted user" not in proc.stderr:
|
||||
test_fail(
|
||||
"untrusted destination symlink failure omitted the actionable "
|
||||
f"diagnostic: {proc.stderr!r}"
|
||||
)
|
||||
|
||||
escaped = sorted(p.name for p in outside.iterdir())
|
||||
if escaped:
|
||||
|
||||
@@ -160,11 +160,11 @@ static void list_file(const char *fname)
|
||||
char linkbuf[4096];
|
||||
int nsecs;
|
||||
|
||||
if (vfs_lstat(VFS_AT_FDCWD, fname, &buf, VFS_ALLOW_SYMLINK) < 0)
|
||||
if (do_lstat(fname, &buf) < 0)
|
||||
failed("stat", fname);
|
||||
#ifdef SUPPORT_CRTIMES
|
||||
if (display_crtimes && (crtime = vfs_get_create_time(fname, &buf)) == 0)
|
||||
failed("vfs_get_create_time", fname);
|
||||
if (display_crtimes && (crtime = get_create_time(fname, &buf)) == 0)
|
||||
failed("get_create_time", fname);
|
||||
#endif
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (am_root < 0)
|
||||
@@ -188,9 +188,9 @@ static void list_file(const char *fname)
|
||||
buf.st_uid = buf.st_gid = 0;
|
||||
strlcpy(linkbuf, " -> ", sizeof linkbuf);
|
||||
/* const-cast required for silly UNICOS headers */
|
||||
len = vfs_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4);
|
||||
len = do_readlink((char*)fname, linkbuf+4, sizeof linkbuf - 4);
|
||||
if (len == -1)
|
||||
failed("vfs_readlink", fname);
|
||||
failed("do_readlink", fname);
|
||||
else
|
||||
/* it's not nul-terminated */
|
||||
linkbuf[4+len] = 0;
|
||||
|
||||
@@ -156,6 +156,11 @@ static void print_info_flags(enum logcode f)
|
||||
#endif
|
||||
"crtimes",
|
||||
|
||||
#ifndef SUPPORT_IDN
|
||||
"no "
|
||||
#endif
|
||||
"IDN",
|
||||
|
||||
"*Optimizations",
|
||||
|
||||
#ifndef USE_ROLL_SIMD
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#include "ifuncs.h"
|
||||
#include "itypes.h"
|
||||
#include "inums.h"
|
||||
#ifdef SUPPORT_IDN
|
||||
#include <idn2.h>
|
||||
#endif
|
||||
|
||||
extern int dry_run;
|
||||
extern int module_id;
|
||||
@@ -34,6 +37,7 @@ extern int relative_paths;
|
||||
extern int preserve_xattrs;
|
||||
extern int omit_link_times;
|
||||
extern int preallocate_files;
|
||||
extern int operator_path_resolve;
|
||||
extern char *module_dir;
|
||||
extern unsigned int module_dirlen;
|
||||
extern char *partial_dir;
|
||||
@@ -41,6 +45,8 @@ extern filter_rule_list daemon_filter_list;
|
||||
|
||||
int sanitize_paths = 0;
|
||||
|
||||
extern char curr_dir[MAXPATHLEN]; /* defined in syscall.c */
|
||||
extern unsigned int curr_dir_len;
|
||||
int curr_dir_depth; /* This is only set for a sanitizing daemon. */
|
||||
|
||||
/* Set a fd into nonblocking mode. */
|
||||
@@ -130,7 +136,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
|
||||
switch (switch_step) {
|
||||
#ifdef HAVE_SETATTRLIST
|
||||
#include "case_N.h"
|
||||
if (vfs_setattrlist_times(fname, stp) == 0)
|
||||
if (do_setattrlist_times(fname, stp) == 0)
|
||||
break;
|
||||
if (errno != ENOSYS)
|
||||
return -1;
|
||||
@@ -139,7 +145,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
|
||||
|
||||
#ifdef HAVE_UTIMENSAT
|
||||
#include "case_N.h"
|
||||
if (vfs_utimensat_at(fname, stp) == 0)
|
||||
if (do_utimensat_at(fname, stp) == 0)
|
||||
break;
|
||||
if (errno != ENOSYS)
|
||||
return -1;
|
||||
@@ -148,7 +154,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
|
||||
|
||||
#ifdef HAVE_LUTIMES
|
||||
#include "case_N.h"
|
||||
if (vfs_lutimes(fname, stp) == 0)
|
||||
if (do_lutimes(fname, stp) == 0)
|
||||
break;
|
||||
if (errno != ENOSYS)
|
||||
return -1;
|
||||
@@ -165,10 +171,10 @@ int set_times(const char *fname, STRUCT_STAT *stp)
|
||||
|
||||
#include "case_N.h"
|
||||
#ifdef HAVE_UTIMES
|
||||
if (vfs_utimes(fname, stp) == 0)
|
||||
if (do_utimes(fname, stp) == 0)
|
||||
break;
|
||||
#else
|
||||
if (vfs_utime(fname, stp) == 0)
|
||||
if (do_utime(fname, stp) == 0)
|
||||
break;
|
||||
#endif
|
||||
|
||||
@@ -186,7 +192,7 @@ int set_times(const char *fname, STRUCT_STAT *stp)
|
||||
int set_times_at(int dfd, const char *name, STRUCT_STAT *stp)
|
||||
{
|
||||
#if defined HAVE_UTIMENSAT && !defined HAVE_SETATTRLIST
|
||||
int r = vfs_utimensat_atfd(dfd, name, stp);
|
||||
int r = do_utimensat_atfd(dfd, name, stp);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
if (errno == ENOSYS)
|
||||
@@ -198,6 +204,91 @@ int set_times_at(int dfd, const char *name, STRUCT_STAT *stp)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Create any necessary directories in fname. Any missing directories are
|
||||
* created with default permissions. Returns < 0 on error, or the number
|
||||
* of directories created. */
|
||||
int make_path(char *fname, int flags)
|
||||
{
|
||||
char *end, *p;
|
||||
int ret = 0;
|
||||
|
||||
if (flags & MKP_SKIP_SLASH) {
|
||||
while (*fname == '/')
|
||||
fname++;
|
||||
}
|
||||
|
||||
while (*fname == '.' && fname[1] == '/')
|
||||
fname += 2;
|
||||
|
||||
if (flags & MKP_DROP_NAME) {
|
||||
end = strrchr(fname, '/');
|
||||
if (!end || end == fname)
|
||||
return 0;
|
||||
*end = '\0';
|
||||
} else
|
||||
end = fname + strlen(fname);
|
||||
|
||||
/* Try to find an existing dir, starting from the deepest dir. */
|
||||
for (p = end; ; ) {
|
||||
if (dry_run) {
|
||||
STRUCT_STAT st;
|
||||
if (do_stat(fname, &st) == 0) {
|
||||
if (S_ISDIR(st.st_mode))
|
||||
errno = EEXIST;
|
||||
else
|
||||
errno = ENOTDIR;
|
||||
}
|
||||
} else if (do_mkdir_at(fname, ACCESSPERMS) == 0) {
|
||||
ret++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (errno != ENOENT) {
|
||||
STRUCT_STAT st;
|
||||
if (errno != EEXIST || (do_stat(fname, &st) == 0 && !S_ISDIR(st.st_mode)))
|
||||
ret = -ret - 1;
|
||||
break;
|
||||
}
|
||||
while (1) {
|
||||
if (p == fname) {
|
||||
/* We got a relative path that doesn't exist, so assume that '.'
|
||||
* is there and just break out and create the whole thing. */
|
||||
p = NULL;
|
||||
goto double_break;
|
||||
}
|
||||
if (*--p == '/') {
|
||||
if (p == fname) {
|
||||
/* We reached the "/" dir, which we assume is there. */
|
||||
goto double_break;
|
||||
}
|
||||
*p = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
double_break:
|
||||
|
||||
/* Make all the dirs that we didn't find on the way here. */
|
||||
while (p != end) {
|
||||
if (p)
|
||||
*p = '/';
|
||||
else
|
||||
p = fname;
|
||||
p += strlen(p);
|
||||
if (ret < 0) /* Skip mkdir on error, but keep restoring the path. */
|
||||
continue;
|
||||
if (do_mkdir_at(fname, ACCESSPERMS) < 0)
|
||||
ret = -ret - 1;
|
||||
else
|
||||
ret++;
|
||||
}
|
||||
|
||||
if (flags & MKP_DROP_NAME)
|
||||
*end = '/';
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write @p len bytes at @p ptr to descriptor @p desc, retrying if
|
||||
* interrupted.
|
||||
@@ -227,6 +318,353 @@ int full_write(int desc, const char *ptr, size_t len)
|
||||
return total_written;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read @p len bytes at @p ptr from descriptor @p desc, retrying if
|
||||
* interrupted.
|
||||
*
|
||||
* @retval >0 the actual number of bytes read
|
||||
*
|
||||
* @retval 0 for EOF
|
||||
*
|
||||
* @retval <0 for an error.
|
||||
*
|
||||
* Derived from GNU C's cccp.c. */
|
||||
static int safe_read(int desc, char *ptr, size_t len)
|
||||
{
|
||||
int n_chars;
|
||||
|
||||
if (len == 0)
|
||||
return len;
|
||||
|
||||
do {
|
||||
n_chars = read(desc, ptr, len);
|
||||
} while (n_chars < 0 && errno == EINTR);
|
||||
|
||||
return n_chars;
|
||||
}
|
||||
|
||||
/* Remove existing file @dest and reopen, creating a new file with @mode */
|
||||
static int unlink_and_reopen(const char *dest, mode_t mode)
|
||||
{
|
||||
int ofd;
|
||||
|
||||
if (robust_unlink(dest) && errno != ENOENT) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs)
|
||||
mode |= S_IWUSR;
|
||||
#endif
|
||||
mode &= INITACCESSPERMS;
|
||||
/* Use do_open_at so the create/truncate goes through a secure
|
||||
* parent dirfd in the daemon-no-chroot deployment. Otherwise
|
||||
* an attacker could swap a parent component with a symlink in
|
||||
* the window between robust_unlink (which uses do_unlink_at,
|
||||
* already secure) and the create here, and redirect the new
|
||||
* file outside the module. */
|
||||
if ((ofd = do_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode)) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
return ofd;
|
||||
}
|
||||
|
||||
/* Copy contents of file @source to file @dest with mode @mode.
|
||||
*
|
||||
* If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new
|
||||
* file with name @dest.
|
||||
*
|
||||
* Otherwise, copy_file writes to and closes the provided file
|
||||
* descriptor.
|
||||
*
|
||||
* In either case, if --xattrs are being preserved, the dest file will
|
||||
* have its xattrs set from the source file.
|
||||
*
|
||||
* This is used in conjunction with the --temp-dir, --backup, and
|
||||
* --copy-dest options. */
|
||||
int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode)
|
||||
{
|
||||
int ifd, ofd;
|
||||
char buf[1024 * 8];
|
||||
int len; /* Number of bytes read into `buf'. */
|
||||
OFF_T prealloc_len = 0, offset = 0;
|
||||
|
||||
/* For any hardened (non-chrooted) receiver, route the source open through
|
||||
* secure_relative_open so a parent-symlink on the source path (e.g.
|
||||
* --copy-dest=cd where cd is a symlink to an outside directory) cannot
|
||||
* redirect the read to a file the attacker should not see. Plain
|
||||
* do_open_nofollow only refuses a final-component symlink; parents are
|
||||
* still followed. An ABSOLUTE source is an operator basis (e.g. an absolute
|
||||
* --copy-dest): confine its parents via the ownership walk -- a foreign-owned
|
||||
* parent symlink is refused, the operator's own dirs/uid0/euid symlinks
|
||||
* followed -- so a flipped parent can't redirect the basis read out of tree.
|
||||
* operator_path_resolve is set only across the walk (so module-exclude is
|
||||
* enforced) and restored, leaving the caller's value for the dest side -- this
|
||||
* is why confining the source here does not re-open the copy_xattrs dest
|
||||
* race the way wrapping the whole copy_altdest_file would. */
|
||||
if (secure_relpath_active() && source && *source && source[0] != '/')
|
||||
ifd = secure_relative_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
else if (secure_relpath_active() && source && source[0] == '/'
|
||||
&& !symlink_optout_allowed()) {
|
||||
int save = operator_path_resolve, dfd, e;
|
||||
const char *leaf;
|
||||
operator_path_resolve = 1;
|
||||
dfd = owner_walk_parent(source, &leaf);
|
||||
operator_path_resolve = save;
|
||||
if (dfd < 0)
|
||||
ifd = -1;
|
||||
else {
|
||||
ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else
|
||||
ifd = do_open_nofollow(source, O_RDONLY);
|
||||
if (ifd < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "open %s", full_fname(source));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tmpfilefd >= 0) {
|
||||
ofd = tmpfilefd;
|
||||
} else {
|
||||
ofd = unlink_and_reopen(dest, mode);
|
||||
if (ofd < 0) {
|
||||
int save_errno = errno;
|
||||
close(ifd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_PREALLOCATION
|
||||
if (preallocate_files) {
|
||||
STRUCT_STAT srcst;
|
||||
|
||||
/* Try to preallocate enough space for file's eventual length. Can
|
||||
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
|
||||
if (do_fstat(ifd, &srcst) < 0)
|
||||
rsyserr(FWARNING, errno, "fstat %s", full_fname(source));
|
||||
else if (srcst.st_size > 0) {
|
||||
prealloc_len = do_fallocate(ofd, 0, srcst.st_size);
|
||||
if (prealloc_len < 0)
|
||||
rsyserr(FWARNING, errno, "do_fallocate %s", full_fname(dest));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
while ((len = safe_read(ifd, buf, sizeof buf)) > 0) {
|
||||
if (full_write(ofd, buf, len) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest));
|
||||
close(ifd);
|
||||
close(ofd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
offset += len;
|
||||
}
|
||||
|
||||
if (len < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "read %s", full_fname(source));
|
||||
close(ifd);
|
||||
close(ofd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Source file might have shrunk since we fstatted it.
|
||||
* Cut off any extra preallocated zeros from dest file. */
|
||||
if (offset < prealloc_len) {
|
||||
#ifdef HAVE_FTRUNCATE
|
||||
/* If we fail to truncate, the dest file may be wrong, so we
|
||||
* must trigger the "partial transfer" error. */
|
||||
if (do_ftruncate(ofd, offset) < 0)
|
||||
rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest));
|
||||
#else
|
||||
rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest));
|
||||
#endif
|
||||
}
|
||||
|
||||
if (do_fsync && fsync(ofd) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest));
|
||||
close(ofd);
|
||||
close(ifd); /* ifd is held open until after the xattr copy below */
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
/* Read the source xattrs through the held source fd (ifd) and set them
|
||||
* through ofd while both are still held, so a parent-symlink race can't
|
||||
* redirect the read out of tree or the write onto a file outside it. */
|
||||
if (preserve_xattrs)
|
||||
copy_xattrs(source, ifd, dest, ofd);
|
||||
#endif
|
||||
|
||||
if (close(ifd) < 0) {
|
||||
rsyserr(FWARNING, errno, "close failed on %s",
|
||||
full_fname(source));
|
||||
}
|
||||
|
||||
if (close(ofd) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* MAX_RENAMES should be 10**MAX_RENAMES_DIGITS */
|
||||
#define MAX_RENAMES_DIGITS 3
|
||||
#define MAX_RENAMES 1000
|
||||
|
||||
/**
|
||||
* Robust unlink: some OS'es (HPUX) refuse to unlink busy files, so
|
||||
* rename to <path>/.rsyncNNN instead.
|
||||
*
|
||||
* Note that successive rsync runs will shuffle the filenames around a
|
||||
* bit as long as the file is still busy; this is because this function
|
||||
* does not know if the unlink call is due to a new file coming in, or
|
||||
* --delete trying to remove old .rsyncNNN files, hence it renames it
|
||||
* each time.
|
||||
**/
|
||||
int robust_unlink(const char *fname)
|
||||
{
|
||||
#ifndef ETXTBSY
|
||||
return do_unlink_at(fname);
|
||||
#else
|
||||
static int counter = 1;
|
||||
int rc, pos, start;
|
||||
char path[MAXPATHLEN];
|
||||
|
||||
rc = do_unlink_at(fname);
|
||||
if (rc == 0 || errno != ETXTBSY)
|
||||
return rc;
|
||||
|
||||
if ((pos = strlcpy(path, fname, MAXPATHLEN)) >= MAXPATHLEN)
|
||||
pos = MAXPATHLEN - 1;
|
||||
|
||||
while (pos > 0 && path[pos-1] != '/')
|
||||
pos--;
|
||||
pos += strlcpy(path+pos, ".rsync", MAXPATHLEN-pos);
|
||||
|
||||
if (pos > (MAXPATHLEN-MAX_RENAMES_DIGITS-1)) {
|
||||
errno = ETXTBSY;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* start where the last one left off to reduce chance of clashes */
|
||||
start = counter;
|
||||
do {
|
||||
snprintf(&path[pos], MAX_RENAMES_DIGITS+1, "%03d", counter);
|
||||
if (++counter >= MAX_RENAMES)
|
||||
counter = 1;
|
||||
} while (access(path, 0) == 0 && counter != start);
|
||||
|
||||
if (INFO_GTE(MISC, 1)) {
|
||||
rprintf(FWARNING, "renaming %s to %s because of text busy\n",
|
||||
fname, path);
|
||||
}
|
||||
|
||||
/* maybe we should return rename()'s exit status? Nah. */
|
||||
if (do_rename_at(fname, path) != 0) {
|
||||
errno = ETXTBSY;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Returns 0 on successful rename, 1 if we successfully copied the file
|
||||
* across filesystems, -2 if copy_file() failed, and -1 on other errors.
|
||||
* If partialptr is not NULL and we need to do a copy, copy the file into
|
||||
* the active partial-dir instead of over the destination file. */
|
||||
int robust_rename(const char *from, const char *to, const char *partialptr,
|
||||
int mode, struct file_struct *file)
|
||||
{
|
||||
int tries = 4;
|
||||
|
||||
/* A resumed in-place partial-dir transfer might call us with from and
|
||||
* to pointing to the same buf if the transfer failed yet again. */
|
||||
if (from == to)
|
||||
return 0;
|
||||
|
||||
while (tries--) {
|
||||
/* tmp -> final usually live in the entry's own dir: rename via the
|
||||
* held dir fd when both do, else the full-path wrapper. */
|
||||
int ofd = held_dfd_for(from, file);
|
||||
int nfd = held_dfd_for(to, file);
|
||||
int rr;
|
||||
if (ofd >= 0 && nfd >= 0) {
|
||||
const char *os = strrchr(from, '/');
|
||||
const char *ns = strrchr(to, '/');
|
||||
rr = do_rename_atfd(ofd, os ? os + 1 : from, nfd, ns ? ns + 1 : to);
|
||||
} else
|
||||
rr = do_rename_at(from, to);
|
||||
if (rr == 0)
|
||||
return 0;
|
||||
|
||||
switch (errno) {
|
||||
#ifdef ETXTBSY
|
||||
case ETXTBSY:
|
||||
if (robust_unlink(to) != 0) {
|
||||
errno = ETXTBSY;
|
||||
return -1;
|
||||
}
|
||||
errno = ETXTBSY;
|
||||
break;
|
||||
#endif
|
||||
case EXDEV: {
|
||||
int save = operator_path_resolve, rc;
|
||||
if (partialptr) {
|
||||
if (!handle_partial_dir(partialptr,PDIR_CREATE))
|
||||
return -2;
|
||||
to = partialptr;
|
||||
}
|
||||
/* Cross-fs fallback: copy then unlink. An absolute --temp-dir
|
||||
* source / --partial-dir dest is an operator path whose parents
|
||||
* do_open_at()/do_unlink_at() would otherwise follow via plain libc
|
||||
* -- confine them through the ownership walk so a raced parent
|
||||
* symlink can't redirect the dest-write or the source-unlink out of
|
||||
* the module. copy_file already confines the source READ; a
|
||||
* relative in-module path stays on the secure_relative_open arm, so
|
||||
* only flip the flag for an absolute (operator) path. */
|
||||
if (*to == '/')
|
||||
operator_path_resolve = 1;
|
||||
rc = copy_file(from, to, -1, mode);
|
||||
operator_path_resolve = save;
|
||||
if (rc != 0)
|
||||
return -2;
|
||||
if (*from == '/')
|
||||
operator_path_resolve = 1;
|
||||
do_unlink_at(from);
|
||||
operator_path_resolve = save;
|
||||
return 1;
|
||||
}
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static pid_t all_pids[10];
|
||||
static int num_pids;
|
||||
|
||||
@@ -366,7 +804,7 @@ static inline void call_glob_match(const char *name, int len, int from_glob,
|
||||
STRUCT_STAT st;
|
||||
int is_dir;
|
||||
|
||||
if (vfs_stat(VFS_AT_FDCWD, glob.arg_buf, &st, VFS_ALLOW_SYMLINK) != 0)
|
||||
if (do_stat(glob.arg_buf, &st) != 0)
|
||||
return;
|
||||
is_dir = S_ISDIR(st.st_mode) != 0;
|
||||
if (arg && !is_dir)
|
||||
@@ -480,16 +918,110 @@ void glob_expand_module(char *base1, char *arg, char ***argv_p, int *argc_p, int
|
||||
|
||||
/**
|
||||
* Convert a string to lower case
|
||||
*
|
||||
* Only ASCII is folded. The hosts allow/deny list that calls this can hold
|
||||
* UTF-8, and a per-byte fold via the locale's ctype would mangle it (in
|
||||
* ISO-8859-1 the 0xC4 lead byte of "č" is an upper-case 'Ä').
|
||||
**/
|
||||
void strlower(char *s)
|
||||
{
|
||||
while (*s) {
|
||||
if (isUpper(s))
|
||||
if (!(*(unsigned char *)s & 0x80) && isUpper(s))
|
||||
*s = toLower(s);
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_IDN
|
||||
/* Does this label hold nothing but the [-a-z0-9] of an A-label? */
|
||||
static int is_a_label(const char *s)
|
||||
{
|
||||
if (!*s)
|
||||
return 0;
|
||||
|
||||
for ( ; *s; s++) {
|
||||
if (!(*s >= 'a' && *s <= 'z') && !(*s >= '0' && *s <= '9') && *s != '-')
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the non-ASCII labels of a host name into their IDNA A-label
|
||||
* (Punycode) form, putting the result in buf. Returns 1 if buf was filled in,
|
||||
* or 0 to tell the caller to keep the name it has.
|
||||
*
|
||||
* A label that is already ASCII is copied verbatim, so an address, a mask, an
|
||||
* xn-- name, and any wildmatch characters come out just as they went in. A
|
||||
* converted label is only used if it comes back as a bare A-label: the IDNA
|
||||
* mapping folds some non-ASCII characters onto ASCII ones (U+FF0A FULLWIDTH
|
||||
* ASTERISK becomes '*'), and a hosts allow/deny entry must not pick up a
|
||||
* wildcard that its author never typed. Anything else leaves the name alone,
|
||||
* which fails to match instead of matching too much.
|
||||
*
|
||||
* Set from_locale for a name that came from the command line, which is in the
|
||||
* user's locale encoding; the daemon's config file is read as UTF-8.
|
||||
**/
|
||||
int idn_to_ascii(const char *name, int from_locale, char *buf, size_t buflen)
|
||||
{
|
||||
const char *lab, *end;
|
||||
size_t len = 0;
|
||||
int converted = 0;
|
||||
|
||||
for (lab = name; ; lab = end + 1) {
|
||||
char label[256], *idn;
|
||||
size_t lablen, alen;
|
||||
int is_ascii = 1;
|
||||
|
||||
for (end = lab; *end && *end != '.'; end++) {
|
||||
if (*(unsigned char *)end & 0x80)
|
||||
is_ascii = 0;
|
||||
}
|
||||
lablen = end - lab;
|
||||
|
||||
if (is_ascii) {
|
||||
if (len + lablen + 2 > buflen)
|
||||
return 0;
|
||||
memcpy(buf + len, lab, lablen);
|
||||
len += lablen;
|
||||
} else {
|
||||
/* IDN2_NFC_INPUT has libidn2 normalize the label, so a name
|
||||
* typed with combining marks folds to the same A-label as
|
||||
* its composed spelling. IDN2_NONTRANSITIONAL asks for the
|
||||
* TR46 processing that everything else does these days. */
|
||||
int flags = IDN2_NFC_INPUT | IDN2_NONTRANSITIONAL;
|
||||
int rc;
|
||||
if (lablen >= sizeof label)
|
||||
return 0;
|
||||
memcpy(label, lab, lablen);
|
||||
label[lablen] = '\0';
|
||||
rc = from_locale ? idn2_lookup_ul(label, &idn, flags)
|
||||
: idn2_to_ascii_8z(label, &idn, flags);
|
||||
if (rc != IDN2_OK)
|
||||
return 0;
|
||||
alen = strlen(idn);
|
||||
if (!is_a_label(idn) || len + alen + 2 > buflen) {
|
||||
idn2_free(idn);
|
||||
return 0;
|
||||
}
|
||||
memcpy(buf + len, idn, alen);
|
||||
len += alen;
|
||||
idn2_free(idn);
|
||||
converted = 1;
|
||||
}
|
||||
|
||||
if (!*end)
|
||||
break;
|
||||
buf[len++] = '.';
|
||||
}
|
||||
|
||||
buf[len] = '\0';
|
||||
|
||||
return converted;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Split a string into tokens based (usually) on whitespace & commas. If the
|
||||
* string starts with a comma (after skipping any leading whitespace), then
|
||||
@@ -776,7 +1308,7 @@ char *sanitize_path(char *dest, const char *p, const char *rootdir, int depth, i
|
||||
}
|
||||
|
||||
/* Like chdir(), but it keeps track of the current directory (in the
|
||||
* global "vfs.curr_dir"), and ensures that the path size doesn't overflow.
|
||||
* global "curr_dir"), and ensures that the path size doesn't overflow.
|
||||
* Also cleans the path using the clean_fname() function. */
|
||||
int change_dir(const char *dir, int set_path_only)
|
||||
{
|
||||
@@ -786,11 +1318,11 @@ int change_dir(const char *dir, int set_path_only)
|
||||
|
||||
if (!initialised) {
|
||||
initialised = 1;
|
||||
if (getcwd(vfs.curr_dir, sizeof vfs.curr_dir - 1) == NULL) {
|
||||
if (getcwd(curr_dir, sizeof curr_dir - 1) == NULL) {
|
||||
rsyserr(FERROR, errno, "getcwd()");
|
||||
exit_cleanup(RERR_FILESELECT);
|
||||
}
|
||||
vfs.curr_dir_len = strlen(vfs.curr_dir);
|
||||
curr_dir_len = strlen(curr_dir);
|
||||
}
|
||||
|
||||
if (!dir) /* this call was probably just to initialize */
|
||||
@@ -801,13 +1333,13 @@ int change_dir(const char *dir, int set_path_only)
|
||||
return 1;
|
||||
|
||||
if (*dir == '/') {
|
||||
if (len >= sizeof vfs.curr_dir) {
|
||||
if (len >= sizeof curr_dir) {
|
||||
errno = ENAMETOOLONG;
|
||||
return 0;
|
||||
}
|
||||
if (!set_path_only) {
|
||||
/* The destination is operator-supplied (like --log-file et al.), so
|
||||
* resolve it with vfs_open_owner_walk: walk each component
|
||||
* resolve it with open_no_attacker_symlinks: walk each component
|
||||
* refusing a symlink not owned by uid 0 or our euid, then fchdir to
|
||||
* the result. This still follows the operator's/root's own symlinked
|
||||
* dest -- the `/backup -> /mnt/disk` / `/var/www -> /srv/www` admin
|
||||
@@ -817,7 +1349,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
* non-daemon receiver can opt back into the legacy plain chdir with
|
||||
* --insecure-links. */
|
||||
if (am_daemon && !am_chrooted) {
|
||||
int dfd = vfs_open_owner_walk(dir, O_RDONLY | O_DIRECTORY, 0, 0);
|
||||
int dfd = open_no_attacker_symlinks_dirfd(dir);
|
||||
if (dfd < 0)
|
||||
return 0;
|
||||
if (fchdir(dfd) != 0) {
|
||||
@@ -848,7 +1380,7 @@ int change_dir(const char *dir, int set_path_only)
|
||||
* another uid. A real dir is opened directly. This closes the
|
||||
* destination chdir TOCTOU; --insecure-links keeps the plain
|
||||
* chdir for an operator whose dest is a foreign-owned symlink. */
|
||||
dfd = vfs_open_owner_walk(nf, O_RDONLY | O_DIRECTORY, 0, 0);
|
||||
dfd = open_no_attacker_symlinks_dirfd(nf);
|
||||
if (dfd < 0)
|
||||
return 0;
|
||||
if (fchdir(dfd) != 0) {
|
||||
@@ -864,16 +1396,16 @@ int change_dir(const char *dir, int set_path_only)
|
||||
}
|
||||
}
|
||||
skipped_chdir = set_path_only;
|
||||
memcpy(vfs.curr_dir, dir, len + 1);
|
||||
memcpy(curr_dir, dir, len + 1);
|
||||
} else {
|
||||
unsigned int save_dir_len = vfs.curr_dir_len;
|
||||
if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir) {
|
||||
unsigned int save_dir_len = curr_dir_len;
|
||||
if (curr_dir_len + 1 + len >= sizeof curr_dir) {
|
||||
errno = ENAMETOOLONG;
|
||||
return 0;
|
||||
}
|
||||
if (!(vfs.curr_dir_len && vfs.curr_dir[vfs.curr_dir_len-1] == '/'))
|
||||
vfs.curr_dir[vfs.curr_dir_len++] = '/';
|
||||
memcpy(vfs.curr_dir + vfs.curr_dir_len, dir, len + 1);
|
||||
if (!(curr_dir_len && curr_dir[curr_dir_len-1] == '/'))
|
||||
curr_dir[curr_dir_len++] = '/';
|
||||
memcpy(curr_dir + curr_dir_len, dir, len + 1);
|
||||
|
||||
if (!set_path_only) {
|
||||
int chdir_failed;
|
||||
@@ -882,20 +1414,20 @@ int change_dir(const char *dir, int set_path_only)
|
||||
* target -- otherwise CWD escapes the module and
|
||||
* every subsequent path-relative syscall (open,
|
||||
* chmod, lchown, ...) inherits the escape, which
|
||||
* defeats vfs_resolve_open's RESOLVE_BENEATH
|
||||
* defeats secure_relative_open's RESOLVE_BENEATH
|
||||
* anchor and re-opens the CVE-2026-29518 class of
|
||||
* symlink TOCTOU attacks. Use the secure resolver
|
||||
* to get a confined dirfd, then fchdir() to it.
|
||||
*
|
||||
* If skipped_chdir is set, a previous CD_SKIP_CHDIR
|
||||
* call buffered an absolute prefix in vfs.curr_dir
|
||||
* call buffered an absolute prefix in curr_dir
|
||||
* (e.g. change_pathname's CD_SKIP_CHDIR to orig_dir)
|
||||
* without syncing the kernel's CWD. Resolve `dir`
|
||||
* relative to that prefix as basedir so the secure
|
||||
* branch still anchors at the operator-trusted
|
||||
* directory rather than wherever the kernel CWD
|
||||
* happens to be. */
|
||||
if (am_daemon && (!am_chrooted || module_dirlen) && !vfs_symlink_optout_allowed()) {
|
||||
if (am_daemon && (!am_chrooted || module_dirlen) && !symlink_optout_allowed()) {
|
||||
const char *basedir = NULL;
|
||||
char prefix[MAXPATHLEN];
|
||||
int dfd;
|
||||
@@ -905,31 +1437,29 @@ int change_dir(const char *dir, int set_path_only)
|
||||
chdir_failed = 1;
|
||||
goto chdir_cleanup;
|
||||
}
|
||||
memcpy(prefix, vfs.curr_dir, save_dir_len);
|
||||
memcpy(prefix, curr_dir, save_dir_len);
|
||||
prefix[save_dir_len] = '\0';
|
||||
basedir = prefix;
|
||||
}
|
||||
dfd = vfs_resolve_open(basedir, dir,
|
||||
O_RDONLY | O_DIRECTORY, 0);
|
||||
dfd = secure_relative_dirfd(basedir, dir);
|
||||
if (dfd < 0) {
|
||||
chdir_failed = 1;
|
||||
} else {
|
||||
chdir_failed = fchdir(dfd) != 0;
|
||||
close(dfd);
|
||||
}
|
||||
} else if (am_daemon && vfs_symlink_optout_allowed()) {
|
||||
} else if (am_daemon && symlink_optout_allowed()) {
|
||||
/* "insecure links = yes": restore the 3.2.7 follow-any-symlink
|
||||
* traversal with a plain chdir to the accumulated path, the same
|
||||
* legacy behaviour the per-operation sites grant under the opt-out. */
|
||||
chdir_failed = chdir(vfs.curr_dir) != 0;
|
||||
chdir_failed = chdir(curr_dir) != 0;
|
||||
} else if (!am_chrooted && !am_sender && !insecure_links) {
|
||||
/* Non-daemon receiver: confine the operator-named relative
|
||||
* destination like the absolute case above -- refuse a component
|
||||
* symlink not owned by uid 0 or our euid, closing the
|
||||
* relative-dest chdir TOCTOU while still following the operator's
|
||||
* own symlinks. --insecure-links keeps the plain chdir. */
|
||||
int dfd = vfs_open_owner_walk(vfs.curr_dir,
|
||||
O_RDONLY | O_DIRECTORY, 0, 0);
|
||||
int dfd = open_no_attacker_symlinks_dirfd(curr_dir);
|
||||
if (dfd < 0)
|
||||
chdir_failed = 1;
|
||||
else {
|
||||
@@ -937,30 +1467,30 @@ int change_dir(const char *dir, int set_path_only)
|
||||
close(dfd);
|
||||
}
|
||||
} else {
|
||||
chdir_failed = chdir(vfs.curr_dir) != 0;
|
||||
chdir_failed = chdir(curr_dir) != 0;
|
||||
}
|
||||
chdir_cleanup:
|
||||
if (chdir_failed) {
|
||||
vfs.curr_dir_len = save_dir_len;
|
||||
vfs.curr_dir[vfs.curr_dir_len] = '\0';
|
||||
curr_dir_len = save_dir_len;
|
||||
curr_dir[curr_dir_len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
skipped_chdir = set_path_only;
|
||||
}
|
||||
|
||||
vfs.curr_dir_len = clean_fname(vfs.curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR);
|
||||
curr_dir_len = clean_fname(curr_dir, CFN_COLLAPSE_DOT_DOT_DIRS | CFN_DROP_TRAILING_DOT_DIR);
|
||||
if (sanitize_paths) {
|
||||
if (module_dirlen > vfs.curr_dir_len)
|
||||
module_dirlen = vfs.curr_dir_len;
|
||||
curr_dir_depth = count_dir_elements(vfs.curr_dir + module_dirlen);
|
||||
if (module_dirlen > curr_dir_len)
|
||||
module_dirlen = curr_dir_len;
|
||||
curr_dir_depth = count_dir_elements(curr_dir + module_dirlen);
|
||||
}
|
||||
|
||||
if (!set_path_only) /* a real chdir invalidates the cwd-relative dir-fd stack */
|
||||
vfs_dircache_reset();
|
||||
reset_dir_fd_cache();
|
||||
|
||||
if (DEBUG_GTE(CHDIR, 1) && !set_path_only)
|
||||
rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), vfs.curr_dir);
|
||||
rprintf(FINFO, "[%s] change_dir(%s)\n", who_am_i(), curr_dir);
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -973,12 +1503,12 @@ char *normalize_path(char *path, BOOL force_newbuf, unsigned int *len_ptr)
|
||||
|
||||
if (*path != '/') { /* Make path absolute. */
|
||||
int len = strlen(path);
|
||||
if (vfs.curr_dir_len + 1 + len >= sizeof vfs.curr_dir)
|
||||
if (curr_dir_len + 1 + len >= sizeof curr_dir)
|
||||
return NULL;
|
||||
vfs.curr_dir[vfs.curr_dir_len] = '/';
|
||||
memcpy(vfs.curr_dir + vfs.curr_dir_len + 1, path, len + 1);
|
||||
path = strdup(vfs.curr_dir);
|
||||
vfs.curr_dir[vfs.curr_dir_len] = '\0';
|
||||
curr_dir[curr_dir_len] = '/';
|
||||
memcpy(curr_dir + curr_dir_len + 1, path, len + 1);
|
||||
path = strdup(curr_dir);
|
||||
curr_dir[curr_dir_len] = '\0';
|
||||
} else if (force_newbuf)
|
||||
path = strdup(path);
|
||||
|
||||
@@ -1010,7 +1540,7 @@ char *full_fname(const char *fn)
|
||||
if (*fn == '/')
|
||||
p1 = p2 = "";
|
||||
else {
|
||||
p1 = vfs.curr_dir + module_dirlen;
|
||||
p1 = curr_dir + module_dirlen;
|
||||
for (p2 = p1; *p2 == '/'; p2++) {}
|
||||
if (*p2)
|
||||
p2 = "/";
|
||||
@@ -1080,22 +1610,26 @@ int handle_partial_dir(const char *fname, int create)
|
||||
* outside the tree): resolve it with the ownership walk -- follow a
|
||||
* uid0/euid-owned symlink, refuse a foreign one, absolute and relative alike.
|
||||
* --insecure-links (or a daemon module's "insecure links =") opts out. */
|
||||
operator_path_resolve = 1;
|
||||
if (create) {
|
||||
STRUCT_STAT st;
|
||||
int statret = vfs_lstat(VFS_AT_FDCWD, dir, &st, VFS_OPERATOR_PATH);
|
||||
int statret = do_lstat_at(dir, &st);
|
||||
if (statret == 0 && !S_ISDIR(st.st_mode)) {
|
||||
if (vfs_unlink(VFS_AT_FDCWD, dir, VFS_OPERATOR_PATH) < 0) {
|
||||
if (do_unlink_at(dir) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
return 0;
|
||||
}
|
||||
statret = -1;
|
||||
}
|
||||
if (statret < 0 && vfs_mkdir(VFS_AT_FDCWD, dir, 0700, VFS_OPERATOR_PATH) < 0) {
|
||||
if (statret < 0 && do_mkdir_at(dir, 0700) < 0) {
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
return 0;
|
||||
}
|
||||
} else
|
||||
vfs_unlink(VFS_AT_FDCWD, dir, VFS_REMOVEDIR | VFS_OPERATOR_PATH);
|
||||
do_rmdir_at(dir);
|
||||
operator_path_resolve = 0;
|
||||
*fn = '/';
|
||||
|
||||
return 1;
|
||||
|
||||
-347
@@ -1,347 +0,0 @@
|
||||
/*
|
||||
* vfs/chmod.c - chmod wrappers (path, parent-resolved, held-dirfd).
|
||||
*
|
||||
* Includes the platform-specific lchmod/setattrlist/SYS_fchmodat2 handling and
|
||||
* the leaf-safe do_fchmodat_nofollow helper.
|
||||
*
|
||||
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "ifuncs.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
#ifdef HAVE_SYS_ATTR_H
|
||||
#include <sys/attr.h> /* for the macOS setattrlist() chmod path */
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#include <sys/syscall.h> /* SYS_fchmodat2 raw-syscall wrapper */
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CHMOD
|
||||
static int vfs__chmod_plain(const char *path, mode_t mode)
|
||||
{
|
||||
static int switch_step = 0;
|
||||
int code;
|
||||
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
RETURN_ERROR_IF_NULL(path);
|
||||
|
||||
switch (switch_step) {
|
||||
#ifdef HAVE_LCHMOD
|
||||
case 0:
|
||||
if ((code = lchmod(path, mode & CHMOD_BITS)) == 0)
|
||||
break;
|
||||
if (errno == ENOSYS)
|
||||
switch_step++;
|
||||
else if (errno != ENOTSUP)
|
||||
break;
|
||||
#endif
|
||||
/* FALLTHROUGH */
|
||||
default:
|
||||
if (S_ISLNK(mode)) {
|
||||
# if defined HAVE_SETATTRLIST
|
||||
struct attrlist attrList;
|
||||
uint32_t m = mode & CHMOD_BITS; /* manpage is wrong: not mode_t! */
|
||||
|
||||
memset(&attrList, 0, sizeof attrList);
|
||||
attrList.bitmapcount = ATTR_BIT_MAP_COUNT;
|
||||
attrList.commonattr = ATTR_CMN_ACCESSMASK;
|
||||
if ((code = setattrlist(path, &attrList, &m, sizeof m, FSOPT_NOFOLLOW)) == 0)
|
||||
break;
|
||||
if (errno == ENOTSUP)
|
||||
code = 1;
|
||||
# else
|
||||
code = 1;
|
||||
# endif
|
||||
} else
|
||||
code = chmod(path, mode & CHMOD_BITS); /* DISCOURAGED FUNCTION */
|
||||
break;
|
||||
}
|
||||
if (code != 0 && (preserve_perms || preserve_executability))
|
||||
return code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* chmod `name` relative to dfd without following a final-component symlink.
|
||||
* The held parent fd confines the ancestors; this closes the leaf race (an
|
||||
* attacker swapping the leaf to a symlink that fchmodat(...,0) would follow out
|
||||
* of the tree).
|
||||
*
|
||||
* Never follows the leaf: a regular file or dir is pinned via
|
||||
* openat(O_NOFOLLOW) and chmod'd with fchmod() (leaf-safe, every kernel, and
|
||||
* fakeroot-wrappable unlike the raw fchmodat2() syscall); a symlink leaf is
|
||||
* refused (ELOOP, or EMLINK/EFTYPE on the BSDs). Other types or an open
|
||||
* failure fall to fchmodat(AT_SYMLINK_NOFOLLOW) (a real no-follow chmod on
|
||||
* glibc>=2.32 / Linux>=6.6), then the raw fchmodat2() syscall. If no
|
||||
* no-follow primitive exists we skip with a warning rather than follow the
|
||||
* leaf.
|
||||
*
|
||||
* A FIFO takes the fd path on Linux and the pathname path elsewhere -- see the
|
||||
* S_ISFIFO arm below for why, and for what that costs. Note the type used to
|
||||
* choose between them comes from the lstat above, so a leaf swapped between
|
||||
* that and the open is classified by what it WAS: an observed regular file or
|
||||
* dir that becomes a FIFO is still opened. O_NOFOLLOW rejects symlinks, not
|
||||
* type changes. Constraining the open to the observed type would close that;
|
||||
* it is not done here. */
|
||||
static int do_fchmodat_nofollow(int dfd, const char *name, mode_t mode)
|
||||
{
|
||||
#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
|
||||
mode &= CHMOD_BITS;
|
||||
# ifdef O_NOFOLLOW
|
||||
{
|
||||
STRUCT_STAT st;
|
||||
int oflags = O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_NOCTTY;
|
||||
if (vfs_lstat(dfd, name, &st, 0) < 0)
|
||||
return -1;
|
||||
if (S_ISLNK(st.st_mode)) {
|
||||
errno = ELOOP; /* refuse to chmod through a symlink leaf */
|
||||
return -1;
|
||||
}
|
||||
if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISFIFO(st.st_mode)) {
|
||||
int fd;
|
||||
# ifndef __linux__
|
||||
/* Never open a FIFO here. Opening one -- even O_NONBLOCK --
|
||||
* makes this process a reader for as long as the descriptor
|
||||
* lives, which wakes a writer blocked in open(O_WRONLY) and
|
||||
* can cost it a SIGPIPE or the bytes it writes before we
|
||||
* close. The pathname call reaches the same end state
|
||||
* without that: it succeeds outright when the mode is
|
||||
* grantable, and when macOS refuses an ungrantable setgid
|
||||
* with EPERM (having applied nothing), asking again without
|
||||
* that bit gives exactly what fchmod() would have -- it drops
|
||||
* the bit it cannot grant and applies the ordinary ones.
|
||||
* Measured on macOS: fchmodat(2750) EPERM leaving 0600,
|
||||
* fchmodat(0750) ok giving 0750, for a FIFO and a directory
|
||||
* alike.
|
||||
*
|
||||
* This is a pathname call, so unlike the descriptor path it
|
||||
* does not pin the inode; a leaf swapped for another object
|
||||
* of the same name is chmod'd instead. AT_SYMLINK_NOFOLLOW
|
||||
* still keeps it off a symlink's target. That trade buys
|
||||
* away the reader hazard, and only for FIFOs.
|
||||
*
|
||||
* Only S_ISGID is retried. An ungrantable S_ISUID would
|
||||
* still fail where fchmod() would have cleared it, but
|
||||
* setuid is meaningless on a FIFO and the behaviour is
|
||||
* undemonstrated, so it is not coded for.
|
||||
*
|
||||
* Linux keeps the fd-first order it has always had. */
|
||||
if (S_ISFIFO(st.st_mode)) {
|
||||
if (fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW) == 0)
|
||||
return 0;
|
||||
if (errno == EPERM && (mode & S_ISGID)
|
||||
&& fchmodat(dfd, name, mode & ~S_ISGID,
|
||||
AT_SYMLINK_NOFOLLOW) == 0)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
# endif
|
||||
# ifdef O_CLOEXEC
|
||||
oflags |= O_CLOEXEC;
|
||||
# endif
|
||||
fd = openat(dfd, name, oflags);
|
||||
if (fd >= 0) {
|
||||
int r = fchmod(fd, mode), e = errno;
|
||||
close(fd);
|
||||
errno = e;
|
||||
return r;
|
||||
}
|
||||
/* A leaf swapped for a symlink between the lstat above and
|
||||
* this open: refuse rather than fall through. The errno is
|
||||
* not the same everywhere -- Linux/Solaris ELOOP, FreeBSD
|
||||
* EMLINK, NetBSD EFTYPE. */
|
||||
if (errno == ELOOP
|
||||
# ifdef EMLINK
|
||||
|| errno == EMLINK
|
||||
# endif
|
||||
# ifdef EFTYPE
|
||||
|| errno == EFTYPE
|
||||
# endif
|
||||
)
|
||||
return -1; /* raced to a symlink: refuse */
|
||||
/* otherwise (e.g. EACCES on an unreadable file) fall through */
|
||||
}
|
||||
}
|
||||
# endif
|
||||
# if defined __linux__
|
||||
{
|
||||
int r = fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
if (errno != ENOTSUP && errno != EOPNOTSUPP && errno != ENOSYS)
|
||||
return r; /* a real error (EPERM, ENOENT, ...) */
|
||||
}
|
||||
# ifdef SYS_fchmodat2
|
||||
{
|
||||
int r = syscall(SYS_fchmodat2, dfd, name, (unsigned int)mode, AT_SYMLINK_NOFOLLOW);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
if (errno != ENOSYS && errno != EPERM && errno != EOPNOTSUPP)
|
||||
return r;
|
||||
}
|
||||
# endif
|
||||
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
|
||||
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
|
||||
return 1;
|
||||
# else
|
||||
return fchmodat(dfd, name, mode, AT_SYMLINK_NOFOLLOW);
|
||||
# endif
|
||||
#else
|
||||
(void)dfd;
|
||||
(void)mode;
|
||||
/* No symlink-safe chmod primitive here: skip rather than follow the leaf. */
|
||||
rprintf(FWARNING, "vfs_chmod: no symlink-safe chmod for \"%s\"; mode not set\n", name);
|
||||
return 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
Symlink-race-safe variant of vfs_chmod() for receiver-side use.
|
||||
|
||||
Threat model: on a daemon running with "use chroot = no" (the prerequisite
|
||||
for CVE-2026-29518), a local attacker can race a symlink swap of one of
|
||||
the parent directory components of a path the receiver is about to chmod.
|
||||
Because chmod() resolves symlinks at every component, the swap redirects
|
||||
the chmod outside the receiver's confinement.
|
||||
|
||||
Defence: open the *parent* directory of fname under vfs_resolve_open()
|
||||
(a portable per-component O_NOFOLLOW walk on held parent dirfds) and do
|
||||
fchmodat() against that dirfd. A symlink substituted into one of the parent
|
||||
components is then either followed within the tree (legitimate dir-symlinks
|
||||
still work) or rejected (escape attempts fail).
|
||||
|
||||
Final-component handling matches vfs_chmod(): fchmodat() with flag 0
|
||||
follows a symlink at the final component, which is the same behaviour as
|
||||
chmod() and matches every current call site (the file being chmod'd is
|
||||
one the receiver itself just created or transferred). For the rare case
|
||||
where the caller wants to chmod a symlink-as-an-object (S_ISLNK in the
|
||||
mode bits), we fall through to vfs_chmod() which has portability code for
|
||||
that case.
|
||||
|
||||
Falls back to vfs_chmod() for absolute paths and for paths with no parent
|
||||
component, where there is nothing to protect against.
|
||||
*/
|
||||
static int vfs__chmod_secure(const char *fname, mode_t mode, int flags)
|
||||
{
|
||||
#ifdef AT_FDCWD
|
||||
char dirpath[MAXPATHLEN];
|
||||
const char *bname;
|
||||
const char *slash;
|
||||
int dfd, ret, e;
|
||||
size_t dlen;
|
||||
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
|
||||
#if defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
/* Operator-supplied path: resolve the parent via the ownership walk, as
|
||||
* the other VFS wrappers do. Without this the caller's VFS_OPERATOR_PATH
|
||||
* has no effect here, and an absolute name would fall straight through to
|
||||
* the unconfined full-path chmod. S_ISLNK(mode) still needs the plain
|
||||
* lchmod()/setattrlist() handling. */
|
||||
if ((flags & VFS_OPERATOR_PATH) && fname && *fname && !S_ISLNK(mode)) {
|
||||
if (vfs_symlink_optout_allowed())
|
||||
return vfs__chmod_plain(fname, mode);
|
||||
dfd = vfs_owner_walk_parent(fname, &bname, 1);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = do_fchmodat_nofollow(dfd, bname, mode);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Only the daemon-without-chroot case is exposed to the symlink-
|
||||
* race attack: a chroot already confines the receiver, and a
|
||||
* non-daemon rsync runs with the user's own authority so a
|
||||
* symlink they planted can only redirect to files they could
|
||||
* already access. Everywhere else, fall through to plain
|
||||
* vfs_chmod() to avoid the dirfd-open overhead on every call. */
|
||||
if (!vfs_relpath_active())
|
||||
return vfs__chmod_plain(fname, mode);
|
||||
|
||||
if (!fname || !*fname || *fname == '/' || S_ISLNK(mode))
|
||||
return vfs__chmod_plain(fname, mode);
|
||||
|
||||
slash = strrchr(fname, '/');
|
||||
if (!slash)
|
||||
return vfs__chmod_plain(fname, mode);
|
||||
|
||||
dlen = slash - fname;
|
||||
if (dlen >= sizeof dirpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
memcpy(dirpath, fname, dlen);
|
||||
dirpath[dlen] = '\0';
|
||||
bname = slash + 1;
|
||||
|
||||
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
|
||||
ret = do_fchmodat_nofollow(dfd, bname, mode);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
#else
|
||||
(void)flags;
|
||||
return vfs__chmod_plain(fname, mode);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Unified chmod. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd makes
|
||||
* `path` a single component chmod'd (no-follow leaf, via do_fchmodat_nofollow)
|
||||
* under it. flags: VFS_ALLOW_SYMLINK (trusted, plain chmod), default 0 (secure
|
||||
* receiver resolve). A symlink-as-object (S_ISLNK(mode)) goes through the plain
|
||||
* lchmod/setattrlist path. */
|
||||
#ifdef HAVE_CHMOD
|
||||
int vfs_chmod(int dirfd, const char *path, mode_t mode, int flags)
|
||||
{
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
RETURN_ERROR_IF_NULL(path);
|
||||
|
||||
if (dirfd != VFS_AT_FDCWD) {
|
||||
#ifdef AT_FDCWD
|
||||
/* Held-fd: reject empty, multi-component and ".." (writing the
|
||||
* parent of the pinned dir); "." (chmod the dir itself) is a
|
||||
* legitimate single-component op. */
|
||||
if (!*path || strchr(path, '/')
|
||||
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return do_fchmodat_nofollow(dirfd, path, mode);
|
||||
#else
|
||||
(void)dirfd; (void)mode;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (flags & VFS_ALLOW_SYMLINK)
|
||||
return vfs__chmod_plain(path, mode);
|
||||
return vfs__chmod_secure(path, mode, flags);
|
||||
}
|
||||
|
||||
/* Mode on an already-open fd (no path, no symlink to follow): the race-free
|
||||
* counterpart for a pinned cross-tree operator leaf -- see set_file_attrs(). */
|
||||
int vfs_fchmod(int fd, mode_t mode)
|
||||
{
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
return fchmod(fd, mode);
|
||||
}
|
||||
#endif
|
||||
-126
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* vfs/chown.c - lchown wrappers (path, parent-resolved, held-dirfd).
|
||||
*
|
||||
* Moved verbatim out of syscall.c.
|
||||
*
|
||||
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "ifuncs.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
|
||||
#ifndef HAVE_LCHOWN
|
||||
#define lchown chown
|
||||
#endif
|
||||
|
||||
static int vfs__lchown_plain(const char *path, uid_t owner, gid_t group)
|
||||
{
|
||||
return lchown(path, owner, group);
|
||||
}
|
||||
|
||||
/* Secure receiver-side resolve: open the parent under vfs_resolve_open() and
|
||||
* fchownat(..., AT_SYMLINK_NOFOLLOW) so a parent-component symlink swap can't
|
||||
* redirect the chown outside the module. VFS_OPERATOR_PATH takes the ownership
|
||||
* walk instead, as the other VFS wrappers do. Falls through to the plain
|
||||
* lchown in non-daemon/sender, chrooted, no-parent and absolute-path cases. */
|
||||
static int vfs__lchown_secure(const char *path, uid_t owner, gid_t group, int flags)
|
||||
{
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY && defined AT_SYMLINK_NOFOLLOW
|
||||
char dirpath[MAXPATHLEN];
|
||||
const char *bname, *slash;
|
||||
int dfd, ret, e;
|
||||
size_t dlen;
|
||||
|
||||
/* Operator-supplied path: without this branch the caller's
|
||||
* VFS_OPERATOR_PATH has no effect here and an absolute name would fall
|
||||
* straight through to the unconfined full-path lchown. */
|
||||
if ((flags & VFS_OPERATOR_PATH) && path && *path) {
|
||||
if (vfs_symlink_optout_allowed())
|
||||
return vfs__lchown_plain(path, owner, group);
|
||||
dfd = vfs_owner_walk_parent(path, &bname, 1);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (!vfs_relpath_active() || !*path || *path == '/')
|
||||
return vfs__lchown_plain(path, owner, group);
|
||||
slash = strrchr(path, '/');
|
||||
if (!slash)
|
||||
return vfs__lchown_plain(path, owner, group);
|
||||
dlen = slash - path;
|
||||
if (dlen >= sizeof dirpath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
memcpy(dirpath, path, dlen);
|
||||
dirpath[dlen] = '\0';
|
||||
bname = slash + 1;
|
||||
|
||||
dfd = vfs_resolve_open(NULL, dirpath, O_RDONLY | O_DIRECTORY, 0);
|
||||
if (dfd < 0)
|
||||
return -1;
|
||||
ret = fchownat(dfd, bname, owner, group, AT_SYMLINK_NOFOLLOW);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
return ret;
|
||||
#else
|
||||
(void)flags;
|
||||
return vfs__lchown_plain(path, owner, group);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Unified lchown. dirfd == VFS_AT_FDCWD resolves `path`; a real held dirfd
|
||||
* makes `path` a single component chowned (no-follow) under it. flags:
|
||||
* VFS_ALLOW_SYMLINK (trusted, plain lchown), default 0 (secure receiver
|
||||
* resolve). */
|
||||
int vfs_lchown(int dirfd, const char *path, uid_t owner, gid_t group, int flags)
|
||||
{
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
RETURN_ERROR_IF_NULL(path);
|
||||
|
||||
if (dirfd != VFS_AT_FDCWD) {
|
||||
#if defined AT_FDCWD && defined AT_SYMLINK_NOFOLLOW
|
||||
/* Held-fd: reject empty, multi-component and ".." (writing the
|
||||
* parent of the pinned dir); "." (chown the dir itself) is a
|
||||
* legitimate single-component op. */
|
||||
if (!*path || strchr(path, '/')
|
||||
|| (path[0] == '.' && path[1] == '.' && path[2] == '\0')) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return fchownat(dirfd, path, owner, group, AT_SYMLINK_NOFOLLOW);
|
||||
#else
|
||||
(void)dirfd; (void)owner; (void)group;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (flags & VFS_ALLOW_SYMLINK)
|
||||
return vfs__lchown_plain(path, owner, group);
|
||||
return vfs__lchown_secure(path, owner, group, flags);
|
||||
}
|
||||
|
||||
/* Mode/owner on an already-open fd (no path, no symlink to follow): the
|
||||
* race-free way to set metadata on a cross-tree operator-path leaf that was
|
||||
* pinned with O_NOFOLLOW. See set_file_attrs(). */
|
||||
int vfs_fchown(int fd, uid_t owner, gid_t group)
|
||||
{
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
return fchown(fd, owner, group);
|
||||
}
|
||||
-228
@@ -1,228 +0,0 @@
|
||||
/*
|
||||
* vfs/copy_file.c - compound VFS op: copy a file's contents (and, for
|
||||
* --xattrs, its xattrs) to a new destination. Layered on the vfs_* open/
|
||||
* read/write primitives; calls out to the metadata layer (copy_xattrs) for
|
||||
* the held-fd xattr copy.
|
||||
*
|
||||
* Moved out of util1.c as part of the VFS compound layer.
|
||||
*
|
||||
* Copyright (C) 1996-2022 Andrew Tridgell, Paul Mackerras, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "ifuncs.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
|
||||
extern int do_fsync;
|
||||
extern int preallocate_files;
|
||||
extern int preserve_xattrs;
|
||||
|
||||
/* Read @p len bytes at @p ptr from descriptor @p desc, retrying if interrupted.
|
||||
* Returns the number of bytes read (0 = EOF), or <0 on error. */
|
||||
static int safe_read(int desc, char *ptr, size_t len)
|
||||
{
|
||||
int n_chars;
|
||||
|
||||
if (len == 0)
|
||||
return len;
|
||||
|
||||
do {
|
||||
n_chars = read(desc, ptr, len);
|
||||
} while (n_chars < 0 && errno == EINTR);
|
||||
|
||||
return n_chars;
|
||||
}
|
||||
|
||||
/* Remove existing file @dest and reopen, creating a new file with @mode.
|
||||
* vfs_flags carries the resolution policy (VFS_OPERATOR_PATH for an operator
|
||||
* dest) to both the robust_unlink and the create. */
|
||||
static int unlink_and_reopen(const char *dest, mode_t mode, int vfs_flags)
|
||||
{
|
||||
int ofd;
|
||||
|
||||
if (robust_unlink(dest, vfs_flags) && errno != ENOENT) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "unlink %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
if (preserve_xattrs)
|
||||
mode |= S_IWUSR;
|
||||
#endif
|
||||
mode &= INITACCESSPERMS;
|
||||
/* Use vfs_open_at so the create/truncate goes through a secure
|
||||
* parent dirfd in the daemon-no-chroot deployment. Otherwise
|
||||
* an attacker could swap a parent component with a symlink in
|
||||
* the window between robust_unlink (which uses vfs_unlink,
|
||||
* already secure) and the create here, and redirect the new
|
||||
* file outside the module. */
|
||||
if ((ofd = vfs_open_at(dest, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, mode, vfs_flags)) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, save_errno, "open %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
return ofd;
|
||||
}
|
||||
|
||||
/* Copy contents of file @source to file @dest with mode @mode.
|
||||
*
|
||||
* If @tmpfilefd is < 0, copy_file unlinks @dest and then opens a new
|
||||
* file with name @dest.
|
||||
*
|
||||
* Otherwise, copy_file writes to and closes the provided file
|
||||
* descriptor.
|
||||
*
|
||||
* In either case, if --xattrs are being preserved, the dest file will
|
||||
* have its xattrs set from the source file.
|
||||
*
|
||||
* This is used in conjunction with the --temp-dir, --backup, and
|
||||
* --copy-dest options. */
|
||||
int copy_file(const char *source, const char *dest, int tmpfilefd, mode_t mode, int vfs_flags)
|
||||
{
|
||||
int ifd, ofd;
|
||||
char buf[1024 * 8];
|
||||
int len; /* Number of bytes read into `buf'. */
|
||||
OFF_T prealloc_len = 0, offset = 0;
|
||||
|
||||
/* For any hardened (non-chrooted) receiver, route the source open through
|
||||
* vfs_resolve_open so a parent-symlink on the source path (e.g.
|
||||
* --copy-dest=cd where cd is a symlink to an outside directory) cannot
|
||||
* redirect the read to a file the attacker should not see. Plain
|
||||
* vfs_open_nofollow only refuses a final-component symlink; parents are
|
||||
* still followed. An ABSOLUTE source is an operator basis (e.g. an absolute
|
||||
* --copy-dest): confine its parents via the ownership walk -- a foreign-owned
|
||||
* parent symlink is refused, the operator's own dirs/uid0/euid symlinks
|
||||
* followed -- so a flipped parent can't redirect the basis read out of tree.
|
||||
* The walk runs with is_operator=1 (module-exclude enforced) and pins only
|
||||
* the source side, leaving the dest open untouched -- this is why confining
|
||||
* the source here does not re-open the copy_xattrs dest race the way wrapping
|
||||
* the whole copy_altdest_file would. */
|
||||
if (vfs_relpath_active() && source && *source && source[0] != '/')
|
||||
ifd = vfs_resolve_open(NULL, source, O_RDONLY | O_NOFOLLOW, 0);
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
else if (vfs_relpath_active() && source && source[0] == '/'
|
||||
&& !vfs_symlink_optout_allowed()) {
|
||||
int dfd, e;
|
||||
const char *leaf;
|
||||
dfd = vfs_owner_walk_parent(source, &leaf, 1);
|
||||
if (dfd < 0)
|
||||
ifd = -1;
|
||||
else {
|
||||
ifd = openat(dfd, leaf, O_RDONLY | O_NOFOLLOW);
|
||||
e = errno;
|
||||
close(dfd);
|
||||
errno = e;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else
|
||||
ifd = vfs_open_nofollow(source, O_RDONLY);
|
||||
if (ifd < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "open %s", full_fname(source));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (tmpfilefd >= 0) {
|
||||
ofd = tmpfilefd;
|
||||
} else {
|
||||
ofd = unlink_and_reopen(dest, mode, vfs_flags);
|
||||
if (ofd < 0) {
|
||||
int save_errno = errno;
|
||||
close(ifd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_PREALLOCATION
|
||||
if (preallocate_files) {
|
||||
STRUCT_STAT srcst;
|
||||
|
||||
/* Try to preallocate enough space for file's eventual length. Can
|
||||
* reduce fragmentation on filesystems like ext4, xfs, and NTFS. */
|
||||
if (vfs_fstat(ifd, &srcst) < 0)
|
||||
rsyserr(FWARNING, errno, "fstat %s", full_fname(source));
|
||||
else if (srcst.st_size > 0) {
|
||||
prealloc_len = vfs_fallocate(ofd, 0, srcst.st_size);
|
||||
if (prealloc_len < 0)
|
||||
rsyserr(FWARNING, errno, "vfs_fallocate %s", full_fname(dest));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
while ((len = safe_read(ifd, buf, sizeof buf)) > 0) {
|
||||
if (full_write(ofd, buf, len) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "write %s", full_fname(dest));
|
||||
close(ifd);
|
||||
close(ofd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
offset += len;
|
||||
}
|
||||
|
||||
if (len < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "read %s", full_fname(source));
|
||||
close(ifd);
|
||||
close(ofd);
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Source file might have shrunk since we fstatted it.
|
||||
* Cut off any extra preallocated zeros from dest file. */
|
||||
if (offset < prealloc_len) {
|
||||
#ifdef HAVE_FTRUNCATE
|
||||
/* If we fail to truncate, the dest file may be wrong, so we
|
||||
* must trigger the "partial transfer" error. */
|
||||
if (vfs_ftruncate(ofd, offset) < 0)
|
||||
rsyserr(FERROR_XFER, errno, "ftruncate %s", full_fname(dest));
|
||||
#else
|
||||
rprintf(FERROR_XFER, "no ftruncate for over-long pre-alloc: %s", full_fname(dest));
|
||||
#endif
|
||||
}
|
||||
|
||||
if (do_fsync && fsync(ofd) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR, errno, "fsync failed on %s", full_fname(dest));
|
||||
close(ofd);
|
||||
close(ifd); /* ifd is held open until after the xattr copy below */
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_XATTRS
|
||||
/* Read the source xattrs through the held source fd (ifd) and set them
|
||||
* through ofd while both are still held, so a parent-symlink race can't
|
||||
* redirect the read out of tree or the write onto a file outside it. */
|
||||
if (preserve_xattrs)
|
||||
copy_xattrs(source, ifd, dest, ofd);
|
||||
#endif
|
||||
|
||||
if (close(ifd) < 0) {
|
||||
rsyserr(FWARNING, errno, "close failed on %s",
|
||||
full_fname(source));
|
||||
}
|
||||
|
||||
if (close(ofd) < 0) {
|
||||
int save_errno = errno;
|
||||
rsyserr(FERROR_XFER, errno, "close failed on %s", full_fname(dest));
|
||||
errno = save_errno;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
-244
@@ -1,244 +0,0 @@
|
||||
/*
|
||||
* vfs/dircache.c - persistent ancestor-dirfd cache for held-directory traversal.
|
||||
*
|
||||
* The file list is path-sorted, so consecutive directory resolutions share a
|
||||
* long leading prefix. Rather than re-resolve a full path from the anchor per
|
||||
* file, we keep the whole current ancestor chain open as pinned, race-safe
|
||||
* dirfds and reuse the longest common component prefix on the next resolution.
|
||||
* vfs_opendir() hands out a held dirfd (or -1 to fall back); vfs_dircache_reset()
|
||||
* drops the chain (called by change_dir() on a real chdir). Moved verbatim out
|
||||
* of syscall.c.
|
||||
*
|
||||
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "ifuncs.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
|
||||
/* Held-directory-fd traversal.
|
||||
*
|
||||
* Rather than re-resolve a full path on every syscall (do_*_at() re-opens the
|
||||
* parent via vfs_resolve_open() each call), the generator and receiver
|
||||
* open each directory ONCE via vfs_opendir() and issue single-component
|
||||
* *at() ops against that held dirfd with the do_*_atfd() wrappers below. The
|
||||
* parent is a pinned fd, not re-resolved, so the per-entry symlink-race window
|
||||
* is closed and the re-resolution overhead is gone.
|
||||
*
|
||||
* vfs_opendir() owns both the authority gate and the resolver choice: it
|
||||
* returns a held dirfd only when hardened resolution is in effect, else -1
|
||||
* with errno==0 so the caller falls back to the do_*_at() wrappers
|
||||
* (behaviour-neutral). The do_*_atfd() wrappers are thin shims with the same
|
||||
* leaf semantics as do_*_at() (dry-run/read-only guards, AT_SYMLINK_NOFOLLOW,
|
||||
* fake-super placeholder files); they never re-check the gate or re-resolve a
|
||||
* parent. */
|
||||
|
||||
int vfs_opendir(const char *dirname)
|
||||
{
|
||||
#ifdef AT_FDCWD
|
||||
int dfd;
|
||||
|
||||
/* Authority gate, identical to the do_*_at() wrappers. When hardened
|
||||
* resolution isn't in effect, return -1 with errno cleared so the caller
|
||||
* uses the full-path wrappers. */
|
||||
if (!vfs_relpath_active()) {
|
||||
errno = 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!dirname || !*dirname) {
|
||||
/* The transfer root itself (file->dirname == NULL): the cwd. */
|
||||
dfd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY);
|
||||
} else if (dirname[0] == '/') {
|
||||
/* An absolute dirname is not expected for an in-transfer entry;
|
||||
* leave it to the legacy path. */
|
||||
errno = 0;
|
||||
return -1;
|
||||
} else {
|
||||
dfd = vfs_resolve_open(NULL, dirname, O_RDONLY | O_DIRECTORY, 0);
|
||||
}
|
||||
|
||||
if (dfd >= 0) {
|
||||
/* O_CLOEXEC on every tier (the per-component walk fallback
|
||||
* doesn't thread our flags onto the returned dirfd). */
|
||||
int fl = fcntl(dfd, F_GETFD);
|
||||
if (fl >= 0)
|
||||
fcntl(dfd, F_SETFD, fl | FD_CLOEXEC);
|
||||
}
|
||||
return dfd;
|
||||
#else
|
||||
(void)dirname;
|
||||
errno = 0;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Persistent ancestor-dirfd stack for held-directory traversal.
|
||||
*
|
||||
* The transfer's file list is path-sorted, so iterating it walks the tree in
|
||||
* DFS order and consecutive directory resolutions share a long leading prefix.
|
||||
* Rather than re-resolve a full path from the anchor each time (re-opening
|
||||
* every ancestor dir per file), we keep the whole current ancestor chain open
|
||||
* as pinned, race-safe dirfds and, on the next resolution, reuse the longest
|
||||
* common component prefix -- popping only the divergent tail and descending the
|
||||
* new tail. Each directory is then opened once while we are inside its subtree.
|
||||
*
|
||||
* The chain is relative to the process cwd (for a NULL anchor), so change_dir()
|
||||
* drops it on any real chdir; it otherwise persists across flist chunks (the
|
||||
* pinned fds stay valid, and a raced/replaced ancestor resolves to the original
|
||||
* inode the fd holds -- the held-dirfd race-safety property, not a hazard).
|
||||
* Each component is resolved with ds_descend(), which follows in-tree directory
|
||||
* symlinks exactly as vfs_resolve_open() does; only the resolved dir fd is
|
||||
* kept (intermediate symlink-target fds are closed -- sound, since an open
|
||||
* dirfd needs no live parent). */
|
||||
#if defined AT_FDCWD && defined O_NOFOLLOW && defined O_DIRECTORY
|
||||
|
||||
void vfs_dircache_reset(void)
|
||||
{
|
||||
while (vfs.dpc.depth > 0)
|
||||
close(vfs.dpc.fd[--vfs.dpc.depth]);
|
||||
if (vfs.dpc.base >= 0)
|
||||
close(vfs.dpc.base);
|
||||
vfs.dpc.base = -1;
|
||||
vfs.dpc.anchor = VFS_DPC_ANCHOR_NONE;
|
||||
}
|
||||
|
||||
/* Resolve directory `dirpath` beneath `anchor` (NULL = cwd, else an absolute
|
||||
* trusted root), reusing the held ancestor stack. Returns a BORROWED dirfd
|
||||
* owned by the cache (do NOT close), or -1 (errno preserved for a real open
|
||||
* error, errno==0 for an uncacheable path -- "..", too deep/long, or a relative
|
||||
* non-cwd anchor) so the caller can fall back to vfs_resolve_open(). */
|
||||
static int dpc_dir_fd(const char *anchor, const char *dirpath)
|
||||
{
|
||||
char copy[MAXPATHLEN];
|
||||
char *comps[VFS_DPC_MAXDEPTH];
|
||||
char *sv = NULL;
|
||||
int nc = 0, p, i;
|
||||
|
||||
if (anchor && anchor[0] != '/') { errno = 0; return -1; }
|
||||
if (!dirpath)
|
||||
dirpath = "";
|
||||
if (dirpath[0] == '/') { errno = 0; return -1; }
|
||||
|
||||
if (anchor != vfs.dpc.anchor || vfs.dpc.base < 0) {
|
||||
int fl;
|
||||
vfs_dircache_reset();
|
||||
vfs.dpc.base = open_anchor_dirfd(anchor ? anchor : ".");
|
||||
if (vfs.dpc.base < 0)
|
||||
return -1;
|
||||
if ((fl = fcntl(vfs.dpc.base, F_GETFD)) >= 0)
|
||||
fcntl(vfs.dpc.base, F_SETFD, fl | FD_CLOEXEC);
|
||||
vfs.dpc.anchor = anchor;
|
||||
}
|
||||
|
||||
if (strlcpy(copy, dirpath, sizeof copy) >= sizeof copy) { errno = ENAMETOOLONG; return -1; }
|
||||
for (char *c = strtok_r(copy, "/", &sv); c; c = strtok_r(NULL, "/", &sv)) {
|
||||
if (c[0] == '.' && c[1] == '\0')
|
||||
continue; /* "." */
|
||||
if (c[0] == '.' && c[1] == '.' && c[2] == '\0') { errno = 0; return -1; }
|
||||
if (nc >= VFS_DPC_MAXDEPTH || strlen(c) >= sizeof vfs.dpc.name[0]) {
|
||||
/* Too deep / a too-long component to cache. Release the held
|
||||
* ancestor fds first so the caller's full-path fallback walk does
|
||||
* not stack on top of them: a deep tree plus a low RLIMIT_NOFILE
|
||||
* (e.g. OpenBSD's default 128) would otherwise exhaust descriptors
|
||||
* (cache depth + walk depth). */
|
||||
vfs_dircache_reset();
|
||||
errno = 0;
|
||||
return -1;
|
||||
}
|
||||
comps[nc++] = c;
|
||||
}
|
||||
|
||||
/* Reuse the longest common prefix; drop the divergent tail. */
|
||||
for (p = 0; p < vfs.dpc.depth && p < nc && strcmp(vfs.dpc.name[p], comps[p]) == 0; p++)
|
||||
;
|
||||
while (vfs.dpc.depth > p)
|
||||
close(vfs.dpc.fd[--vfs.dpc.depth]);
|
||||
|
||||
/* Descend the new tail, holding each resolved component. */
|
||||
for (i = p; i < nc; i++) {
|
||||
int afd = vfs.dpc.depth > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base;
|
||||
struct dirstack ds;
|
||||
int hops = SECURE_OPEN_MAXSYMLINKS;
|
||||
int fd, fl;
|
||||
if (ds_init(&ds, afd) < 0)
|
||||
return -1;
|
||||
if (ds_descend(&ds, comps[i], &hops) < 0) {
|
||||
int e = errno;
|
||||
ds_free(&ds);
|
||||
errno = e;
|
||||
return -1;
|
||||
}
|
||||
fd = ds_take(&ds);
|
||||
ds_free(&ds); /* closes intermediate symlink fds, not afd */
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if ((fl = fcntl(fd, F_GETFD)) >= 0)
|
||||
fcntl(fd, F_SETFD, fl | FD_CLOEXEC);
|
||||
strlcpy(vfs.dpc.name[vfs.dpc.depth], comps[i], sizeof vfs.dpc.name[0]);
|
||||
vfs.dpc.fd[vfs.dpc.depth++] = fd;
|
||||
}
|
||||
|
||||
return nc > 0 ? vfs.dpc.fd[vfs.dpc.depth-1] : vfs.dpc.base;
|
||||
}
|
||||
|
||||
/* Public entry for the sender (no vfs_relpath_active gate: its send paths
|
||||
* confine unconditionally). Borrowed fd; -1 => caller uses the full walk. */
|
||||
int vfs_path_dirfd(const char *anchor, const char *dirpath)
|
||||
{
|
||||
return dpc_dir_fd(anchor, dirpath);
|
||||
}
|
||||
|
||||
int vfs_get_dirfd(const char *dirname)
|
||||
{
|
||||
if (!vfs_relpath_active()) { errno = 0; return -1; }
|
||||
return dpc_dir_fd(NULL, dirname);
|
||||
}
|
||||
#else
|
||||
void vfs_dircache_reset(void)
|
||||
{
|
||||
}
|
||||
int vfs_path_dirfd(const char *anchor, const char *dirpath)
|
||||
{
|
||||
(void)anchor;
|
||||
(void)dirpath;
|
||||
errno = 0;
|
||||
return -1;
|
||||
}
|
||||
int vfs_get_dirfd(const char *dirname)
|
||||
{
|
||||
(void)dirname;
|
||||
errno = 0;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Return the cached current-directory fd iff `path` lives directly in the
|
||||
* entry's own directory (file->dirname) -- the common case for held-dirfd
|
||||
* traversal. Returns -1 (caller falls back to the do_*_at() wrappers) for
|
||||
* anything elsewhere: --temp-dir/--partial-dir/--backup-dir, an absolute path,
|
||||
* a differently-nested dir, or when vfs_opendir() is gated off. The dirfd
|
||||
* is opened once and cached.
|
||||
*
|
||||
* file->basename is NOT assumed to equal `path`'s leaf (a temp file has a
|
||||
* different basename), so the caller derives the leaf from `path`. */
|
||||
int vfs_cached_dirfd(const char *path, const struct file_struct *file)
|
||||
{
|
||||
const char *slash, *dn;
|
||||
size_t plen;
|
||||
|
||||
if (!path || *path == '/')
|
||||
return -1;
|
||||
dn = file && file->dirname ? file->dirname : "";
|
||||
slash = strrchr(path, '/');
|
||||
plen = slash ? (size_t)(slash - path) : 0;
|
||||
if (strlen(dn) != plen || memcmp(path, dn, plen) != 0)
|
||||
return -1;
|
||||
return vfs_get_dirfd(file ? file->dirname : NULL);
|
||||
}
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
/*
|
||||
* vfs/dirstack.c - race-safe component-walk primitives for rsync's VFS.
|
||||
*
|
||||
* The dirstack walks a relative path one component at a time, keeping an open
|
||||
* dirfd for every ancestor from the anchor down, so a parent renamed mid-walk
|
||||
* cannot redirect the climb (TOCTOU). Plus the module-confinement helpers that
|
||||
* decide whether a resolved absolute path has escaped the served module root.
|
||||
* Moved verbatim out of syscall.c.
|
||||
*
|
||||
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
|
||||
/* Returns 1 if path has any "/"-separated component that is exactly
|
||||
* "..", 0 otherwise. Used by vfs_resolve_open's front-door
|
||||
* validation to reject ".." inputs (bare "..", "foo/..", "subdir/..")
|
||||
* for non-re-anchored paths; the walk itself resolves an in-tree ".."
|
||||
* safely (ds_descend pops to the held parent) for a re-anchored path. */
|
||||
int path_has_dotdot_component(const char *path)
|
||||
{
|
||||
const char *p = path;
|
||||
|
||||
while (*p) {
|
||||
const char *q;
|
||||
if (*p == '/') { p++; continue; }
|
||||
q = p;
|
||||
while (*q && *q != '/')
|
||||
q++;
|
||||
if (q - p == 2 && p[0] == '.' && p[1] == '.')
|
||||
return 1;
|
||||
p = q;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* True if `path` lies within directory `root` (path == root, or path begins
|
||||
* with root followed by '/'). `rootlen` is strlen(root). */
|
||||
static int path_within(const char *root, size_t rootlen, const char *path)
|
||||
{
|
||||
return strncmp(path, root, rootlen) == 0
|
||||
&& (path[rootlen] == '\0' || path[rootlen] == '/');
|
||||
}
|
||||
|
||||
/* Refuse (return 1) when the ABSOLUTE resolved path `abspath` lands OUTSIDE the
|
||||
* serving module's root, for an operator/peer-supplied path that must stay in the
|
||||
* module (--partial-dir/--backup-dir/alt-basis: is_operator). An
|
||||
* in-tree symlink owned by uid 0 / the euid is followed by design, so it can
|
||||
* redirect the resolved target outside the module; this catches that escape.
|
||||
*
|
||||
* This is module-ROOT confinement only. The daemon exclude/filter list is a
|
||||
* name-based visibility filter, NOT a physical-path boundary: a symlink whose own
|
||||
* name is not excluded may still resolve into an excluded IN-module subtree,
|
||||
* exactly as in stock rsync. The defense for a writable module is `munge
|
||||
* symlinks` (see rsyncd.conf(5)), not this walk. No-op unless we're a daemon. */
|
||||
/* The root an operator/peer-supplied path must stay under, or NULL when nothing
|
||||
* is confined. A daemon has the served module; a server launched by a wrapper
|
||||
* with its own restricted directory (rrsync) gets one from --confine-root.
|
||||
*
|
||||
* A daemon never honours --confine-root: vfs.module_dir is the boundary there,
|
||||
* and the option arrives in a peer-supplied argv, so obeying it could only
|
||||
* loosen the module. */
|
||||
static const char *confinement_root(unsigned int *lenp)
|
||||
{
|
||||
if (am_daemon) {
|
||||
*lenp = vfs.module_dirlen;
|
||||
return vfs.module_dir;
|
||||
}
|
||||
*lenp = confine_rootlen;
|
||||
return confine_root;
|
||||
}
|
||||
|
||||
/* Split the "/proc/<self|pid>/fd" prefix off `p`, returning the tail -- "" for
|
||||
* the pin directory itself, otherwise a string starting with '/'. NULL when `p`
|
||||
* is not in the fd-pin namespace at all. */
|
||||
const char *vfs_fd_pin_tail(const char *p)
|
||||
{
|
||||
const char *s;
|
||||
|
||||
if (strncmp(p, "/proc/", 6) != 0)
|
||||
return NULL;
|
||||
s = p + 6;
|
||||
if (strncmp(s, "self/", 5) == 0) /* "/proc/self/..." */
|
||||
s += 4;
|
||||
else { /* "/proc/<pid>/..." */
|
||||
const char *d = s;
|
||||
while (*s >= '0' && *s <= '9')
|
||||
s++;
|
||||
if (s == d || *s != '/')
|
||||
return NULL;
|
||||
}
|
||||
if (strncmp(s, "/fd", 3) != 0)
|
||||
return NULL;
|
||||
s += 3;
|
||||
return (*s == '\0' || *s == '/') ? s : NULL;
|
||||
}
|
||||
|
||||
/* An EXACT pin entry, "/proc/self/fd/7" -- the one spelling whose target is what
|
||||
* confinement must judge. rrsync also writes a pinned parent as
|
||||
* ".../fd/7/<leaf>", but the walk resolves the magic link itself and checks the
|
||||
* components past it, so only the bare entry is resolved here. Requiring all
|
||||
* digits keeps a planted name like ".../fd/outside-secret" out. */
|
||||
static int is_exact_fd_pin(const char *p)
|
||||
{
|
||||
const char *tail = vfs_fd_pin_tail(p);
|
||||
|
||||
if (!tail || *tail != '/')
|
||||
return 0;
|
||||
for (++tail; *tail >= '0' && *tail <= '9'; tail++) {}
|
||||
return *tail == '\0' && tail[-1] != '/';
|
||||
}
|
||||
|
||||
int abspath_outside_confinement(const char *abspath, int is_operator)
|
||||
{
|
||||
unsigned int rootlen;
|
||||
const char *root = confinement_root(&rootlen);
|
||||
char pinned[MAXPATHLEN];
|
||||
|
||||
if (!root || !abspath)
|
||||
return 0;
|
||||
if (rootlen <= 1) /* root is "/": nothing is outside */
|
||||
return 0;
|
||||
/* An fd pin (rrsync rewrites a validated option path to /proc/self/fd/N so
|
||||
* no later symlink can redirect it) is spelled outside the root by
|
||||
* construction. Judge it by what it points AT rather than by its spelling,
|
||||
* so a pin is neither wrongly refused nor blindly trusted. A pin we cannot
|
||||
* resolve to an absolute path is refused, not waved through: an unreadable
|
||||
* pin is exactly the case where we cannot say where the open would land. */
|
||||
if (!am_daemon) {
|
||||
const char *tail = vfs_fd_pin_tail(abspath);
|
||||
if (tail && !*tail)
|
||||
return 0; /* the pin directory: transit, opens nothing */
|
||||
if (is_exact_fd_pin(abspath)) {
|
||||
ssize_t n = readlink(abspath, pinned, sizeof pinned - 1);
|
||||
if (n <= 0 || pinned[0] != '/')
|
||||
return is_operator ? 1 : 0;
|
||||
pinned[n] = '\0';
|
||||
abspath = pinned;
|
||||
}
|
||||
}
|
||||
if (path_within(root, rootlen, abspath))
|
||||
return 0; /* inside: name-based exclude is not a boundary */
|
||||
/* Not under the root. An ABSOLUTE walk passes through the root's ancestors
|
||||
* ("/", "/home", ...) on the way down -- those are not "outside", just
|
||||
* not-yet-arrived, so allow them. A path that has truly DIVERGED is
|
||||
* outside: refuse it for an operator/peer path that must stay in the tree
|
||||
* (is_operator); other opens (--log-file, --*-from, lock/motd) may
|
||||
* legitimately live elsewhere. The --insecure-links / "insecure links =
|
||||
* yes" opt-out short-circuits before we get here. */
|
||||
if (!*abspath || path_within(abspath, strlen(abspath), root))
|
||||
return 0; /* ancestor of the root: still descending */
|
||||
return is_operator ? 1 : 0;
|
||||
}
|
||||
|
||||
#if defined(O_NOFOLLOW) && defined(O_DIRECTORY) && defined(AT_FDCWD)
|
||||
|
||||
/* Open a trusted absolute anchor directory as an owned dirfd. When the anchor is
|
||||
* the served module root and the daemon pinned it by identity (vfs.module_dirfd), dup
|
||||
* that fd rather than re-resolving the absolute path with openat(AT_FDCWD, ...) --
|
||||
* which re-traverses the module's ancestors as the dropped-privilege module uid
|
||||
* and EACCESes when the module sits under a non-traversable parent (a 0700 home).
|
||||
* Functionally identical (same inode), just privilege-drop-safe. Gated like its
|
||||
* callers (the secure resolver and dpc_dir_fd both require these three). */
|
||||
int open_anchor_dirfd(const char *path)
|
||||
{
|
||||
if (vfs.module_dirfd >= 0 && am_daemon && vfs.module_dir && strcmp(path, vfs.module_dir) == 0)
|
||||
return dup(vfs.module_dirfd);
|
||||
return openat(AT_FDCWD, path, O_RDONLY | O_DIRECTORY);
|
||||
}
|
||||
|
||||
/* Append "/comp" to ds->abspath (no-op if it's unseeded/empty so non-daemon
|
||||
* callers pay nothing). Returns -1 (ENAMETOOLONG) on overflow. */
|
||||
static int ds_path_push(struct dirstack *ds, const char *comp)
|
||||
{
|
||||
size_t al = strlen(ds->abspath);
|
||||
if (al == 0)
|
||||
return 0; /* unseeded: tracking disabled for this walk */
|
||||
size_t cl = strlen(comp);
|
||||
if (al + 1 + cl >= sizeof ds->abspath) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
ds->abspath[al] = '/';
|
||||
memcpy(ds->abspath + al + 1, comp, cl + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Drop the last component of ds->abspath (mirrors a ".." pop). */
|
||||
static void ds_path_pop(struct dirstack *ds)
|
||||
{
|
||||
char *slash;
|
||||
if (!ds->abspath[0])
|
||||
return;
|
||||
slash = strrchr(ds->abspath, '/');
|
||||
if (slash && slash != ds->abspath)
|
||||
*slash = '\0';
|
||||
}
|
||||
|
||||
/* Initialise with `anchor` (which may be AT_FDCWD) as the un-owned base.
|
||||
* Returns int for caller symmetry, but cannot fail (the fd array is inline). */
|
||||
int ds_init(struct dirstack *ds, int anchor)
|
||||
{
|
||||
ds->abspath[0] = '\0';
|
||||
ds->fds[0] = anchor;
|
||||
ds->top = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Close every pushed fd (but not the borrowed anchor at index 0). */
|
||||
void ds_free(struct dirstack *ds)
|
||||
{
|
||||
while (ds->top > 0)
|
||||
close(ds->fds[ds->top--]);
|
||||
}
|
||||
|
||||
int ds_cur(struct dirstack *ds)
|
||||
{
|
||||
return ds->fds[ds->top];
|
||||
}
|
||||
|
||||
static int ds_push(struct dirstack *ds, int fd)
|
||||
{
|
||||
if (ds->top + 1 >= DS_MAXDEPTH) { /* deeper than we'll hold open */
|
||||
close(fd);
|
||||
errno = ENOMEM;
|
||||
return -1;
|
||||
}
|
||||
ds->fds[++ds->top] = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Detach the current dir as an owned fd the caller must close. At the anchor
|
||||
* (top 0) the anchor is borrowed, so return a fresh dup of it instead. */
|
||||
int ds_take(struct dirstack *ds)
|
||||
{
|
||||
if (ds->top > 0)
|
||||
return ds->fds[ds->top--];
|
||||
return openat(ds->fds[0], ".", O_RDONLY | O_DIRECTORY);
|
||||
}
|
||||
|
||||
/* Descend one path component on the stack: "." stays, ".." pops to the pinned
|
||||
* parent (ELOOP at the anchor), a real subdirectory is pushed, and an in-tree
|
||||
* directory symlink is followed by walking its (relative, possibly
|
||||
* ..-containing) target on the same stack. Returns 0, or -1 with errno set:
|
||||
* ELOOP for a refused/escaping symlink or a hop overrun, otherwise the
|
||||
* underlying openat()/readlinkat() errno (ENOENT, a real ENOTDIR, EACCES). */
|
||||
int ds_descend(struct dirstack *ds, const char *part, int *hops)
|
||||
{
|
||||
if (part[0] == '.' && part[1] == '\0')
|
||||
return 0; /* "." -- no movement */
|
||||
if (part[0] == '.' && part[1] == '.' && part[2] == '\0') {
|
||||
if (ds->top == 0) { /* would rise above the anchor */
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
close(ds->fds[ds->top--]); /* pop to the held parent fd */
|
||||
ds_path_pop(ds);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fd = openat(ds_cur(ds), part, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
|
||||
if (fd != -1) { /* a real subdirectory */
|
||||
if (ds_push(ds, fd) < 0)
|
||||
return -1;
|
||||
if (ds_path_push(ds, part) < 0)
|
||||
return -1;
|
||||
/* exclude-aware: refuse descending into a module-hidden dir (catches a
|
||||
* symlink that redirected the walk into an excluded subtree). */
|
||||
/* The strict resolver stays confined beneath the anchor (within the
|
||||
* module), so this never actually refuses; pass is_operator=0. */
|
||||
if (abspath_outside_confinement(ds->abspath, 0)) {
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* O_NOFOLLOW refused a symlink (NOFOLLOW_HIT_SYMLINK: ELOOP on Linux, EMLINK
|
||||
* on FreeBSD, EFTYPE on NetBSD/OpenBSD), or O_DIRECTORY hit a non-directory
|
||||
* (ENOTDIR). Either may be a symlink, so fall through to the readlink probe;
|
||||
* anything else is a hard error. */
|
||||
if (errno != ENOTDIR && !NOFOLLOW_HIT_SYMLINK(errno)) {
|
||||
if (errno == EMFILE || errno == ENFILE) {
|
||||
/* The resolver holds one dirfd per path component, so a deep path
|
||||
* can exhaust descriptors where plain open() would not. Hint at
|
||||
* the fix once -- otherwise "Too many open files" is opaque. */
|
||||
static int warned = 0;
|
||||
if (!warned) {
|
||||
int e = errno;
|
||||
warned = 1;
|
||||
rprintf(FWARNING, "out of file descriptors resolving a deep path;"
|
||||
" raise the open-file limit (e.g. `ulimit -n`)\n");
|
||||
errno = e;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
int open_errno = errno;
|
||||
|
||||
char buf[MAXPATHLEN];
|
||||
ssize_t n = readlinkat(ds_cur(ds), part, buf, sizeof buf - 1);
|
||||
if (n < 0) {
|
||||
if (errno == EINVAL) /* not a symlink: a real non-dir */
|
||||
errno = open_errno;
|
||||
return -1;
|
||||
}
|
||||
if (n == 0 || (size_t)n >= sizeof buf - 1) {
|
||||
errno = ELOOP; /* empty or truncated target */
|
||||
return -1;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
if (buf[0] == '/') { /* absolute target: refuse */
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
if (--(*hops) < 0) {
|
||||
errno = ELOOP;
|
||||
return -1;
|
||||
}
|
||||
return ds_walk_path(ds, buf, hops);
|
||||
}
|
||||
|
||||
/* Walk every component of a relative path on the stack (used for the basedir,
|
||||
* and for a followed symlink's target -- which may contain ".."). */
|
||||
int ds_walk_path(struct dirstack *ds, char *path, int *hops)
|
||||
{
|
||||
char *save = NULL;
|
||||
for (char *c = strtok_r(path, "/", &save); c; c = strtok_r(NULL, "/", &save)) {
|
||||
if (ds_descend(ds, c, hops) < 0)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* O_NOFOLLOW && O_DIRECTORY && AT_FDCWD */
|
||||
@@ -1,2 +0,0 @@
|
||||
This is a dummy file to ensure that the vfs directory gets created
|
||||
by configure when a VPATH is used.
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
/*
|
||||
* vfs/fileio.c - fd-based file-data ops: ftruncate, lseek, fallocate,
|
||||
* hole-punching.
|
||||
*
|
||||
* Copyright (C) 1998-2022 Andrew Tridgell, Martin Pool, Wayne Davison
|
||||
* Copyright (C) 2026 Wayne Davison, Andrew Tridgell
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#include "rsync.h"
|
||||
#include "ifuncs.h"
|
||||
#include "vfs/vfs_internal.h"
|
||||
#if defined HAVE_SYS_FALLOCATE && !defined HAVE_FALLOCATE
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
|
||||
#ifndef S_BLKSIZE
|
||||
# if defined hpux || defined __hpux__ || defined __hpux
|
||||
# define S_BLKSIZE 1024
|
||||
# elif defined _AIX && defined _I386
|
||||
# define S_BLKSIZE 4096
|
||||
# else
|
||||
# define S_BLKSIZE 512
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_FTRUNCATE
|
||||
int vfs_ftruncate(int fd, OFF_T size)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (dry_run) return 0;
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
|
||||
do {
|
||||
ret = ftruncate(fd, size);
|
||||
} while (ret < 0 && errno == EINTR);
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
OFF_T vfs_lseek(int fd, OFF_T offset, int whence)
|
||||
{
|
||||
#ifdef HAVE_LSEEK64
|
||||
return lseek64(fd, offset, whence);
|
||||
#else
|
||||
return lseek(fd, offset, whence);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
#ifdef SUPPORT_PREALLOCATION
|
||||
#ifdef FALLOC_FL_KEEP_SIZE
|
||||
#define DO_FALLOC_OPTIONS FALLOC_FL_KEEP_SIZE
|
||||
#else
|
||||
#define DO_FALLOC_OPTIONS 0
|
||||
#endif
|
||||
|
||||
OFF_T vfs_fallocate(int fd, OFF_T offset, OFF_T length)
|
||||
{
|
||||
/* FALLOC_FL_KEEP_SIZE lets --preallocate/--inplace keep the file size at 0
|
||||
* until data is written, but a later hole-punch (for --sparse) can only
|
||||
* deallocate blocks that lie within the file's size -- with KEEP_SIZE the
|
||||
* reserved blocks sit beyond EOF and the punch silently does nothing,
|
||||
* leaving the file fully allocated. So when holes will also be punched,
|
||||
* preallocate at full size instead (write_sparse then punches the nulls). */
|
||||
int opts = (inplace || preallocate_files) && sparse_files <= 0 ? DO_FALLOC_OPTIONS : 0;
|
||||
int ret;
|
||||
RETURN_ERROR_IF(dry_run, 0);
|
||||
RETURN_ERROR_IF_RO_OR_LO;
|
||||
if (length & 1) /* make the length not match the desired length */
|
||||
length++;
|
||||
else
|
||||
length--;
|
||||
#if defined HAVE_FALLOCATE
|
||||
ret = fallocate(fd, opts, offset, length);
|
||||
#elif defined HAVE_SYS_FALLOCATE
|
||||
ret = syscall(SYS_fallocate, fd, opts, (loff_t)offset, (loff_t)length);
|
||||
#elif defined HAVE_EFFICIENT_POSIX_FALLOCATE
|
||||
ret = posix_fallocate(fd, offset, length);
|
||||
#else
|
||||
#error Coding error in SUPPORT_PREALLOCATION logic.
|
||||
#endif
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
if (opts == 0) {
|
||||
STRUCT_STAT st;
|
||||
if (vfs_fstat(fd, &st) < 0)
|
||||
return length;
|
||||
return st.st_blocks * S_BLKSIZE;
|
||||
}
|
||||
/* With FALLOC_FL_KEEP_SIZE the blocks for [0, length) are reserved even
|
||||
* though the file size stays put. Return that reserved length (not 0) so
|
||||
* the caller's preallocated_len is meaningful: write_sparse() needs it to
|
||||
* choose vfs_punch_hole() over a plain lseek() when turning a null run into
|
||||
* a hole, and the receiver uses it to trim any over-preallocation. (A
|
||||
* stray 0 here, from 2019's switch to KEEP_SIZE, is why --preallocate
|
||||
* --sparse stopped producing sparse files.) */
|
||||
return length;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Write all @len bytes from @ptr to @fd, retrying short writes and EINTR.
|
||||
* Returns 0 on success, -1 on error. */
|
||||
static int safe_write(int fd, const char *ptr, size_t len)
|
||||
{
|
||||
while (len > 0) {
|
||||
int wrote = write(fd, ptr, len);
|
||||
if (wrote <= 0) {
|
||||
if (wrote < 0 && errno == EINTR)
|
||||
continue;
|
||||
return -1;
|
||||
}
|
||||
ptr += wrote;
|
||||
len -= wrote;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Punch a hole at pos for len bytes. The current file position must be at pos and will be
|
||||
* changed to be at pos + len. */
|
||||
int vfs_punch_hole(int fd, OFF_T pos, OFF_T len)
|
||||
{
|
||||
#ifdef HAVE_FALLOCATE
|
||||
# ifdef HAVE_FALLOC_FL_PUNCH_HOLE
|
||||
if (fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, pos, len) == 0) {
|
||||
if (vfs_lseek(fd, len, SEEK_CUR) != pos + len)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
# endif
|
||||
# ifdef HAVE_FALLOC_FL_ZERO_RANGE
|
||||
if (fallocate(fd, FALLOC_FL_ZERO_RANGE, pos, len) == 0) {
|
||||
if (vfs_lseek(fd, len, SEEK_CUR) != pos + len)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
# endif
|
||||
#else
|
||||
(void)pos;
|
||||
#endif
|
||||
{
|
||||
char zeros[4096];
|
||||
memset(zeros, 0, sizeof zeros);
|
||||
while (len > 0) {
|
||||
int chunk = len > (int)sizeof zeros ? (int)sizeof zeros : len;
|
||||
if (safe_write(fd, zeros, chunk) < 0)
|
||||
return -1;
|
||||
len -= chunk;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user