Files
rsync/testsuite/deep-path_test.py
T
Andrew TridgellandOmar Elsayed 4705b6fb92 testsuite: symlink-race and operator/peer path-resolution coverage
The TOCTOU / symlink-race suite for the secure resolver and operator-supplied
paths: chdir/chmod/rename/mknod/source/dest symlink races, relative make_path and
symlinked-parent cases, the operator-path matrix (--temp/partial/backup-dir,
alt-dest basis, files-from, log-file, insecure-links), and the admin-file opens
(--password-file / daemon secrets / config / log-file / early-input symlinks),
plus the daemon module-confinement and chroot inner-module cases.

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

60 lines
2.5 KiB
Python

#!/usr/bin/env python3
# Exercise the DPC_MAXDEPTH (64) boundary of the held ancestor-dirfd cache.
#
# The receiver/generator (get_dir_fd) and the sender (held_dir_path_fd) resolve
# a path against a persistent ancestor-dirfd stack capped at DPC_MAXDEPTH=64
# path components (syscall.c). A directory deeper than that is declined by the
# cache (dpc_dir_fd returns -1) and must fall back to the un-cached
# secure_relative_open() walk -- which handles arbitrary depth -- and still
# produce a byte-identical result with no overflow/crash at the array bound.
#
# We build a single chain with a file at EVERY level past the cap, so dir-depths
# both within the cache (<=64 components, fast path) and beyond it (>64, the
# fallback) are transferred in one run, exercising the fast-path -> fallback
# handoff on the sender AND the receiver (whose caches are independent).
# Plain local transfer + a no-chroot daemon push; no root required.
import os
from rsyncfns import (
SCRATCHDIR, checkit, make_tree, rmtree, start_test_daemon, test_fail,
test_skipped, walk_files, write_daemon_conf,
)
DEPTH = 70 # > DPC_MAXDEPTH (64): the deepest files use the fallback
DAEMON_PORT = 12905
base = SCRATCHDIR / 'deep-path'
src = base / 'src'
rmtree(base)
# One deep chain, a file at each level: src/f0, src/d1/f1, ..., src/d1/.../d70/f70.
try:
make_tree(src, depth=DEPTH, data=True)
except OSError as e:
test_skipped(f"cannot build a {DEPTH}-deep tree ({e})")
# Sanity: the deepest file's parent really has more components than the cache
# cap, so the fallback is genuinely exercised (not just the fast path).
deepest = max(walk_files(src), key=lambda p: len(p.relative_to(src).parts))
parent_components = len(deepest.relative_to(src).parts) - 1 # drop the filename
if parent_components <= 64:
test_fail(f"deep tree only {parent_components} components deep -- "
"does not cross DPC_MAXDEPTH (64); test is ineffective")
# --- local transfer: sender AND receiver caches both cross the boundary -----
dest = base / 'dest'
checkit(['-a', f'{src}/', str(dest)], src, dest)
# --- daemon push: the daemon receiver uses the confined resolver ------------
mod = base / 'module'
mod.mkdir(parents=True, exist_ok=True)
conf = write_daemon_conf([
('deep', {'path': str(mod), 'use chroot': 'no', 'read only': 'no'}),
])
daemon_url = start_test_daemon(conf, DAEMON_PORT).rstrip('/')
checkit(['-a', f'{src}/', f'{daemon_url}/deep/'], src, mod, allowed_codes=(0, 23))
print(f"deep-path: {DEPTH}-deep tree round-trips past the dir-fd cache cap "
"(local + daemon)")