Skip to content

Dangling symlink at the destination escapes conflict detection #5

Description

@darkone-linux

Summary

A dangling symlink sitting at the destination path is invisible to the
conflict detection, because Path::exists() follows the link and answers
false when the target is missing. Two consequences, one cosmetic and one that
can destroy the symlink.

1. Confusing error message (Linux)

$ ln -s nowhere.txt cafe.txt     # dangling
$ printf 'x' > 'Café.txt'
$ rename-simple 'Café.txt'
[E] .: Café.txt -> File exists (os error 17)

Nothing is lost — renameat2(RENAME_NOREPLACE) refuses the rename, exactly as
designed — but the raw errno leaks out instead of the usual
File name already exists. The op slipped past filter_conflicts
(src/main.rs:328, exists()false) and only got caught at the syscall,
where the error is reported verbatim (src/main.rs:414).

2. The dangling symlink is silently replaced (non-Linux)

On macOS/BSD/Windows, rename_no_clobber falls back to a try_exists()
pre-check followed by std::fs::rename (src/main.rs:392-401).
try_exists() follows the link too, so it also answers false for a dangling
symlink, and fs::rename on Unix then overwrites it. The symlink is
destroyed without a word — the destination looked free but was not.

This contradicts the guarantee stated in the README and the man page: an
existing destination is never overwritten.

Suggested fix

Both come from the same root cause: existence is tested through the link
instead of on the entry itself. symlink_metadata() answers about the entry —
it is what compare_entries already switched to in 0.7.1 (src/lib.rs:739).

  • In filter_conflicts, detect the destination with
    to.symlink_metadata().is_ok() rather than to.exists(), so a dangling
    symlink is reported as a normal conflict with the usual message and never
    reaches the rename.
  • In rename_no_clobber, use the same check in the non-Linux fallback, closing
    the overwrite. The Linux path already refuses at the syscall level.

compare_entries needs no change: a symlink on either side already answers
NotComparable, so -D will not touch it.

Worth adding a tests/unix_tests.rs case asserting that a dangling symlink at
the destination survives, and that the error message is the regular one.

Found during the code review that led to 0.7.1. Pre-existing on all platforms.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions