Files
rsync/testsuite/sender-remove-source-secure_test.py
T
Andrew Tridgell 653c4e8db9 testsuite: make --race-timeout actually control the race budget
The race tests are the suite's slowest by a wide margin -- a race test is
a negative oracle, so it passes by spending its entire budget. Most of
them wrote `max(RACE_TIMEOUT, 10.0)`, which ignored --race-timeout below
10s: the documented knob did nothing for 10 of the 16 tests.

Replace the floor idiom with race_budget(default), where the per-test
default applies only when the operator did not pass --race-timeout, and
runtests.py exports race_timeout only when the flag was actually given.
Defaults are unchanged (measured identical at 15.3s/10.3s/5.2s).

Validate the value rather than take it on trust. A race test loops
`while monotonic() < deadline`, so a zero, negative or NaN budget runs the
body zero times and the test reports PASS without ever racing, and an
infinite one runs until the unrelated per-test timeout; the old
max(..., 10.0) floor had made all of that unreachable, so removing the
floor had to come with rejecting the input. An unparsable value in the
environment counts as unset for the same reason -- falling back to the 5s
baseline while still counting as "set" would silently halve a 10s or 15s
oracle that nobody asked to shorten.

NB the *_test.py glob spans four committed symlinks (chown-fake,
devices-fake, exclude-lsh, xattrs-hlink); sed -i would replace each with a
copy of its target, so they are rewritten with --follow-symlinks semantics
and left as symlinks.
2026-08-02 16:05:24 +10:00

71 lines
2.8 KiB
Python

#!/usr/bin/env python3
import os
import platform
import subprocess
import time
from rsyncfns import (
race_budget, SCRATCHDIR, rmtree, rsync_argv,
start_path_flipper, start_test_daemon, stop_flipper, test_fail, test_xfail,
write_daemon_conf,
)
_CYGWIN = platform.system().startswith('CYGWIN')
# The sender's secure parent-dir open resolves on held dirfds with O_NOFOLLOW
# (race-free by construction on every platform), so a flipped parent component
# cannot redirect the --remove-source-files unlink outside the module.
base = SCRATCHDIR / 'sender-remove-source'
mod = base / 'module'
outside = base / 'outside'
dest = base / 'dest'
rmtree(base)
(mod / 'real').mkdir(parents=True)
outside.mkdir(parents=True)
dest.mkdir(parents=True)
inside_file = mod / 'real' / 'file'
outside_file = outside / 'file'
inside_file.write_text('payload\n')
outside_file.write_text('payload\n')
st = inside_file.stat()
os.utime(outside_file, (st.st_atime, st.st_mtime))
os.symlink(outside, mod / 'evil')
conf = write_daemon_conf([
('src', {'path': str(mod), 'read only': 'no', 'use chroot': 'no'}),
])
url = start_test_daemon(conf, 12937)
flip = start_path_flipper(mod / 'real', mod / 'evil')
deadline = time.monotonic() + race_budget()
try:
while time.monotonic() < deadline and outside_file.exists():
if not inside_file.exists() and (mod / 'real').is_dir() and not os.path.islink(mod / 'real'):
try:
inside_file.write_text('payload\n')
os.utime(inside_file, (st.st_atime, st.st_mtime))
except FileNotFoundError:
pass
subprocess.run(
rsync_argv('-a', '--remove-source-files', f'{url}src/real/file', str(dest) + '/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True)
finally:
stop_flipper(flip)
if not outside_file.exists():
if _CYGWIN:
# Cygwin resolves directory descriptors by path name rather than a pinned
# inode and emulates symlinks as special files, so the confined sender's
# held-fd O_NOFOLLOW walk -- though compiled in -- does not enforce the
# parent-component pin here, and a raced parent flip can still redirect
# the --remove-source-files unlink. Documented Cygwin platform residual
# (see SECURITY.md and the matching xfail in symlink-race-source_test.py);
# Cygwin is a dev/interop target, not a privilege boundary host.
test_xfail("cygwin: --remove-source-files parent-flip race still unlinks "
"the outside victim -- documented Cygwin platform residual")
test_fail("daemon sender --remove-source-files cleanup unlinked the outside victim through a raced parent symlink")
print("sender-remove-source-secure: remove-source cleanup did not unlink outside the module")