]> git.ipfire.org Git - thirdparty/rsync.git/commitdiff
testsuite: probe RESOLVE_BENEATH support functionally for the #715 test
authorAndrew Tridgell <andrew@tridgell.net>
Sat, 23 May 2026 22:12:39 +0000 (08:12 +1000)
committerAndrew Tridgell <andrew@tridgell.net>
Sun, 24 May 2026 02:31:52 +0000 (12:31 +1000)
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>
testsuite/rsyncfns.py
testsuite/symlink-dirlink-basis_test.py

index b9bcbf17bc1776ee058a2019e4e5c6f959c301c5..3a3b37b198df667eae619df94eaafe57165e9068 100644 (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.
index b952b4de2a2ccecf0b1b8c09027f65ae492f59f5..fd880144ac332352549f08cd3bd73b9f8fc1f5f0 100644 (file)
 
 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')