testsuite: probe RESOLVE_BENEATH support functionally for the #715 test

Add resolve_beneath_supported() to rsyncfns: it functionally probes whether the
rsync binary can follow an in-tree directory symlink under its secure resolver
(an initial transfer plus a delta update through a dir-symlink, the operation
issue #715 is about). This tracks the actual binary instead of a platform name.

Use it in symlink-dirlink-basis_test.py in place of the SunOS/OpenBSD/NetBSD/
Cygwin name check: it skips on those platforms too, and additionally on
Linux < 5.6, a seccomp-blocked openat2, and the new --disable-openat2 build,
where the portable O_NOFOLLOW fallback rejects the in-tree symlink.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrew Tridgell
2026-05-24 08:12:39 +10:00
parent e57c7f5d87
commit b0ba699031
2 changed files with 54 additions and 5 deletions

View File

@@ -1126,6 +1126,48 @@ def assert_not_exists(path, label: str = '') -> 'None':
test_fail(f"{_tag(label)}{path} exists but should not")
_rb_cache = None
def resolve_beneath_supported() -> bool:
"""True if this rsync can FOLLOW an in-tree directory symlink under its
secure resolver -- i.e. update a file through a dir-symlink on the receiver
(--keep-dirlinks; issue #715).
False wherever the portable per-component O_NOFOLLOW fallback is the active
resolver: a platform with no kernel "beneath" primitive, Linux < 5.6, a
seccomp-blocked openat2, or a --disable-openat2 build. There the delta
update through the symlinked directory fails verification. Probed
functionally (an initial transfer plus a delta update through a dir-symlink)
so it tracks the actual binary rather than a platform name, and cached."""
global _rb_cache
if _rb_cache is not None:
return _rb_cache
probe = SCRATCHDIR / '.rb_probe'
rmtree(probe)
(probe / 'home' / 'real').mkdir(parents=True)
os.symlink('real', probe / 'home' / 'link')
(probe / 'src' / 'link').mkdir(parents=True)
f = probe / 'src' / 'link' / 'f'
make_data_file(f, 40000)
def push():
subprocess.run(
rsync_argv('-KRl', '--no-whole-file', 'link/f',
f"{probe / 'home'}/"),
cwd=str(probe / 'src'),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
push()
with open(f, 'ab') as fh: # size change -> forces a delta update
fh.write(b'appended tail for delta\n')
push()
dst = probe / 'home' / 'real' / 'f'
_rb_cache = dst.is_file() and filecmp.cmp(str(f), str(dst), shallow=False)
rmtree(probe)
return _rb_cache
def write_daemon_conf(modules, globals=None, *,
name: str = 'test-rsyncd.conf') -> 'Path':
"""Write a custom rsyncd.conf for daemon-parameter tests.

View File

@@ -14,20 +14,27 @@
import filecmp
import os
import platform
import subprocess
import time
from rsyncfns import (
RSYNC, SCRATCHDIR, SRCDIR, TMPDIR,
make_data_file, rsync_argv, test_fail, test_skipped,
make_data_file, resolve_beneath_supported, rsync_argv, test_fail,
test_skipped,
)
if platform.system() in ('SunOS', 'OpenBSD', 'NetBSD') or platform.system().startswith('CYGWIN'):
# Following an in-tree directory symlink under the secure resolver needs a
# kernel "beneath" primitive (RESOLVE_BENEATH/O_RESOLVE_BENEATH). Probe the
# actual binary rather than guessing from the platform name: this also skips
# correctly on Linux < 5.6, a seccomp-blocked openat2, and a --disable-openat2
# build, where the portable O_NOFOLLOW fallback rejects the in-tree symlink
# (issue #715).
if not resolve_beneath_supported():
test_skipped(
f"secure_relative_open lacks RESOLVE_BENEATH equivalent on "
f"{platform.system()}; issue #715 still affects this platform"
"this rsync can't follow an in-tree directory symlink under its "
"secure resolver (no RESOLVE_BENEATH equivalent / --disable-openat2); "
"issue #715 still affects this configuration"
)
os.environ['RSYNC_RSH'] = str(SRCDIR / 'support' / 'lsh.sh')