fix(daemon): keep the DOTDIR marker on a module operand ending in /. - #7635
Merged
Conversation
Upstream sanitizes every daemon positional with sanitize_path(NULL, argv[i], "", 0, SP_KEEP_DOT_DIRS) (options.c:2402-2405, gated on the sanitize_paths that clientserver.c:1068 sets whenever the module has a dir). That sanitizer copies each component through its trailing slash (util1.c:1201) before examining the next one, so discarding a "." (util1.c:1163-1172) or a ".." (util1.c:1183-1191) leaves the separator that preceded it: "sym-to-dir/." sanitizes to "sym-to-dir/", not "sym-to-dir". That surviving slash is the DOTDIR marker. send_file_list reads it at flist.c:2589-2594 and sets name_type = DOTDIR_NAME, and the operand's stat is then link_stat(fbuf, &st, copy_dirlinks || name_type != NORMAL_NAME) (flist.c:2696) - so a symlink whose target is a directory is followed and its contents are sent (flist.c:286-299). collapse_module_relative split the tail on '/', dropped every "."/".." segment together with its separator, rejoined the survivors, and re-attached a trailing slash only when the RAW tail ended in one. "sym-to-dir/." does not, so the marker was destroyed before the sender saw the operand. Measured on a loopback daemon against rsync 3.5.0: upstream delivers the directory contents, oc delivered the symlink itself and exited 0. Delegating the collapse to the shared sanitize_path restores upstream's separator handling and keeps the two from drifting again. Four of sixteen daemon pull operand shapes diverged before the change; all sixteen now match the 3.5.0 binary on the same fixture.
oferchen
force-pushed
the
fix/daemon-dotdir-operand-marker
branch
from
September 3, 2026 05:49
780e414 to
cf09f0f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The daemon sender destroyed the DOTDIR marker on a module operand ending in
/., sorsync://host/mod/sym-to-dir/.shipped the symlink itself instead of the directory it points at.Upstream
sanitize_pathnever splits and rejoins. Each component is copied through its separator:so the slash is already in the output buffer when the next component is discarded (
util1.c:1163-1172skips a.).sym-to-dir/.therefore sanitizes tosym-to-dir/, andflist.c:2589-2594reads that trailing slash asDOTDIR_NAME, whichflist.c:2696turns into a followinglink_stat.The daemon runs every positional through this (
options.c:2402-2405, enabled for any module with a dir atclientserver.c:1068).Defect
collapse_module_relative(path_resolution.rs:78) split on/, dropped""/"."segments, and re-joined — then both call sites re-attached a trailing slash only when the raw tail ended in one.sym-to-dir/.does not, so the marker was gone before the sender's stat decision.Fix delegates the collapse to the shared
filters::sanitize_path, which already implements the copy-through-the-slash walk, and reads the trailing slash off the sanitized result...gets the same repair for free (d1/d2/..→d1/). Containment is unchanged: same depth-0..collapse, same/-only separator policy,\still an ordinary filename byte.Measured against the real 3.5.0 binary
Client is always upstream 3.5.0; only the daemon differs.
sym-to-dir/.deep/ deep/g.txt f.txtsym-to-dirdeep/ deep/g.txt f.txtrealdir/.deep/ deep/g.txt f.txtrealdir/ realdir/deep/… realdir/f.txtdeep/ deep/g.txt f.txtrealdir/./.realdir/ …realdir/deep/../.realdir/ …12/16 → 16/16. Cross-bisect localises it to the sender: with arg
sym-to-dir/.,daemon=upgives./f.txtfor either client,daemon=ocgives./sym-to-dirfor either.Local, SSH and
--files-fromlegs were already correct (26/26 before any change) — this is daemon-only.Pins
crates/transfer/tests/daemon_pull_dotdir_operand_marker.rs(4 end-to-end daemon cells) + 5 resolver unit cells.Mutation-proven — with the fix reverted and the tests kept: daemon
34 run: 31 passed, 3 failed, transfer4 run: 3 passed, 1 failed. With the fix: 34/34 and 4/4. The reddening celldotdir_marker_on_a_trailing_dot_follows_the_symlinked_directoryfails["sym-to-dir"]vs["f.txt"]— the defect's exact signature.Companions stay green in both arms, so the fixture cannot be inert:
resolve_sender_sources_does_not_invent_a_dotdir_marker,an_operand_without_the_marker_still_ships_the_symlink_itself(asserts the opposite outcome, so it also refuses a follow-everything fix),a_bare_dot_operand_transfers_the_whole_module,a_bare_module_root_operand_transfers_the_whole_module.Gates
check_rustfmt_all.pyclean (3389 files);clippy -p daemon --all-targetsand-p transfer --testsclean on touched files;nextest -p daemon1801 passed.-p transfer -p core -p cli9422/9424 — the 2 failures arerun_client_sparse_copy_creates_holesandtransfer_request_with_sparse_preserves_holes, verified failing on the pristine base, pre-existing APFS sparse-block noise.Found while measuring, filed separately
The daemon
--relativesender leaks absolute module paths:-aR rsync://h/m/<anything>diverges in 14 of 16 shapes because oc hands the sender an absolute module path where upstreamchange_dirs so names are module-relative. Architectural, larger than this fix, and provably untouched by it — the same 14 cells diverge identically before and after.