mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-06-08 14:15:46 -04:00
A daemon module with path=/ makes F_PATHNAME absolute, so the secure_path built
for the content open starts with '/'. secure_relative_open() rejects an
absolute relpath with EINVAL, so a use-chroot=no daemon with path=/ could not
send any file ('failed to open ...: Invalid argument (22)') -- a regression
from 3.4.2. Strip leading slashes to a module-relative path; resolution stays
confined beneath module_dir.
80 lines
3.5 KiB
Python
80 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
# Functional regression: a daemon module with `path = /` (use chroot = no)
|
|
# cannot send ANY file in 3.4.3 -- every read fails with "Invalid argument (22)".
|
|
#
|
|
# Reported as #897 ("Regression in 3.4.3, Invalid argument (22) on all file
|
|
# reads when using native protocol"). Works in 3.4.2, works over a remote
|
|
# shell; only the native (daemon) protocol with an absolute module path breaks.
|
|
#
|
|
# Root cause: the 3.4.3 symlink-race hardening routes the sender's file open
|
|
# through secure_relative_open(module_dir, secure_path, ...) (sender.c), where
|
|
# secure_path = F_PATHNAME + "/" + f_name.
|
|
# With `path = /` the module-relative F_PATHNAME is itself ABSOLUTE, so
|
|
# secure_path starts with '/'. secure_relative_open()'s front door rejects any
|
|
# absolute relpath with EINVAL *before* it ever calls openat2 (matching the
|
|
# reporter's strace: the file is stat'd and access()'d but never opened). The
|
|
# generator then reports "send_files failed to open ...: Invalid argument (22)"
|
|
# and the whole transfer ends in code 23.
|
|
#
|
|
# This is a pure functional regression (no attacker, no symlink): XFAIL until
|
|
# the sender open is made to tolerate an absolute module-root path (the
|
|
# accompanying sender.c fix). Runs at any uid.
|
|
|
|
import subprocess
|
|
|
|
from rsyncfns import (
|
|
SCRATCHDIR, makepath, rmtree, rsync_argv, start_test_daemon, test_fail,
|
|
write_daemon_conf,
|
|
)
|
|
|
|
DAEMON_PORT = 12897
|
|
|
|
# A small source tree under the scratch dir: a file at the served-subdir root
|
|
# and one nested deeper (the bug fails on EVERY file, regardless of depth).
|
|
served = SCRATCHDIR / 'served'
|
|
dst = SCRATCHDIR / 'pulldst'
|
|
rmtree(served)
|
|
rmtree(dst)
|
|
makepath(served / 'sub')
|
|
makepath(dst)
|
|
(served / 'README').write_text("readme-contents\n")
|
|
(served / 'sub' / 'deep.txt').write_text("deep-contents\n")
|
|
|
|
# Module rooted at the filesystem root, exactly like the report (path = /,
|
|
# use chroot = no). We then request the served subtree by its absolute path
|
|
# with the leading '/' stripped, so the daemon serves $served from "/".
|
|
conf = write_daemon_conf([
|
|
('root', {'path': '/', 'read only': 'yes'}),
|
|
])
|
|
url = start_test_daemon(conf, DAEMON_PORT)
|
|
|
|
served_rel = str(served).lstrip('/') # e.g. tmp/.../served
|
|
proc = subprocess.run(
|
|
rsync_argv('-a', f'{url}root/{served_rel}/', f'{dst}/'),
|
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
|
)
|
|
out = proc.stdout or ''
|
|
print(out)
|
|
|
|
# Bug present: the sender refuses to open the files with EINVAL(22).
|
|
if 'Invalid argument (22)' in out or ('failed to open' in out and proc.returncode != 0):
|
|
from rsyncfns import test_xfail
|
|
test_xfail(
|
|
"#897: daemon module `path = /` (use chroot = no) cannot send files -- "
|
|
"`send_files failed to open ...: Invalid argument (22)`. The sender's "
|
|
"secure_relative_open(module_dir, secure_path) gets an ABSOLUTE "
|
|
"secure_path (F_PATHNAME is absolute when path=/) and the front door "
|
|
"rejects absolute relpaths with EINVAL before any openat2. To be closed "
|
|
"by letting the sender open succeed for an absolute module-root path.")
|
|
|
|
# Bug fixed (or never present): the files transfer intact.
|
|
if proc.returncode != 0:
|
|
test_fail(f"daemon pull failed unexpectedly (rc={proc.returncode}); "
|
|
f"not the #897 EINVAL symptom:\n{out}")
|
|
for rel in ('README', 'sub/deep.txt'):
|
|
got = dst / rel
|
|
if not got.is_file():
|
|
test_fail(f"daemon pull did not deliver {rel} (dst={dst})")
|
|
if got.read_text() != (served / rel).read_text():
|
|
test_fail(f"delivered {rel} content differs from source")
|