Files
rsync/testsuite/operator-path-dir-daemon-outside_test.py
Andrew TridgellandOmar Elsayed a55ab05c19 testsuite: daemon exclude/filter name-based behaviour
The exclude/filter is a name filter, not a symlink boundary (3.2.7-equivalent):
symlink-exclude family, daemon-exclude-namebased, the operator-path exclude /
traversal / dir-daemon cases, filter-merge and implied-trailing-backslash.

Co-authored-by: Omar Elsayed <omarelsayed161@gmail.com>
2026-07-20 14:05:31 +10:00

57 lines
2.4 KiB
Python

#!/usr/bin/env python3
# A daemon must not let a peer-supplied --partial-dir/--backup-dir symlink resolve
# OUTSIDE the served module at all (not just into an excluded subtree).
# (Companion to the exclude-bypass tests; reported via Omar Elsayed's findings.)
#
# An in-module symlink owned by the daemon euid whose target is ABSOLUTE (or
# climbs out with ../) points to a sibling directory outside the module root.
# An unprivileged peer overwrites a dest file with --backup/--partial-dir aimed
# at that symlink, moving/staging the data OUTSIDE the served set -- modifying
# files the module never exposed. The module-confined resolver must refuse a
# resolved path that leaves the module root.
#
# Runs unprivileged: the boundary under test is the module root, not a uid.
import os
import subprocess
from rsyncfns import (
SCRATCHDIR, rmtree, rsync_argv, start_test_daemon, test_fail, write_daemon_conf,
)
def run_case(label, opt, extra, port):
base = SCRATCHDIR / f'outside-{label}'
rmtree(base)
base.mkdir()
mod = base / 'mod'
mod.mkdir()
outside = base / 'outside' # sibling of the module -- NOT served
outside.mkdir()
(mod / 'f0').write_text("OLD-DEST-CONTENT\n") # existing dest -> overwritten
src = base / 'src'
src.mkdir()
(src / 'f0').write_text("NEW\n")
# euid-owned symlink inside the module, absolute target outside the module.
os.symlink(str(outside), mod / 'elink')
conf = write_daemon_conf(
[('mod', {'path': str(mod), 'read only': 'no'})],
{'pid file': str(SCRATCHDIR / f'rsyncd-outside-{label}.pid')},
name=f'outside-{label}.conf')
url = start_test_daemon(conf, port)
subprocess.run(
rsync_argv('-a', *extra, f'{opt}=/elink', f'{src}/', f'{url}mod/'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
leaked = [os.path.join(r, f) for r, _d, fs in os.walk(outside) for f in fs]
if leaked:
test_fail(
f"{label}: the daemon followed an in-module symlink whose target is "
f"OUTSIDE the module root and wrote there: {leaked} (a peer modified "
"files the module does not serve). The resolver must confine an "
"operator/peer path to the module root.")
run_case('backup', '--backup-dir', ['--backup'], 12905)
run_case('partial', '--partial-dir', [], 12909)
print("daemon confines peer --partial-dir/--backup-dir symlinks to the module root")