ln -f A B unlinks B first and only then calls link. When the link fails, B is already gone and the diagnostic talks only about the source. GNU tries the link first and unlinks only on EEXIST, so the destination survives every failed link.
$ echo keep > B
$ ln -f nonexistent B ; echo "exit=$?" ; cat B
| source |
uutils 0.11.0 / main |
GNU 9.11 |
| missing |
exit 1, B deleted |
exit 1, B keeps its contents |
| a directory |
exit 1, B deleted |
exit 1, B keeps its contents |
dangling symlink with -L |
exit 1, B deleted |
exit 1, B keeps its contents |
All three verified on main against GNU 9.11.
Cause
In the OverwriteMode::Force arm of link() (src/uu/ln/src/ln.rs:436) the destination is removed unconditionally, and everything that can make the link fail comes after it: the -L resolution at ln.rs:454 and fs::hard_link at ln.rs:463. The Err(_) if p.is_dir() arm that prints "hard link not allowed for directory" is diagnosing a failure that has already cost the destination.
GNU's ln.c documents the deliberate opposite order - POSIX 2008 lets an application fail early if it can prove continuing cannot succeed, so it links to a temporary name in the destination's directory and renames it over B. That also removes the window during a successful ln -f where B briefly does not exist at all.
Suggested fix
Attempt the link first and take the force path only on AlreadyExists; resolve -L and reject a directory source before removing anything. Better, link under a temporary name and rename over dst, as GNU does.
Affected: 0.11.0 and main. Reported privately by Hongkai Chen (SEFCOM Lab, Arizona State University); handled as a normal bug rather than an advisory.
ln -f A BunlinksBfirst and only then callslink. When the link fails,Bis already gone and the diagnostic talks only about the source. GNU tries the link first and unlinks only onEEXIST, so the destination survives every failed link.BdeletedBkeeps its contentsBdeletedBkeeps its contents-LBdeletedBkeeps its contentsAll three verified on
mainagainst GNU 9.11.Cause
In the
OverwriteMode::Forcearm oflink()(src/uu/ln/src/ln.rs:436) the destination is removed unconditionally, and everything that can make the link fail comes after it: the-Lresolution atln.rs:454andfs::hard_linkatln.rs:463. TheErr(_) if p.is_dir()arm that prints "hard link not allowed for directory" is diagnosing a failure that has already cost the destination.GNU's
ln.cdocuments the deliberate opposite order - POSIX 2008 lets an application fail early if it can prove continuing cannot succeed, so it links to a temporary name in the destination's directory and renames it overB. That also removes the window during a successfulln -fwhereBbriefly does not exist at all.Suggested fix
Attempt the link first and take the force path only on
AlreadyExists; resolve-Land reject a directory source before removing anything. Better, link under a temporary name and rename overdst, as GNU does.Affected: 0.11.0 and
main. Reported privately by Hongkai Chen (SEFCOM Lab, Arizona State University); handled as a normal bug rather than an advisory.