Probe shows the candidate choice is observable only as --stats bandwidth counters (tree/exit always identical). FastSync already uses rsync's fuzzy_distance/find_filename_suffix name heuristic; the residual is the narrower delta eligibility window (>=16 KiB, <=10x) vs rsync's wider one. Row -> caveat; tally 116/14/27.
16 KiB
FastSync — Session Handoff (2026-09-17)
Current status
- Release
v2.21.0tagged (919a729, "Release v2.21.0"); full CI green (run 552: lint, build-and-test, ASan, UBSan, fuzz-build, coverage, valgrind).devhas the release commit plus later doc-only merges (a README refresh and this handoff). - Release PR #284 (
dev->main) open, CI green (run 553).mainis protected: it needs review/approval to merge. #284 PROTOCOL_VERSION="2.27.0"(src/shared/config.h); CMakeproject(FastFileTransfer VERSION 2.27.0).- Working tree clean; no wave worktrees remain.
What landed this session
-
Wave 8 (refactors): Config X-macro wire table; single-owner
authorized_root; daemon per-module/per-host caps + cross-process auth lockout (daemon_limits.[ch]);Datacharge returns to its owningProtocolSession. -
Wave 9 (protocol 2.21.0): optional
STATUS_ERROR_DETAILrejection reasons; server-contacting--dry-run(STATUS_DRY_RUN_TRANSFER, receiver mutates nothing). -
Security wave: ran 5 parallel audits (wire parsing; daemon/transport/TLS/auth; receiver confinement; client/CLI/SSH; crypto/memory/limits). Fixed all HIGH and the confirmed MEDIUMs:
- SSH
-o ProxyCommand=…argument injection (RCE) — reject leading-, insert--. - Truncated zstd frame infinite CPU loop (remote DoS).
- FIFO receiver opens lacked
O_NONBLOCK(indefinite hang). --inplacecould write a FIFO/device (bypass of--write-devicesgate).--forcenot gated by server--allow-delete.- Privileged standalone server defaulted super activities on; added
--allow-super(never honored with--stdio). --dry-runcontent/hash oracle onread only/basis files removed.- Empty
hosts allow/deny/auth usersnow rejected. - TLS: AEAD-only 1.2 + server preference, TOCTOU-safe key load, IP-SAN verify,
CN-truncation guard. Glob backtracking bounded; line reads bounded; ACL xattrs
gated on
--acls; decompression/chunk memory charged; pre-authbasis_countNULL-deref fixed.
- SSH
-
Tooling: benchmark accuracy (data mix, verification, percentiles,
tc,build-bench/,--warmmode);shell.nixfull toolchain and no build-on-entry; docs state push-only / remote-source unsupported. -
Preserve-attribute split (protocol 2.22.0) landed on
feat/preserve-attr-split: per-attribute-p/-t/-o/-g+--no-*negations,-a=-rlptgoD, and the 2.21.0 → 2.22.0 wire bump. -
Rsync-parity wave (protocol 2.23.0) on
feat/rsync-parity: rsync short options/clustering/attached values (-r/-b/-L/-B,-av,-aAX,-B1000,-essh,-MOPT),-cchecksum quick-check,--checksum-choice/--compress-choicevalidation and seed randomization, rsync timeout/max-alloc defaults, temp-dir confinement +EXDEVfallback, ownership/mapping parity (numeric-ids modifier, map ranges/*/empty-FROM,--chown+map conflicts, fake-super resolved-owner record), verbatim symlink storage with rsync--safe-links/--munge-links, socket recreation under--specials,--chmod3.4.1 semantics, and delete scoping +--max-deletepartial/exit-25. Wire: appended delete-manifest synchronized-directory section andSTATUS_DELETE_LIMIT. -
Parity-completion wave (protocol 2.24.0 → 2.26.0) on
feat/parity-completion: per-directory delete plans (STATUS_DELETE_PLAN) for--delete-during/--delete-delay; receiverSTATUS_STATScounters feeding--stats/--progressand--out-format %b/%c/%C, plus-n --deletelines;lz4/zlib/zlibxcompression andmd4/sha1/nonechecksums withautonegotiation (defaultxxh128/zstd); general-R/--no-implied-dirs/-d; the full filter grammar (merge/dir-merge/hide/show/protect/risk/clear+ modifiers) and corrected-F/-FF; receiver-side--chown/map TO-name resolution; absolute basis dirs +--link-destrelink; receiver-side--ignore-existingshort-circuit;--preallocateover--sparseviafallocate(2);--iconv=./-/--no-iconv; lone-hhelp; aliases--ignore-non-existing/--protect-args/--msgs2stderr; and the full--info/--debugvocabulary.RSYNC_COMPAT.mdreclassifies the matrix to 106 ✅ / 27 ⚠️ / 23 ❌; the later rsync-parity-stats pass (fix/parity-stats) moves it to 107 ✅ / 25 ⚠️ / 24 ❌ (see item 8). -
rsync-parity-stats pass on
fix/parity-stats(no wire change,PROTOCOL_VERSIONstays2.26.0):--delete-delaynow reports only entries it actually removes, while the--max-deletebudget is charged at plan/snapshot time (planned, viadefer_add) to bound the deferred list (a refilled deferred directory that survivesENOTEMPTYis not reported but still consumes budget);--statsgained the(reg/dir/link/special)Number of filesbreakdown and now counts only regular files actually stored forNumber of regular files transferred/transferred size/literal data (up-to-date re-runs report 0);Total file sizeincludes symlink target lengths;--progressprints the leading./root line and counts it into-chkso a single-file transfer matches rsync; and%Cuses the selected transfer checksum withchecksum_digest_filesupporting md4/sha1/none, byte-identical to rsync for every algorithm.--out-formatreclassified ❌ (%b/delta-%care protocol-specific). Differential + regression tests added; full suite + ASan + clang-format + cppcheck clean. -
Option-parity wave (protocol 2.26.0 → 2.27.0, on
fix/parity-options):--bwlimitnow ports rsync 3.4.1's units/quantization and paces like its leaky bucket;--ignore-errorsreproduces rsync's default (an I/O error skips deletion unless the flag is set; the readable tree still transfers and the run exits 23) across every delete timing; the--infocategories with a FastSync event (name/flist/del/remove/nonreg/progress) emit rsync's line format, with real-rundeleting/*deletinglines carried over the new trailing config boolreport_deletes(golden wire updated bytests/test_config.c). Two residuals were reclassified divergent:-Mover daemon/TCP (no argv channel in FastSync's binary config handshake; rsync-daemon differential pins the rsync behavior) and receiver-sideprotect/riskre-derivation for destination-only entries (would need a receiver filter engine; differential pins the divergence — reversed by track 4a below, which adds that engine). The options pass stands at 110 ✅ / 21 ⚠️ / 26 ❌. Newtests/integration/test_option_parity.pyholds the rsync differentials (bwlimit parse+rate, info lines, real-setpriv--ignore-errors, rsync-daemon-M, filter-protect pin). -
rsync-parity-fs pass on
fix/parity-fs(no wire change of its own; integrated on top of the 2.27.0 options wave): recursive transfers now recreate empty source directories (and-m/--prune-empty-dirsstill suppresses them), a directory entry replaces a blocking destination regular file, and-R --no-implied-dirs --files-fromplaces a listed file under a missing implied parent with default attributes instead of refusing (real rsync 3.4.1 parity, differential-tested).--iconvnow reproduces rsync's push direction (destination charset = the spec's REMOTE half; a server--iconvoverrides), and-T/--temp-dirrelative semantics are confirmed identical while the absolute-path confinement is a deliberate divergence. The basis-dir options,--delay-updatesand--dry-runwere reclassified to ❌ after a differential test reproduced each exact residual (basis content verification, fixed staging-name collision, and dry-run would-delete over-report).--fuzzywas also reclassified to ❌ (deterministic heuristic with a 10× size window, not rsync's matcher), but its residual is the candidate-selection heuristic itself: the final tree is byte-exact by design, so it is pinned by theTestFuzzythreshold suite rather than a byte-level rsync differential. (Track 5b later found the name heuristic is rsync's own and moved the row ❌ → ⚠️, leaving only the narrower delta size window; see entry 15.) The parity-review pass then moved--delete-delayto ⚠️ (the plan-time--max-deletecharge and non-recursive deferred removal differ from rsync when a snapshotted entry fails removal). Differential-gate allowlist entriesmin_size/empty_dirs_recursive/dirs_plainwere removed. The integrated stats+options+fs branch stands at 111 ✅ / 13 ⚠️ / 33 ❌ = 157; full suite + ASan + clang-format + cppcheck clean. -
No-wire parity track 1 on
feat/parity-2.28(no protocol change):-n --deletenow sends the same filter-excluded + size-pruned protected prefixes and synchronized-directory scope as a real run (dry-run would-delete matches rsync for source-derived protections; the destination-only exclude residual was later closed by track 4a, readdir ordering remains);--delete-delaynow charges--max-deleteon actual removals and re-scans a queued directory at commit to remove content created after the plan, with an independent deferred-list cap (only partial-delete ordering remains); and--info=name2emitsNAME is uptodateplus the leading./root name line for--info=name(only the root-line trigger condition and receiver-sideskipwording remain). Matrix now 111 ✅ / 14 ⚠️ / 32 ❌ = 157; differential + unit tests added intest_features.py,test_option_parity.py,test_delete_plan.c,test_delete_delay_budget_parity.py,test_delete_timing_parity.py. -
No-wire parity track 2b on
feat/parity-2.28(no protocol change):--progress/-P/--info=progress(when not--quiet) now run an opt-in paths-only metadata pre-count (no file reads/hashing) that supplies rsync's full file-list total for theto-chkdenominator and the directory names, and emits per-directory/symlink/special name lines, in both the sequential and--threadspaths.--delete-during/--delete-delayreuse their keep-set pre-scan instead of a second walk; non-progress runs are unaffected. Differential tests (progress/progress_threadsover a newmultidircorpus) match rsync's name set andto-chkdenominator on a fresh transfer, and the single-file byte-identical test still passes; emission order (rsync's sorted depth-first vs FastSync's readdir/BFS stream) plus re-run over-naming (unconditional./, ancestor dirs named with a transferred child, and no quick-check for symlinks/empty dirs) remain the caveats, so the row stays ⚠️ and the matrix is unchanged at 111 ✅ / 14 ⚠️ / 32 ❌ = 157. -
Wire parity track 4a on
feat/parity-2.28(PROTOCOL_VERSIONstays2.28.0): the receiver now has a delete-time filter engine. The sender compiles its root-level selection rules exactly as the scanner does (filter_base_build) and streams them as one bounded, self-describing config-frame block (action, sides, anchored, dir-only, negate, owner, pattern; bounded rule count and pattern bytes, unknown action/sides is a protocol error). The receiver reconstructsprotect_rulesand applies them first-match-wins to each extraneous destination path in every delete timing (the whole-tree commit walker, the--delete-during/--delete-delayper-directory plans, and the-nwould-delete enumeration), so aP *.logrule protects a destination-onlyextra.loglike rsync (withriskcancelling); the sender-derived protected-prefix behavior is preserved when no rules are sent and--delete-excludedsemantics are unchanged. Per-directory merge (:/.) receiver re-derivation remains the residual.TestFilterProtect(real + dry-run) plus differential casesfilter_protect,filter_protect_during,filter_protect_delayadded and the--filter=RULErow moves ❌ → ✅: matrix now 115 ✅ / 11 ⚠️ / 31 ❌ = 157; unit tests, the three named integration files, clang-format and cppcheck clean. -
Wire parity track 5a on
feat/parity-2.28(PROTOCOL_VERSIONstays2.28.0by project decision): the three basis-dir options now default to rsync's metadata quick-check (equal size + equal mtime, or size alone under--size-only;-Idisables matching) instead of FastSync's historical xxHash64 content equality, so a same-size/different-content basis is trusted exactly as rsync trusts it. A new FastSync-only, long-only--verify-basisflag restores the strict whole-file content equality; its bool is appended to the basis block of the config frame (golden wire frame 882 → 886 bytes).--verify-basisstreams the confined basis descriptor to hash it, and a basis hit is no longer capped at the 256 MiB whole-file payload bound:--copy-deststreams the basis through a bounded buffer and--link-dest's copy fallback streams from the basis, so an over-limit hit materializes (a basis MISS still falls back to the normal transfer and keeps its own bound). A--copy-desthit re-applies the SOURCE attributes (the sender transmits the source metadata with the basis check frame), matching rsync's "copy then fix attributes"; a--link-destsuccess keeps the shared inode's attributes (writing through it would mutate the basis). Differential casescopy_destandverify_basisadded;test_basis_dir_size_only_content_residualconverted to a passing parity assertion;TestBasisDestDirsupdated for the new default +--verify-basis; unit tests cover the quick-check/verify decision and the same-size/different-content handshake. The--compare-dest/--copy-dest/--link-destrows move ❌ → ⚠️ (relative-DIR resolution base and over-limit MISS refusal): matrix now 116 ✅ / 13 ⚠️ / 28 ❌ = 157. -
No-wire parity track 5b on
feat/parity-2.28(PROTOCOL_VERSIONstays2.28.0by project decision):-y/--fuzzyreclassified ❌ → ⚠️. A probe against real rsync 3.4.1 (pinned-B8192, repeated-content 64 KiB corpus) showed the name heuristic is already rsync's (util1.c fuzzy_distance/find_filename_suffix+ the exact size+mtime pass) and the output is always byte-exact; the only residual is candidate ELIGIBILITY, because FastSync'sdelta_should_attemptgate caps the size ratio at 10× and requires both files ≥ 16 KiB while rsync will reuse a basis from 0.25× to 10000× and below 16 KiB. The choice is observable only as--statsbandwidth counters. Added differential casefuzzy_basis(same-suffix sibling, one name edit, identical content, block size pinned) asserting tree and normalized--statsparity where the choices coincide, plusTestFuzzypinning the window boundary on both sides (>10× and <16 KiB siblings declined by FastSync while rsync uses them, both trees byte-identical). Matrix now 116 ✅ / 14 ⚠️ / 27 ❌ = 157.
Next steps
- Merge PR #284 (
dev->main) once reviewed (protected branch). - Deferred security items (documented, not implemented):
- Pre-auth config/daemon-auth handshake has no aggregate wall-clock deadline (per-message timeout only) — slowloris holds connection slots.
- Per-source registry fails open when the shared table is full (per-module/global caps and host ACLs still apply); consider fail-closed or larger/evicting table.
- SCRAM-like daemon auth has no TLS channel binding (and is not RFC 5802).
cleanup()signal handler calls non-async-signal-safe teardown; daemonumask(0).- Wire protocol assumes homogeneous word size/endianness (lengths are native
size_t) — document or move to fixed-width framing.
- Out of scope / intentional: pull (remote source) mode is not planned —
FastSync is push-only; see
RSYNC_COMPAT.md#direction.
Key facts / commands
- CI image:
gitea.tap-tap.win/taptap/fastsync-ci:v11(aliasfastsync-ci:local). - Build/test:
cmake -B build -S . -DSTRICT_WARNINGS=ON && cmake --build build -j$(nproc) && ./build/teststhenpython3 -m pytest tests/integration/ -n 4 --dist=load -m "not setpriv". - Dev shell:
nix-shell(provides clang-format, cppcheck, pytest-xdist, openssh, rsync, iproute2, valgrind, lcov; does not build on entry). - Gitea API token: supplied out-of-band via the
TOKENenvironment variable; it is intentionally not recorded in this file. - CI polling:
GET /api/v1/repos/TapTap/FastSync/actions/runs?limit=N, matchhead_sha, then/actions/runs/<id>/jobs.