Files
rsync/testsuite/daemon-chroot-munge-default_test.py
Andrew Tridgell e927a00b2e testsuite: pin documented option behaviour with 8 oracle tests
Add behaviour tests that nail down option semantics the man pages
describe vaguely, each verified to pass against both this branch and the
3.2.7 oracle (so they document long-standing behaviour, not regressions):

  daemon-strict-modes-matrix   secrets-file mode rule (st_mode & 06)
  daemon-chroot-munge-default  munge-symlinks default vs chroot/path /./
  safe-links-unsafe-def        --copy-unsafe-links lexical unsafe rule
  no-implied-dirs-symlink      --no-implied-dirs follows in-tree dest symlink
  files-from-path-clamp        --files-from collapse-then-reject ".."
  relative-implied-symlink     --relative sends implied dirs as real dirs
  keep-dirlinks-rule           --keep-dirlinks opening rule
  backup-dir-relative          --backup-dir resolves relative to dest

no-implied-dirs-symlink relies on the -R "/./" implied-dir marker, a
protocol-30+ feature; under protocol 29 the generator rejects the
multi-component path (same as the 3.2.7 oracle), so it passes through
without testing, matching the sibling relative-implied test.

daemon-chroot-munge-default needs root to exercise the chroot regimes
and skips otherwise; add it to RSYNC_EXPECT_SKIPPED only in the
almalinux-8 and cygwin workflows, which run make check non-root. The
macos workflow runs it as root, so the test runs there for real.
2026-07-20 14:05:31 +10:00

108 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Pin WHEN the daemon applies default symlink munging, by config regime.
The rsyncd.conf(5) `use chroot` entry says:
"If the daemon is serving the / dir (either directly or due to being chrooted
to the module's path), rsync does not do any path sanitizing or (default)
munging. When it has to limit access to a particular subdir (either due to
chroot being disabled or having an inside-chroot path set), rsync will munge
symlinks (by default) and sanitize paths."
This was flagged as a suspected code mismatch during the doc audit. It is NOT a
mismatch: clientserver.c derives `module_dirlen` (0 when the inside-chroot path
is "/", >0 for a real subdir) and sets
munge_symlinks default = !use_chroot || module_dirlen
so the doc's regime split is exactly right. This test confirms it by observing
the default (no explicit `munge symlinks`) across three regimes:
A. use chroot = no -> module_dirlen>0 -> munge ON
B. use chroot = yes, path=DIR -> chrooted to DIR, inside path "/", len 0 -> munge OFF
C. use chroot = yes, path=BASE/./SUB -> inside path "SUB", len>0 -> munge ON
Regimes B and C need root (chroot). A runs anywhere. The observable is whether
an uploaded symlink is stored verbatim or with the /rsyncd-munged/ prefix.
(Path sanitizing -- the other half of the same sentence -- is exercised by the
operator-path-* and daemon-scan-dir-escape tests.)
"""
import os
import subprocess
from rsyncfns import (
SCRATCHDIR,
rmtree, rsync_argv, start_test_daemon, test_fail, test_skipped,
write_daemon_conf,
)
DAEMON_PORT = 12911
LINKVAL = 'realfile' # a plain rel-within target
base = SCRATCHDIR / 'chroot-munge'
rmtree(base)
base.mkdir(parents=True)
# Upload source: one symlink we can inspect after it lands.
srcup = base / 'srcup'
srcup.mkdir()
os.symlink(LINKVAL, srcup / 'sl')
is_root = os.geteuid() == 0
# Module A: no chroot. Modules B/C: chroot (root only).
mod_a_dir = base / 'a_root'
mod_b_dir = base / 'b_root'
mod_c_base = base / 'c_base'
mod_c_sub = mod_c_base / 'sub'
for d in (mod_a_dir, mod_b_dir, mod_c_base, mod_c_sub):
d.mkdir(parents=True)
modules = [('a_nochroot', {'path': str(mod_a_dir), 'use chroot': 'no',
'read only': 'no'})]
if is_root:
modules += [
('b_chroot_root', {'path': str(mod_b_dir), 'use chroot': 'yes',
'read only': 'no'}),
('c_chroot_sub', {'path': f'{mod_c_base}/./sub', 'use chroot': 'yes',
'read only': 'no'}),
]
conf = write_daemon_conf(modules, name='chroot-munge.conf')
url = start_test_daemon(conf, DAEMON_PORT)
def stored_link(module, landing_dir):
"""Upload the symlink into `module`; return the value as stored on disk."""
subprocess.run(rsync_argv('-al', f'{srcup}/', f'{url}{module}/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
sl = landing_dir / 'sl'
if not sl.is_symlink():
test_fail(f"{module}: uploaded symlink did not land at {sl}")
return os.readlink(sl)
MUNGED = '/rsyncd-munged/' + LINKVAL
# A: no chroot -> default munge ON.
got = stored_link('a_nochroot', mod_a_dir)
if got != MUNGED:
test_fail(f"use chroot=no: expected default munge ({MUNGED!r}), stored {got!r}")
if is_root:
# B: chrooted to the module path, inside path "/" -> default munge OFF.
got = stored_link('b_chroot_root', mod_b_dir)
if got != LINKVAL:
test_fail(f"use chroot=yes serving '/': expected NO munge ({LINKVAL!r}), "
f"stored {got!r}")
# C: chroot with a /./ inner subdir -> default munge ON.
got = stored_link('c_chroot_sub', mod_c_sub)
if got != MUNGED:
test_fail(f"use chroot=yes with inside-path 'sub': expected default munge "
f"({MUNGED!r}), stored {got!r}")
print("daemon-chroot-munge-default: default munge OFF when serving '/', ON "
"for a non-chroot module and for a chroot module with an inside subdir "
"-- the use-chroot doc regime split is accurate")
else:
test_skipped("chroot regimes (B/C) need root; verified the no-chroot regime "
"munges by default")