mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-13 05:38:57 -04:00
The malformed-peer fuzz/crash regression tests: io flood/argv, proto cleared-flist/ ndx/hlink/selftest, match nullmap, max-alloc / size-arg / argv limits, sum-blocklen, nested-socket specials, malicious-sender delete scope, xattr-wire-cap.
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
#!/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})")
|