mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-07-31 15:56:10 -04:00
The symlink-race hardening routed the receiver's basis open through secure_relative_open(), which rejects any '..' -- so a sibling --link-dest=../01 on a use-chroot=no daemon was silently ignored and every file re-transferred (#915/#928, a regression from 3.4.1). Narrow the confinement to the sanitizing daemon (am_daemon && !am_chrooted) and re-anchor it at the module root, the real trust boundary: secure_relative_open() prefixes the cwd's module-relative path (from rsync's logical curr_dir[], a guaranteed lexical prefix of module_dir) and resolves beneath module_dir, so RESOLVE_BENEATH permits an in-module '..' climb while still rejecting one that escapes the module. secure_basis_open() opens with a bare do_open() in the non-sanitizing cases. t_stub.c gains weak curr_dir[]/curr_dir_len for the helpers (via #pragma weak on non-GNU compilers, where rsync.h erases __attribute__). Two tests: link-dest-relative-basis asserts the in-module '..' is honoured; link-dest-module-escape asserts a --link-dest=../../OUTSIDE climb that leaves the module is refused (not hard-linked to an outside file). See upstream PR #930.
64 lines
2.5 KiB
Python
64 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
# Functional regression: a relative --link-dest=../sibling against a daemon
|
|
# module with `path = /` (the intersection of #897 and #915).
|
|
#
|
|
# #915 re-anchors the receiver's basis open at the module root so an in-module
|
|
# "../01" climb is honoured. The gate keyed on a nonzero module_dirlen, but a
|
|
# `path = /` module has module_dirlen == 0 (clientserver.c), so the re-anchor
|
|
# was skipped there and --link-dest=../01 was silently ignored (every file
|
|
# re-transferred) even though plain #915 modules were fixed.
|
|
#
|
|
# Like link-dest-relative-basis this XFAILs on platforms without
|
|
# openat2/O_RESOLVE_BENEATH (the portable resolver rejects the '..' for safety);
|
|
# it flips to PASS where the kernel can adjudicate the in-module climb. Runs at
|
|
# any uid.
|
|
|
|
import shutil
|
|
import subprocess
|
|
|
|
from rsyncfns import (
|
|
SCRATCHDIR, make_data_file, makepath, rmtree, rsync_argv, start_test_daemon,
|
|
test_fail, test_xfail, write_daemon_conf,
|
|
)
|
|
|
|
DAEMON_PORT = 12931
|
|
DATA_SIZE = 40000
|
|
|
|
# dest 00 and basis 01 live side by side under `base`; the module is rooted at
|
|
# "/", so the served subtree is addressed by its absolute path minus the leading
|
|
# slash, and --link-dest=../01 climbs dest 00 -> sibling 01 (both inside /).
|
|
base = SCRATCHDIR / 'bakroot'
|
|
src = SCRATCHDIR / 'srcroot'
|
|
rmtree(base)
|
|
rmtree(src)
|
|
makepath(base / '01', src)
|
|
make_data_file(src / 'f.dat', DATA_SIZE)
|
|
shutil.copy2(src / 'f.dat', base / '01' / 'f.dat')
|
|
|
|
conf = write_daemon_conf([
|
|
('root', {'path': '/', 'read only': 'no'}),
|
|
])
|
|
url = start_test_daemon(conf, DAEMON_PORT)
|
|
|
|
base_rel = str(base).lstrip('/') # address `base` via the path=/ module
|
|
rmtree(base / '00')
|
|
proc = subprocess.run(
|
|
rsync_argv('-a', '--link-dest=../01', f'{src}/', f'{url}root/{base_rel}/00/'),
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
out = proc.stdout or ''
|
|
if proc.returncode not in (0, 23): # 23: no-RESOLVE_BENEATH platforms reject the basis
|
|
test_fail(f"path=/ --link-dest push failed unexpectedly (rc={proc.returncode}):\n{out}")
|
|
|
|
dest = base / '00' / 'f.dat'
|
|
basis = base / '01' / 'f.dat'
|
|
if not dest.is_file():
|
|
test_fail(f"destination file missing ({dest})")
|
|
|
|
ds, bs = dest.stat(), basis.stat()
|
|
if (ds.st_dev, ds.st_ino) != (bs.st_dev, bs.st_ino):
|
|
test_xfail(
|
|
"#915 (path=/ case): a `path = /` daemon module ignored --link-dest=../01 "
|
|
"(module_dirlen==0 skipped the re-anchor) -- the file was re-transferred "
|
|
"instead of hard-linked. Honoured once the re-anchor covers path=/.")
|
|
# Honoured: the dest is hard-linked to the in-module sibling basis.
|