Files
rsync/testsuite/rrsync-debug-denied_test.py
Andrew Tridgell 0f6f35e522 exclude: give the ":e" self-exclude rule its merge rule's provenance
The exclude-self rule that a ":e" merge synthesizes is built by hand with
new0(), so it inherited no flags.  While the merge file was still being
parsed the global parse state masked that, but once parsing finished the
stored rule looked argument-origin, and report_filter_result() printed its
pattern -- a merge file's own text -- verbatim:

    [sender] hiding file PAT-x9 because of pattern PAT-[x]9 [per-dir ...]

Plain -vv reaches this on a stock client; no --debug is involved.  That is
the fifth site of this shape, and the first to get there by constructing a
rule rather than by printing one, so the redaction helper could not catch it.

Also fix the location a per-directory merge reports.  Its fname points into
dirbuf, which is cut back to the directory before the name was saved, so the
error said "<rule from .../src/ line 1>" instead of naming .rsync-filter --
no leak, but it breaks the "redact what, keep where" bargain the rest of this
work depends on.  Save the name before the truncation.

The rrsync test's claim to close "the rest of the FILTER trace family" was
too strong and is corrected: options.c maps verbosity onto the debug flags,
so -vvv still raises a restricted server to FILTER2 and its trace metadata
comes back.  Rule text stays redacted at every verbosity, which is the
property that matters; -vvv is added to the unaffected-transfer cases.
2026-08-02 20:32:33 +10:00

79 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""rrsync must not let a peer turn on the server's --debug output.
Defence in depth, not the fix: what actually stops a merge file's contents
coming back is the redaction in exclude.c (see filter-merge-content-echo).
This closes ONE spelling of the control -- an explicit -M--debug -- and no
more. It does not close the FILTER trace family: options.c maps verbosity
onto the debug flags, so -vvv still raises a restricted server to FILTER2 and
forwards it, and the trace metadata (merge-file openability, the server's
absolute paths, mergelist bookkeeping) comes back with it. Rule TEXT stays
redacted at every verbosity, which is the property that matters and which
filter-merge-content-echo pins. Do not read this test as sealing -vvv.
It costs nothing: server_options() forwards --info but never --debug, so no
stock client sends one, and the only way it reaches a server is a peer asking
for it by name with -M.
"""
import os
import signal
import subprocess
import sys
signal.signal(signal.SIGUSR1, signal.SIG_IGN)
signal.signal(signal.SIGUSR2, signal.SIG_IGN)
if '--shell' in sys.argv:
index = sys.argv.index('--shell') + 2
env = {**os.environ, 'SSH_ORIGINAL_COMMAND': ' '.join(sys.argv[index:])}
signal.signal(signal.SIGUSR1, signal.SIG_DFL)
signal.signal(signal.SIGUSR2, signal.SIG_DFL)
wrapper, root = env['RRSYNC_WRAPPER'], env['RRSYNC_ROOT']
os.execve(wrapper, [wrapper, '-wo', root], env)
from rsyncfns import RSYNC, SCRATCHDIR, makepath, patched_rrsync, rmtree, rsync_argv
base = SCRATCHDIR / 'rrsync-debug-denied'
rmtree(base)
src = base / 'src'
root = base / 'root'
makepath(src, root)
(src / 'f').write_bytes(b'hi\n')
# RSYNC may be a multi-word command (the runner's --protocol=N, or valgrind)
# while rrsync hands its RSYNC to execlp() as a single executable name, so it
# has to be wrapped before patched_rrsync() sees it.
shim = base / 'rsync-shim'
shim.write_text('#!/bin/sh\nexec ' + RSYNC + ' "$@"\n')
shim.chmod(0o755)
wrapper = patched_rrsync(base, rsync_path=str(shim))
rsh = f'{sys.executable} {os.path.abspath(__file__)} --shell'
env = {**os.environ, 'RRSYNC_WRAPPER': str(wrapper), 'RRSYNC_ROOT': str(root)}
for spelling in ('-M--debug=FILTER2', '--remote-option=--debug=ALL3'):
got = subprocess.run(
rsync_argv('-r', spelling, '-e', rsh, f'{src}/', 'ignored:'),
env=env, capture_output=True, text=True, timeout=20)
out = got.stdout + got.stderr
assert got.returncode != 0 and 'option --debug has been disabled' in out, (
f'rrsync accepted a peer-selected {spelling}: rc={got.returncode}, '
f'out={out!r}')
# The option table is generated by the cull-options script, so the entry this
# relies on can come back on a regeneration. Check the value, not just the
# behaviour, so that is unambiguous when it happens.
assert "'debug': -1," in wrapper.read_text(), (
"support/rrsync no longer disables 'debug' -- a cull-options regeneration "
'probably restored it')
# Ordinary transfers must be unaffected -- including -vvv, which reaches
# FILTER2 by verbosity alone and so must NOT be caught by the refusal above.
for extra in ([], ['-vv'], ['-vvv']):
ok = subprocess.run(
rsync_argv('-r', *extra, '-e', rsh, f'{src}/', 'ignored:'),
env=env, capture_output=True, text=True, timeout=20)
assert ok.returncode == 0 and (root / 'f').read_bytes() == b'hi\n', (
f'an ordinary transfer broke with {extra}: rc={ok.returncode}, '
f'stderr={ok.stderr!r}')