#!/usr/bin/env python3"""Daemon-mode: the server must reject a wire-supplied --max-alloc=0.max-alloc-zero-rejected_test.py only proves the *local* client refuses--max-alloc=0. That alone doesn't protect a daemon: a modified or older clientstill forwards --max-alloc=0 on the wire, and an unpatched daemon honours it anddisables its my_alloc() allocation cap (the defence behind CVE-2024-12084 andfriends). This test drives an older rsync client -- which lacks the reject-zerocheck and so forwards the option -- against the current rsync daemon, andasserts the *daemon* refuses it.It uses the in-tree old_versions/rsync_3.2.7 as the client (3.2.7 predates thereject-zero fix, so it forwards --max-alloc=0 on the wire). If that binary ismissing or can't run here (e.g. a non-Linux host that can't run the staticarchive) the test skips."""import subprocessfrom pathlib import Pathfrom rsyncfns import ( FROMDIR, RSYNC, SCRATCHDIR, makepath, rmtree, start_test_daemon, test_fail, test_skipped, write_daemon_conf,)DAEMON_PORT = 12932REJECT_MSG = 'max-alloc must be greater than zero'OLD_CLIENT = Path(__file__).resolve().parents[1] / 'old_versions' / 'rsync_3.2.7'if not OLD_CLIENT.exists(): test_skipped(f"{OLD_CLIENT} not present")# Confirm the static binary actually runs as rsync on this OS/arch before we# depend on it: exec of a foreign-arch/OS binary raises OSError, while one that# loads but can't run won't print the rsync banner. (3.2.7 predates the# reject-zero fix, so once it runs it forwards --max-alloc=0 on the wire.)try: probe = subprocess.run([str(OLD_CLIENT), '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)except OSError as e: test_skipped(f"cannot run {OLD_CLIENT.name} on this OS/arch: {e}")if probe.returncode != 0 or 'version 3.2.7' not in probe.stdout: test_skipped(f"{OLD_CLIENT.name} does not run as rsync on this OS/arch")# Module served by the *current* (patched) daemon.src = FROMDIRrmtree(src)makepath(src)(src / 'file.txt').write_text('hello\n')conf = write_daemon_conf([('mod', {'path': str(src), 'read only': 'yes'})])url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)dest = SCRATCHDIR / 'out.txt'def run_client(*extra): argv = [str(OLD_CLIENT), *extra, f'{url}mod/file.txt', str(dest)] return subprocess.run(argv, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)# Positive control: the old client and current daemon transfer fine without the# option, so the failure below is specifically the daemon refusing the option.dest.unlink(missing_ok=True)ctrl = run_client()if ctrl.returncode != 0: test_fail(f"old client could not talk to the current daemon:\n{ctrl.stderr}")# The attack: a forwarded --max-alloc=0 must be refused by the daemon.dest.unlink(missing_ok=True)proc = run_client('--max-alloc=0')if proc.returncode == 0: test_fail("daemon accepted a wire-supplied --max-alloc=0")if REJECT_MSG not in proc.stderr: test_fail("daemon did not reject --max-alloc=0 with the expected message; " f"stderr:\n{proc.stderr}")print("daemon-max-alloc-zero: daemon refuses a wire-supplied --max-alloc=0 " f"(client {OLD_CLIENT.name})")
#!/usr/bin/env python3
"""Daemon-mode: the server must reject a wire-supplied --max-alloc=0.
max-alloc-zero-rejected_test.py only proves the *local* client refuses
--max-alloc=0. That alone doesn't protect a daemon: a modified or older client
still forwards --max-alloc=0 on the wire, and an unpatched daemon honours it and
disables its my_alloc() allocation cap (the defence behind CVE-2024-12084 and
friends). This test drives an older rsync client -- which lacks the reject-zero
check and so forwards the option -- against the current rsync daemon, and
asserts the *daemon* refuses it.
It uses the in-tree old_versions/rsync_3.2.7 as the client (3.2.7 predates the
reject-zero fix, so it forwards --max-alloc=0 on the wire). If that binary is
missing or can't run here (e.g. a non-Linux host that can't run the static
archive) the test skips.
"""
import subprocess
from pathlib import Path
from rsyncfns import (
FROMDIR, RSYNC, SCRATCHDIR,
makepath, rmtree, start_test_daemon, test_fail, test_skipped,
write_daemon_conf,
)
DAEMON_PORT = 12932
REJECT_MSG = 'max-alloc must be greater than zero'
OLD_CLIENT = Path(__file__).resolve().parents[1] / 'old_versions' / 'rsync_3.2.7'
if not OLD_CLIENT.exists():
test_skipped(f"{OLD_CLIENT} not present")
# Confirm the static binary actually runs as rsync on this OS/arch before we
# depend on it: exec of a foreign-arch/OS binary raises OSError, while one that
# loads but can't run won't print the rsync banner. (3.2.7 predates the
# reject-zero fix, so once it runs it forwards --max-alloc=0 on the wire.)
try:
probe = subprocess.run([str(OLD_CLIENT), '--version'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True)
except OSError as e:
test_skipped(f"cannot run {OLD_CLIENT.name} on this OS/arch: {e}")
if probe.returncode != 0 or 'version 3.2.7' not in probe.stdout:
test_skipped(f"{OLD_CLIENT.name} does not run as rsync on this OS/arch")
# Module served by the *current* (patched) daemon.
src = FROMDIR
rmtree(src)
makepath(src)
(src / 'file.txt').write_text('hello\n')
conf = write_daemon_conf([('mod', {'path': str(src), 'read only': 'yes'})])
url = start_test_daemon(conf, DAEMON_PORT, rsync_cmd=RSYNC)
dest = SCRATCHDIR / 'out.txt'
def run_client(*extra):
argv = [str(OLD_CLIENT), *extra, f'{url}mod/file.txt', str(dest)]
return subprocess.run(argv, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, text=True)
# Positive control: the old client and current daemon transfer fine without the
# option, so the failure below is specifically the daemon refusing the option.
dest.unlink(missing_ok=True)
ctrl = run_client()
if ctrl.returncode != 0:
test_fail(f"old client could not talk to the current daemon:\n{ctrl.stderr}")
# The attack: a forwarded --max-alloc=0 must be refused by the daemon.
dest.unlink(missing_ok=True)
proc = run_client('--max-alloc=0')
if proc.returncode == 0:
test_fail("daemon accepted a wire-supplied --max-alloc=0")
if REJECT_MSG not in proc.stderr:
test_fail("daemon did not reject --max-alloc=0 with the expected message; "
f"stderr:\n{proc.stderr}")
print("daemon-max-alloc-zero: daemon refuses a wire-supplied --max-alloc=0 "
f"(client {OLD_CLIENT.name})")