mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-09-14 14:18:23 -04:00
operator-path-temp-dir and operator-path-partial-dir decided whether a symlink had been followed by sampling the target directory's st_mtime_ns, sleeping 10ms, and looking for a change. The temp file is renamed away, so an mtime bump was the only trace left. On a filesystem whose timestamps have 1-second granularity -- HFS+, and it is not alone -- a change within the same second is invisible. The delta is zero, the test concludes the symlink was not followed, and reports the operator's OWN euid-owned symlink as refused when it was followed correctly. Both fail that way on HFS+ while passing on APFS, and operator-path-partial-dir is one of the failures Roland Kletzing reported on macOS. Pin the directory's mtime to a fixed past epoch instead, read back what the filesystem actually stored, and ask afterwards whether it still holds -- reading back because a filesystem may clamp or round the value, and comparing against the requested epoch would then read an unfollowed symlink as followed. temp-dir-symlink-injection already works this way. This is not proof against every clock: a directory whose mtime lands exactly on the stored sentinel would still read as unfollowed. That needs the host clock set to 2001 or a deliberate restore, where the old 10ms delta failed on any coarse-granularity filesystem. Verified in both directions by running as root, where the matrix also exercises the cross-uid cells: a followed symlink moves the mtime off the sentinel, a refused one leaves it.
44 lines
2.0 KiB
Python
44 lines
2.0 KiB
Python
#!/usr/bin/env python3
|
|
# --temp-dir symlink policy matrix. A followed --temp-dir symlink makes the
|
|
# receiver create its scratch temp file in an out-of-tree directory (the data is
|
|
# written there, then renamed to dest). The temp is renamed away, so we detect
|
|
# the escape by the target directory's mtime advancing (a file was created in
|
|
# it). The ownership walk must follow only uid0/euid-owned symlinks; a relative
|
|
# --temp-dir anchors at the cwd. --insecure-links is the local opt-out.
|
|
|
|
import os
|
|
import subprocess
|
|
|
|
from rsyncfns import rsync_argv, run_symlink_matrix, plant_operator_symlink
|
|
|
|
PINNED = 1000000000 # 2001-09-09; any later mtime means it was touched
|
|
|
|
|
|
def case(ctx):
|
|
src = ctx.base / 'src'
|
|
dest = ctx.base / 'dest'
|
|
src.mkdir()
|
|
dest.mkdir()
|
|
(src / 'f0').write_text("PAYLOAD-DATA\n")
|
|
# A relative --temp-dir is resolved by the receiver, whose cwd is the
|
|
# destination directory -- so a relative plant anchors at dest, not the cwd.
|
|
opt, escape = plant_operator_symlink(ctx, dest)
|
|
escape.mkdir(parents=True, exist_ok=True) # --temp-dir must exist
|
|
# Pin the mtime far in the past rather than sampling it and looking for a
|
|
# sub-second delta: HFS+ (and any filesystem with 1-second timestamps)
|
|
# cannot show a change that happens within the same second, so a delta test
|
|
# reads "not followed" for a symlink that WAS followed. Against a pinned
|
|
# epoch, any update at all is visible at any resolution.
|
|
os.utime(escape, (PINNED, PINNED))
|
|
pinned = escape.stat().st_mtime # what the fs actually stored
|
|
extra = ['--insecure-links'] if ctx.insecure else []
|
|
subprocess.run(
|
|
rsync_argv('-a', f'--temp-dir={opt}', *extra, 'src/', 'dest/'),
|
|
cwd=str(ctx.base), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
# Followed == the receiver created its temp in the out-of-tree dir.
|
|
return escape.stat().st_mtime != pinned
|
|
|
|
|
|
run_symlink_matrix('--temp-dir', case)
|
|
print("--temp-dir symlink policy matrix: enforced")
|