Skip to content

fix(daemon): keep the DOTDIR marker on a module operand ending in /. - #7635

Merged
oferchen merged 1 commit into
masterfrom
fix/daemon-dotdir-operand-marker
Sep 3, 2026
Merged

fix(daemon): keep the DOTDIR marker on a module operand ending in /.#7635
oferchen merged 1 commit into
masterfrom
fix/daemon-dotdir-operand-marker

Conversation

@oferchen

@oferchen oferchen commented Sep 2, 2026

Copy link
Copy Markdown
Owner

The daemon sender destroyed the DOTDIR marker on a module operand ending in /., so rsync://host/mod/sym-to-dir/. shipped the symlink itself instead of the directory it points at.

Upstream

sanitize_path never splits and rejoins. Each component is copied through its separator:

/* util1.c:1201 */
while (*p && (*sanp++ = *p++) != '/') {}

so the slash is already in the output buffer when the next component is discarded (util1.c:1163-1172 skips a .). sym-to-dir/. therefore sanitizes to sym-to-dir/, and flist.c:2589-2594 reads that trailing slash as DOTDIR_NAME, which flist.c:2696 turns into a following link_stat.

The daemon runs every positional through this (options.c:2402-2405, enabled for any module with a dir at clientserver.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.

operand upstream before after
sym-to-dir/. deep/ deep/g.txt f.txt sym-to-dir deep/ deep/g.txt f.txt
realdir/. deep/ deep/g.txt f.txt realdir/ realdir/deep/… realdir/f.txt deep/ deep/g.txt f.txt
realdir/./. same realdir/ … same
realdir/deep/../. same realdir/ … same
12 other shapes already matched unchanged

12/16 → 16/16. Cross-bisect localises it to the sender: with arg sym-to-dir/., daemon=up gives ./f.txt for either client, daemon=oc gives ./sym-to-dir for either.

Local, SSH and --files-from legs 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, transfer 4 run: 3 passed, 1 failed. With the fix: 34/34 and 4/4. The reddening cell dotdir_marker_on_a_trailing_dot_follows_the_symlinked_directory fails ["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.py clean (3389 files); clippy -p daemon --all-targets and -p transfer --tests clean on touched files; nextest -p daemon 1801 passed. -p transfer -p core -p cli 9422/9424 — the 2 failures are run_client_sparse_copy_creates_holes and transfer_request_with_sparse_preserves_holes, verified failing on the pristine base, pre-existing APFS sparse-block noise.

Found while measuring, filed separately

The daemon --relative sender 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 upstream change_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.

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
oferchen force-pushed the fix/daemon-dotdir-operand-marker branch from 780e414 to cf09f0f Compare September 3, 2026 05:49
@oferchen
oferchen merged commit 0310ec8 into master Sep 3, 2026
73 checks passed
@oferchen
oferchen deleted the fix/daemon-dotdir-operand-marker branch September 3, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant