Compare commits

..
26 Commits
Author SHA1 Message Date
Samuel Henrique 43a5fa44a8 docs: list --password-file among the confined operator paths (#1082) 2026-09-09 13:30:01 +10:00
Zen Dodd 7fef1c827e docs: correct devices and specials behaviour (#1078) 2026-09-04 16:54:28 +10:00
Zen Dodd 59be391641 syscall: explain untrusted symlink refusal (#1074)
* syscall: explain untrusted symlink refusal
* syscall: emphasise insecure-links warning
2026-09-03 17:05:03 +10:00
Omar Elsayed a93490077d batch.c: Allow FIFO pipes in batch file processing. Regression fix (#1060) 2026-09-03 17:03:54 +10:00
Omar ElsayedandZen Dodd a68b32cd5c Fix ENOENT when resolving kernel pseudo-paths in ona_open (#1054)
* Fix ona_open to safely resolve bash process substitution pseudo-paths
Bash process substitution (e.g., `<(...)` or `>(...)`) exposes file
descriptors as symlinks under `/proc/self/fd/X` pointing to kernel
pseudo-paths such as `pipe:[12345]`. Previously, `ona_open()` would read
this target and attempt to resolve it as a literal file path on disk,
causing the operation to fail with `ENOENT` and breaking legitimate local
process substitution.

This patch safely intercepts and resolves these pseudo-paths while
maintaining strict confinement boundaries and averting TOCTOU risks:

- Detects kernel pseudo-paths (`pipe:[`, `socket:[`, `anon_inode:`)
  only when `fd_pin_tail` confirms the path resolves precisely to a
  direct child of a valid FD directory.
- Categorically rejects pseudo-path resolution if `confine_root` is
  active (yielding `ENOENT`).
- Strips `O_NOFOLLOW` for legitimate leaf pseudo-paths, allowing
  `openat()` to correctly delegate resolution.
- Reverts `fd_pin_tail` to its upstream signature, as manual PID
  validation is no longer required due to the secure `openat()` design.

* testsuite: expect pseudo-path skip on Alma
* syscall: reject trailing pseudo-path components

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-09-03 08:11:16 +10:00
LIChengGang a4bfa170fa main: allow --contimeout for daemon connections over --rsh (#1071)
* main: allow --contimeout for daemon connections over --rsh

The --contimeout guard rejected the option whenever connect_timeout was
set and the connection was not being made by a socket, so a daemon
reached through a remote shell (daemon_connection == 1) was refused the
same way as a plain non-daemon remote-shell transfer.  That is how
rsync-ssl talks to a daemon: it runs rsync with --rsh pointing at its
helper, so "rsync-ssl --contimeout=60 host::mod" died with "The
--contimeout option may only be used when connecting to an rsync
daemon" before it could even connect.

Only reject --contimeout when there is no daemon connection at all, so
the option is accepted for a daemon-via-rsh connection (and still works
unchanged for the socket daemon path, which returns earlier through
start_socket_client).

Add a regression test that drives a daemon-via-rsh invocation and
asserts the option is not rejected, while a plain remote-shell
destination is still refused.

* main, io: honor --contimeout for daemon-via-rsh connections

The previous commit made --contimeout legal for a daemon reached
through a remote shell, but rsync still did not act on it: the socket
path times its connect() via an alarm, while the daemon-via-rsh path
spawned the helper and then blocked in start_inband_exchange() waiting
for the daemon greeting with no connect timeout at all.  A hung helper
-- e.g. rsync-ssl's openssl connect to an unreachable host -- would hang
the transfer forever.

Give the client a second wall-clock deadline, client_connect_deadline,
set from --contimeout around do_cmd() + start_inband_exchange() and
cleared once the greeting exchange finishes.  handshake_poll_timeout_ms()
now also checks it and exits with RERR_CONTIMEOUT ("timeout waiting for
daemon connection"), matching the socket path's exit code, instead of the
daemon-handshake deadline's RERR_TIMEOUT.

This covers the helper spawn, its connect/TLS handshake, and the greeting
exchange as one connection-establishment phase, so it works uniformly for
openssl, stunnel, and gnutls rsync-ssl backends.

Extend the contimeout-rsh test to drive a helper that never connects and
assert rsync aborts with exit 35 within the timeout, and that a working
daemon-via-rsh connection still completes.
2026-09-03 08:10:05 +10:00
Samuel Henrique c529eccff7 options: accept --max-alloc=0 again, resolved to the parser's own ceiling (#1069)
* options: accept --max-alloc=0 again, resolved to the parser's own ceiling

3.5.0 rejected --max-alloc=0 (CVE-2026-53794).  The stated rationale was that a
zero cap "disabled the per-allocation size cap (the defense behind
CVE-2024-12084)".  That was true up to 3.2.7, where the check short-circuited:

	if (max_alloc && num >= max_alloc/size)

but 2f9b963a ("Make `--max-alloc=0` safer", 3.3.0) removed the short-circuit and
mapped 0 to SIZE_MAX at parse time, leaving the guard unconditional.  Since the
guard admits an allocation only when num < max_alloc/size, num*size stays below
max_alloc at every setting, so the num*size overflow check was armed for 0 just
as for any other value.  From 3.3.0 onward, 0 raised the magnitude ceiling and
nothing else -- and an explicit 8191P raises it exactly as far, is accepted, and
is forwarded to the peer, so rejecting 0 removed no capability.

What it did remove is the only portable spelling.  The parser's ceiling is
SIZE_MAX/2, so it tracks the build's word size: on ILP32 the suffix multiplier
alone exceeds the bound, making every P and T value an error whatever the digits
and capping the option at 2047M.  Because max_alloc_arg goes on the wire
un-normalized, a 0 was re-resolved by each side against its own SIZE_MAX; any
literal is resolved once on the client and shipped verbatim, so nothing above
2047M survives a 64-bit client talking to a 32-bit daemon.  There is no number a
user can compute that does what 0 did, which is what #1056 ran into.

So accept 0 again, but resolve it to SIZE_ARG_MAX (SIZE_MAX/2) rather than
SIZE_MAX, so it lands exactly on the largest value that could also be typed and
is no longer a limit only the 0 spelling can reach.  Keep forwarding it
verbatim: that per-side resolution is the property worth having.  This leaves
the substantive half of the 3.5.0 hardening -- bounding parse_size_arg() against
the unbounded `size *= atof(size_arg)` -- untouched.

The residual concern, a 0 forwarded to a <= 3.2.7 daemon that honours it, is not
something a client-side check can address: the client is the attacker's own
code, as daemon-max-alloc-zero_test.py noted in its own docstring.  Operators
who want peers kept off their cap have `refuse options = max-alloc`.

Also drops the now-unreachable rejection message, which left the min-value error
recommending a value the parser refused ("min: 1.00M or 0 for unlimited").

Tests: max-alloc-zero replaces max-alloc-zero-rejected and
daemon-max-alloc-zero, whose assertions are the behaviour being reverted.  It
checks that 0 is accepted, that it reaches the peer as the literal "0" rather
than a resolved number (verified against a negative control that normalizes it),
and that the parser's upper bound still rejects an out-of-range value.

Fixes #1056

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* testsuite: refresh Cygwin expectations
* docs: note max-alloc safety
2026-09-03 07:42:34 +10:00
Zen Dodd 270202affb ci: adjust Cygwin common skip expectations (#1073) 2026-09-03 07:22:38 +10:00
Zen Dodd 90992088d6 ci: apt-update before install (#1072) 2026-09-03 06:43:29 +10:00
Zayd Rajab 240bd9df96 syscall: use O_PATH for held directory traversal (#1065)
Use O_PATH on Linux for directory descriptors used only for path
traversal, fchdir(), or as *at() parents. Reopen final directory
endpoints with the caller-requested access mode and retain the existing
O_RDONLY fallback elsewhere.

Add coverage for exact sources and readable descendants beneath
search-only directories, known-file creation beneath write/search-only
destinations, and retained refusal to enumerate unreadable directories.

Fixes #1064.
2026-08-28 19:37:01 +10:00
LIChengGang 7c20b077c9 rsync-ssl, testsuite: accept --type=SSL_TYPE anywhere in the args (#1047)
* rsync-ssl, testsuite: accept --type=SSL_TYPE anywhere in the args

The --type=... option was only recognized as the first argument, so a
command such as "rsync-ssl --dry-run --type=stunnel host::mod" passed
the option through to the underlying rsync, which rejected it as
unknown.  Scan the full argument list for --type=..., export
RSYNC_SSL_TYPE, and drop the option before handing the remaining args
to rsync.  The manpage no longer says the option must be first.

Adds a test that runs rsync-ssl with a fake rsync in PATH and checks
that --type= is consumed in first, middle, and last positions.

* fix(rsync-ssl): stop interpreting wrapper options at --

rsync-ssl consumed a --type=... operand that appeared after a --
argument, even though -- explicitly protects the rest of the command
line from option parsing.  Stop scanning for --type=... at a --
argument: preserve the -- and every subsequent argument verbatim,
passing them through to rsync unchanged.  Add regression coverage for
this case and document the behavior in the manpage.

* test(rsync-ssl): assert RSYNC_SSL_TYPE and exit status in the type-option test

The rsync-ssl-type-option test only checked that --type= tokens were
removed from the rsync argv, so it would also pass if the wrapper
silently discarded the requested SSL implementation instead of
exporting RSYNC_SSL_TYPE.  The fake rsync now records both the argv it
receives and the RSYNC_SSL_TYPE value it observes; each invocation
asserts the expected value (or UNSET when the wrapper must not consume
anything), and run() fails the test unless rsync-ssl exits
successfully.
2026-08-20 19:27:55 +10:00
ᴄʜʀɪsᴛᴏᴘʜᴇʀ ᴍᴇɴɢ f0177d82a8 Add IDN support (#986)
* Add IDN support

rsync can now connect to IDN (internationalized domain name) hosts, and
IDN names are recognized in a daemon's hosts allow/deny.

* idn: convert host names label by label

The IDNA mapping folds some non-ASCII characters onto ASCII ones, so
running a whole hosts allow/deny token through idn2_to_ascii_8z() could
hand back a pattern the admin never wrote: a "*" (U+FF0A FULLWIDTH
ASTERISK) entry came back as "*" and let every host in.

Convert label by label instead, keeping an ASCII label byte for byte and
using a converted label only when it comes back as a bare A-label. An
ASCII-only config now behaves as it did before there was IDN support, and
a token that cannot be converted is left alone and so matches nothing. The
client side shares the same helper, and neither side truncates a name at
its 1024-byte buffer any more. strlower() folds only ASCII now, since its
one caller is the hosts allow/deny list, which can hold UTF-8.

Adds testsuite/daemon-access-idn and extends testsuite/idn to cover
Unicode, punycode, mixed-case and invalid input on both sides.

* idn: hand libidn2 the same flags on both sides

The client path called idn2_lookup_ul() without IDN2_NFC_INPUT while the
daemon path passed it to idn2_to_ascii_8z(). Both normalize either way --
idn2_lookup_ul() ors the flag in itself, and TR46 normalizes as it maps --
but there is no reason for the two calls to read differently, so pass one set
of flags from one place. The flag asks libidn2 to normalize the label rather
than promising that it already is: it gates the u32_normalize() call, and
without it a decomposed label comes back IDN2_NOT_NFC.

Adds composed/decomposed cases to testsuite/idn, which sees the exact host
name rsync hands out, and a decomposed hosts allow token to
testsuite/daemon-access-idn.
2026-08-20 18:51:23 +10:00
Samuel Henrique 0145b9128d testsuite: interpose the large-file spellings of open/openat/fstatat (#1063)
Which symbol names this test's LD_PRELOAD hook exports is decided by the
compiler that builds it, and which names rsync imports is decided by
configure -- and the two do not have to agree.

Where off_t is not already 64 bits, configure's AC_SYS_LARGEFILE adds
-D_FILE_OFFSET_BITS=64 (i386 and alpha in Debian), so glibc redirects every
open()/openat()/fstatat() call in rsync to open64(), openat64() and
fstatat64().  The hook is compiled by a bare "cc", so on those architectures
it defines only the unsuffixed names: the receiver's opens never reach it, no
EACCES is injected, the marker the positive control looks for is never
written, and the test fails with

    positive control failed: receiver did not open the existing partial file
    with O_CREAT (rc=0, output='')

It happens to work on Debian's 64-bit time_t ports (armhf, hppa, powerpc,
...) only by luck: their gcc predefines -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64,
so glibc's __REDIRECT renames the hook's own DEFINITIONS as well and it ends
up exporting exactly the *64 names rsync imports.

Define both spellings explicitly so the hook interposes whichever set the
rsync under test was linked against, and #undef the two macros at the top of
the hook so that renaming cannot happen -- otherwise, on precisely those ports
whose compiler predefines them, open() would be emitted as open64() and
collide with the explicit wrapper ("symbol `open64' is already defined"),
leaving the hook unbuildable and the test skipped.

The new pointers are resolved through hook_resolve(), so the existing
nested-dlsym recursion guard covers them as well.

Failing build logs:
  i386  https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=i386&ver=3.5.0%2Bds1-1&stamp=1786938290&raw=0
  alpha https://buildd.debian.org/status/fetch.php?pkg=rsync&arch=alpha&ver=3.5.0%2Bds1-1&stamp=1786973432&raw=0
2026-08-19 20:37:09 +10:00
Samuel Henrique 9371b33320 testsuite: probe the punch granularity each hole assertion relies on (#1062)
The last section of preallocate_test.py writes 256 blocks of
[4 KiB data][24 KiB zeros][4 KiB data] and requires --inplace --sparse to
deallocate at least half of the file.  A punch only frees whole allocation
units, and that interior run sits 4 KiB into each 32 KiB block: with a 4 KiB
unit it covers six whole units (24 KiB freed per block), but with a 16 KiB
unit it covers none at all -- [0,16K) holds live data at [0,4K) and [16K,32K)
holds live data at [28K,32K) -- so st_blocks cannot move whatever rsync does.

That fails the build on Debian loong64, whose kernel uses 16 KiB pages:

    --inplace --sparse left matching interior zero runs allocated: 8388608 of
    8388608 bytes remain allocated after a 8388608-byte matched-block update

i.e. exactly "nothing was freed".  fs_can_punch_holes() does not catch it
because it punches a whole 64 KiB file, which frees blocks at any granularity.

Generalise that helper into punch_frees(offset, length, size) so each
assertion can probe the shape it actually depends on, and gate the interior
one on the exact layout written just above it.  Where the filesystem can free
that run the assertion runs exactly as before; where it cannot, the test says
so rather than reporting a regression.  The content check is unconditional
either way.

While here, call fallocate64() instead of fallocate().  ctypes declares the
offset and length as c_longlong, but glibc's fallocate() takes 32-bit off_t
where off_t is 32 bits, so the callee reads the high half of `offset` as its
`length`, the call fails with EINVAL, and can_punch comes back False -- which
is why none of this file's hole-punching assertions have ever run on a 32-bit
port.  On i386:

    fs_can_punch_holes() as written : before=128 ret=-1 errno=22 -> False
    the same probe via fallocate64  : before=128 ret=0            -> True

fallocate64() takes off64_t on every architecture and is a plain alias of
fallocate() where off_t is already 64 bits.  The probe also writes urandom
rather than a repeated byte, so a filesystem that compresses does not answer
"nothing was freed" merely because nothing was allocated.
2026-08-19 20:26:04 +10:00
Zen Dodd 7f01ae0af1 testsuite: sort platform skip lists (#1061) 2026-08-16 22:02:30 +10:00
Zen Dodd 3d2caaa0cb testsuite: bound the unshare probe (#1049) 2026-08-16 21:55:00 +10:00
Zen Dodd 3b1eb8dd7f syscall: use O_PATH for directory traversal (#1052) 2026-08-16 21:52:11 +10:00
Zen Dodd 324a1f0716 rrsync: support fd pins in user namespaces (#1048)
* rrsync: support fd pins in user namespaces
* rrsync: support /dev/fd pins in user namespaces
2026-08-16 21:51:07 +10:00
Zen Dodd 010aa15dec testsuite: test install-strip tool selection (#1027) 2026-08-16 21:50:52 +10:00
Alessandro Di NepiandClaude Opus 4.8 a49f085a4f Honor $(STRIP) in install-strip for cross-compilation (#1024)
* Honor $(STRIP) in install-strip for cross-compilation

The install-strip target hard-coded `install -s`, which strips via the
install program using the build host's strip and ignores the STRIP
variable. When cross-compiling this runs the host strip against a
target binary and fails.

Pass --strip-program=$(or $(STRIP),strip) so the target strip is used
when STRIP is set (as cross toolchains and build systems provide),
falling back to plain `strip` for native builds. A plain `make install`
is unaffected.

* Make install-strip portable (address review)

- Detect the target strip via AC_CHECK_TOOL([STRIP],[strip],[strip]) in
  configure.ac (picks up the cross-prefixed strip when cross-compiling,
  defaults to plain strip otherwise) and substitute @STRIP@ in Makefile.in.
- Rewrite install-strip to run a normal install then $(STRIP) on the
  installed rsync binary, dropping the GNU Make $(or ...) and the GNU
  install --strip-program extension that broke with install-sh/BSD install.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-16 21:14:14 +10:00
Zen Dodd 4c029dd9d3 ci: stabilise Cygwin tests (#1057)
* ci: stabilise Cygwin tests
* ci: install Cygwin directly
* ci: wait for Cygwin setup
2026-08-15 17:03:31 +10:00
Zen Dodd 5351b53276 ci: run PR checks without labels (#1058) 2026-08-15 16:45:26 +10:00
Anja Lea Brinkmann ec52eed3ed Update COPYING.html (openssl & xxhash exceptions) (#1040) 2026-08-15 16:28:18 +10:00
Zen Dodd 195b4c6d30 testsuite: make basis xname oracle deterministic (#1051) 2026-08-15 16:19:15 +10:00
Zen Dodd bd48f0751b docs: clarify --files-from deletion scope (#1041) 2026-08-15 16:01:09 +10:00
Andrew Tridgell dcef974c7c packaging: fixed an error in release process 2026-08-13 10:42:24 +10:00
117 changed files with 7221 additions and 6242 deletions

No files matched your search

+5 -4
View File
@@ -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:
+8 -6
View File
@@ -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
+6 -5
View File
@@ -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 \
+7 -6
View File
@@ -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
+6 -5
View File
@@ -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
+50 -10
View File
@@ -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.
+6 -5
View File
@@ -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: |
+6 -5
View File
@@ -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
+6 -5
View File
@@ -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
+6 -5
View File
@@ -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
+6 -5
View File
@@ -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
+7 -9
View File
@@ -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)
+6 -5
View File
@@ -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
+7 -5
View File
@@ -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
+7 -5
View File
@@ -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
+7 -5
View File
@@ -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
+6 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()`),
+11
View File
@@ -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;
+2 -2
View File
@@ -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
View File
@@ -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);
}
+27 -24
View File
@@ -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;
}
+8 -8
View File
@@ -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
View File
@@ -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;
+2 -2
View File
@@ -58,7 +58,7 @@ void close_all(void)
max_fd = sysconf(_SC_OPEN_MAX) - 1;
for (fd = max_fd; fd >= 0; fd--) {
if ((ret = vfs_fstat(fd, &st)) == 0) {
if ((ret = do_fstat(fd, &st)) == 0) {
if (is_a_socket(fd))
ret = shutdown(fd, 2);
ret = close(fd);
@@ -198,7 +198,7 @@ NORETURN void _exit_cleanup(int code, const char *file, int line)
switch_step++;
if (cleanup_fname)
vfs_unlink(VFS_AT_FDCWD, cleanup_fname, 0);
do_unlink_at(cleanup_fname);
if (exit_code)
kill_all(SIGUSR1);
if (cleanup_pid && cleanup_pid == getpid()) {
+16 -24
View File
@@ -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
View File
@@ -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
View File
@@ -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. */
+9 -16
View File
@@ -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;
}
}
+21 -12
View File
@@ -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")))
+10 -10
View File
@@ -45,17 +45,17 @@ int sparse_end(int f, OFF_T size, int updating_basis_or_equiv)
int ret = 0;
if (updating_basis_or_equiv) {
if (sparse_seek && vfs_punch_hole(f, sparse_past_write, sparse_seek) < 0)
if (sparse_seek && do_punch_hole(f, sparse_past_write, sparse_seek) < 0)
ret = -1;
#ifdef HAVE_FTRUNCATE /* A compilation formality -- in-place requires ftruncate() */
else /* Just in case the original file was longer */
ret = vfs_ftruncate(f, size);
ret = do_ftruncate(f, size);
#endif
} else if (sparse_seek) {
#ifdef HAVE_FTRUNCATE
ret = vfs_ftruncate(f, size);
ret = do_ftruncate(f, size);
#else
if (vfs_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
if (do_lseek(f, sparse_seek-1, SEEK_CUR) != size-1)
ret = -1;
else {
do {
@@ -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));
+30 -28
View File
@@ -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
View File
@@ -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;
+2 -5
View File
@@ -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))
+33 -7
View File
@@ -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";
+4 -3
View File
@@ -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);
+37 -18
View File
@@ -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
+31 -18
View File
@@ -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)
+2 -2
View File
@@ -416,7 +416,7 @@ static int include_config(char *include, int manage_globals)
char *match = manage_globals ? "*.conf" : "*.inc";
int ret;
if (vfs_stat(VFS_AT_FDCWD, include, &sb, VFS_ALLOW_SYMLINK) < 0) {
if (do_stat(include, &sb) < 0) {
rsyserr(FLOG, errno, "unable to stat config file \"%s\"", include);
return 0;
}
@@ -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
View File
@@ -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();
+15 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
&lt;<a href="https://www.gnu.org/philosophy/why-not-lgpl.html">https://www.gnu.org/philosophy/why-not-lgpl.html</a>&gt;.
&lt;<a href="https://www.gnu.org/licenses/why-not-lgpl.html">https://www.gnu.org/licenses/why-not-lgpl.html</a>&gt;.
+47 -16
View File
@@ -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"
+39 -64
View File
@@ -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;
+4 -6
View File
@@ -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
+14
View File
@@ -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:
+32 -35
View File
@@ -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);
+8
View File
@@ -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. */
+8 -1
View File
@@ -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:
+1
View File
@@ -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
+4062
View File
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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)
+61 -137
View File
@@ -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(
+1 -1
View File
@@ -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(
+69
View File
@@ -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")
+169
View File
@@ -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")
+1 -3
View File
@@ -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 = [
-83
View File
@@ -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})")
+2 -3
View File
@@ -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.",
+186
View File
@@ -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}")
+97
View File
@@ -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")
+88
View File
@@ -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();
+40 -12
View File
@@ -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 "
+11 -5
View File
@@ -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()
+138
View File
@@ -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)
+80
View File
@@ -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")
+133
View File
@@ -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)
+95
View File
@@ -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")
+1 -7
View File
@@ -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
+77
View File
@@ -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')
+215
View File
@@ -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)
+1
View File
@@ -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 |
+9
View File
@@ -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
+1
View File
@@ -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
+5 -3
View File
@@ -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
+5 -2
View File
@@ -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
+14 -2
View File
@@ -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:
+5 -5
View File
@@ -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;
+5
View File
@@ -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
+584 -50
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 */
-2
View File
@@ -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
View File
@@ -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