Files
FastSync/tests/integration
TapTap 423a62e691 fix(receiver): confine --temp-dir scratch dir and gate setuid bits
Three receiver security fixes from the audit:

1. --temp-dir symlink escape (High): file_open_temp_dir() opened the
   client-controlled scratch dir with a bare open(), so a symlink planted
   under the receive root let a peer redirect receiver scratch files
   outside the authorized root.  The opened dir is now judged by the REAL
   path of its fd (via /proc/self/fd), and any target outside the
   authorized receive root is refused with a logged error (EACCES).  An
   in-root symlink (the EXDEV cross-filesystem fallback case) still works,
   and the no-root local batch path is unchanged.

2. setuid/setgid/sticky under SUPER_MODE_OFF (High): the special bits were
   applied under --perms (and via --chmod) even when the connection forbade
   super-user activities.  FileAttrPolicy gains super_permitted, set by
   file_attr_policy_from_config() from privilege_super_mode_permitted();
   metadata_mode_for_policy(), the symlink path, the special-node creation
   path, and the deferred directory-mode apply now strip the special bits
   when it is false.  Exact rsync semantics are preserved when permitted.

3. daemon umask (Low): daemonize() forced umask(0), so implied parent
   directories created without -p were world-writable 0777.  Set the
   conventional daemon umask 022 instead (rsync never forces 0); -p/-a mode
   preservation is unaffected because it restores modes via fchmod.

Tests: new unit tests for file_open_temp_dir confinement and the
masked/unmasked special-bit policy (incl. the --chmod path), a daemon
world-writable-dir regression test, an integration escape test, and a
root-only integration test asserting special bits are masked without
--allow-super.  The old cross-filesystem test encoded the vulnerable
behavior (symlink target outside the root) and is replaced by the escape
test; the EXDEV fallback code is retained for in-root links.
2026-09-21 18:45:29 +02:00
..

Integration tests

The integration suite drives the built build/server and build/client against local corpora. Unit tests live in tests/ (the custom C framework); the Python suite here covers the full transfer pipeline, transports, features, and rsync parity.

Running

# Full suite (excludes privilege-dependent tests on CI runners)
python3 -m pytest tests/integration/ -n 4 --dist=load -m "not setpriv"

# Fast PR subset only
python3 -m pytest tests/integration/ -n 4 --dist=load -m ci

The tests expect build/server and build/client (configure/build with CMake first); common.py derives BUILD_DIR from the repository root.

Differential rsync-parity gate

test_differential_parity.py runs the same transfer with real rsync 3.4.1 and with FastSync over separate destinations, then compares:

  • the destination trees — relative paths, file content hashes, symlink targets, modes (where the case is about perms), and hard-link grouping;
  • the normalized stdout for output-oriented flags (-i, --out-format=..., --stats), after stripping volatile fields (timings, rates, wire byte counts) and directory-only itemize lines that FastSync's recursive scanner documents as absent.

FastSync mirrors the absolute source path under its receive root (see get_dest_received_dir); the harness normalizes that layout (and the -R/--files-from layouts) before comparing.

# Fast subset that guards the ✅ surface on pull requests
python3 -m pytest tests/integration/test_differential_parity.py -n 4 --dist=load -m parity_ci

# Full differential case table (`_CASES`): every row is marked `parity`, and a
# case with an allowlisted residual in `parity_caveats.py` is included too.
python3 -m pytest tests/integration/test_differential_parity.py -n 4 --dist=load -m parity

-m parity selects only the _CASES table in this module. Differential coverage for options outside that table (--temp-dir, --delay-updates, --dry-run, --fuzzy, the basis-dir options, -M over daemon/TCP, and receiver filter-protect) lives in dedicated modules (test_option_parity.py, test_parity_blockers.py, test_parity_quickwins.py, ...) and is not part of this gate. The suite skips cleanly when rsync is not installed.

Allowlist (parity_caveats.py)

parity_caveats.py is the single data-driven allowlist of known differences. Each entry maps a case id to the aspects that may differ (tree, stdout, extra, rc) and cites the governing row in RSYNC_COMPAT.md:

CAVEATS = {
    # no known residuals at present -- the burn-down reached zero
    # "some_case_id": {"tree": "documented residual ... ref: RSYNC_COMPAT.md ..."},
}

A differential mismatch in an aspect that is not listed fails the gate with a readable tree/stdout diff.

If a case is allowlisted but now matches rsync, the gate emits a loud warning naming the stale entry — that is the parity burn-down signal. Run with FASTSYNC_PARITY_STRICT=1 to make stale entries fail instead (the full CI parity job sets this). To add a residual:

  1. Reproduce it with -m parity and read the failure's tree/stdout diff.
  2. Confirm it is a documented ⚠️/❌ residual (or get the ✅ row reclassified) and cite the row.
  3. Add the case id and aspect(s) to CAVEATS, keeping the reason concise.

Do not allowlist an undocumented divergence from a ✅ row — fix it or get the row reclassified first.