83 lines · 3.2 KB · Python
RawDownload
#!/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 Path from 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})")

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t