mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-13 05:38:57 -04:00
The outside-module victim-intact oracle holds at every protocol and stays unconditional. The stronger 'daemon actively rejects the forced --partial-dir operand' behavior is protocol-30+ only: at protocol 29 the operand is handled differently and the transfer completes normally with the victim still untouched (verified). So gate that check on protocol 30+, and at protocol 29 require the known-good normal outcome (rc==0, dest replaced) rather than skipping -- keeping the branch non-vacuous. Surfaced by the fleet's proto29 pass.
78 lines
2.8 KiB
Python
78 lines
2.8 KiB
Python
import os
|
|
import subprocess
|
|
|
|
from rsyncfns import (
|
|
SCRATCHDIR, forced_protocol, rmtree, rsync_argv, start_test_daemon,
|
|
test_fail, write_daemon_conf,
|
|
)
|
|
|
|
DAEMON_PORT = 12961
|
|
SECRET = "PROTECTED-OUTSIDE-MODULE\n"
|
|
PUSHED = "NEW\n"
|
|
OLD_DEST = "OLD-DESTINATION-FILE-CONTENT\n"
|
|
|
|
base = SCRATCHDIR / 'partialdirdaemon'
|
|
rmtree(base)
|
|
base.mkdir()
|
|
|
|
mod = base / 'mod'
|
|
secret = base / 'secret'
|
|
if secret.exists():
|
|
rmtree(secret)
|
|
secret.mkdir(parents=True)
|
|
|
|
victim = secret / 'f0'
|
|
victim.write_text(SECRET) # a file the partial must not clobber
|
|
mod.mkdir()
|
|
dest = mod / 'f0'
|
|
dest.write_text(OLD_DEST)
|
|
|
|
blink = mod / 'blink' # euid-owned symlink -> outside module
|
|
os.symlink(f'{secret}', blink)
|
|
|
|
src = base / 'src'
|
|
src.mkdir()
|
|
(src / 'f0').write_text(PUSHED)
|
|
|
|
conf = write_daemon_conf(
|
|
[('mod', {'path': str(mod), 'read only': 'no'})])
|
|
url = start_test_daemon(conf, DAEMON_PORT)
|
|
|
|
proc = subprocess.run(
|
|
rsync_argv('-a', '--delay-updates', '--partial-dir=/blink',
|
|
f'{src}/', f'{url}mod/'),
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
# The security oracle -- the outside-module victim must never be clobbered --
|
|
# holds at every protocol and is checked unconditionally below. The stronger
|
|
# "daemon actively rejects the forced --partial-dir operand" behavior only
|
|
# applies at protocol 30+; at protocol 29 the operand is handled differently and
|
|
# the transfer completes normally (dest replaced, victim still untouched), so
|
|
# gate just that secondary check.
|
|
proto = forced_protocol()
|
|
dest_after = dest.read_text() if dest.exists() else None
|
|
if proto is None or proto >= 30:
|
|
if proc.returncode == 0 or dest_after != OLD_DEST:
|
|
test_fail(
|
|
"the daemon did not reject the forced --partial-dir staging path: "
|
|
f"rsync exited {proc.returncode} and {dest} is now {dest_after!r}. "
|
|
"If the operand were ignored, the upload would replace the destination "
|
|
f"with {PUSHED!r}.")
|
|
else:
|
|
# At protocol 29 the operand is handled differently: the transfer must still
|
|
# complete normally (no escape) rather than fail for an unrelated reason, so
|
|
# require the known-good outcome instead of skipping the check entirely.
|
|
if proc.returncode != 0 or dest_after != PUSHED:
|
|
test_fail(
|
|
"protocol-29 --partial-dir run did not complete normally: rsync exited "
|
|
f"{proc.returncode} and {dest} is {dest_after!r} (expected a clean "
|
|
f"transfer replacing it with {PUSHED!r})")
|
|
|
|
after = victim.read_text() if victim.exists() else None
|
|
if after != SECRET:
|
|
test_fail(
|
|
"the daemon followed a --partial-dir symlink outside the module: "
|
|
f"{victim} is now {after!r} (a peer backed up over a file the module "
|
|
"does not serve).")
|
|
print("daemon confines a peer-supplied --partial-dir symlink to the served set")
|