Skip to content

fix(install): follow symlink components in destination path with -D - #11505

Merged
Ecordonnier merged 1 commit into
uutils:mainfrom
abendrothj:fix/install-d-follow-symlinks
May 25, 2026
Merged

Ecordonnier merged 1 commit into
uutils:mainfrom
abendrothj:fix/install-d-follow-symlinks

Conversation

@abendrothj

@abendrothj abendrothj commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #11469

install -D was replacing pre-existing symlinks in the destination path with real directories instead of following them. This broke any workflow where part of the install prefix is a symlink; including BOSH deployments, Homebrew, Nix, stow, and any make install targeting a symlinked prefix.

Reproduction (from the issue):

mkdir -p /tmp/target
ln -s /tmp/target /tmp/link
echo hello > /tmp/file.txt
install -D -m 644 /tmp/file.txt /tmp/link/subdir/file.txt
# GNU coreutils 8.32: /tmp/link stays a symlink, file lands in /tmp/target/subdir/file.txt
# uutils 0.7.0:       /tmp/link is replaced with a real directory — wrong

Root cause

PR #10140 introduced create_dir_all_safe() in safe_traversal.rs to prevent TOCTOU symlink race conditions. The fix was correct in intent but too aggressive: open_or_create_subdir() unconditionally unlinked and recreated any symlink it encountered, including pre-existing legitimate ones.

Changes

src/uucore/src/lib/features/safe_traversal.rs

  • open_or_create_subdir: when stat_at returns S_IFLNK, call open_subdir(Follow) instead of unlink_at + mkdir_at. The O_DIRECTORY flag already in open_subdir means dangling or non-directory symlinks still return an error cleanly.
  • find_existing_ancestor: switch from fs::symlink_metadata to fs::metadata so that a symlink-to-directory is recognised as an existing ancestor rather than a component to recreate (this was already the stated intent in the function's doc comment).

src/uu/install/src/install.rs

  • Align the dir_exists check and the DirFd::open call to also follow symlinks, consistent with the above.

tests/by-util/test_install.rs

TOCTOU / security note

The true TOCTOU race (a symlink injected during the operation into a not-yet-existing path component) is still blocked: mkdirat fails with EEXIST if an attacker creates a symlink between stat_at returning ENOENT and our mkdir_at. Newly-created directories are still opened with O_NOFOLLOW.

What changes is that pre-existing symlinks are now followed — which is exactly what GNU coreutils 8.32 does. The previous behavior was stricter than GNU in this regard.

@abendrothj
abendrothj force-pushed the fix/install-d-follow-symlinks branch from 9c7f91a to 4fc3ac8 Compare March 26, 2026 11:13
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/timeout/timeout-group. tests/timeout/timeout-group is passing on 'main'. Maybe you have to rebase?
Note: The gnu test tests/seq/seq-epipe is now being skipped but was previously passing.
Congrats! The gnu test tests/cp/link-heap is now passing!
Note: The gnu test tests/misc/write-errors was skipped on 'main' but is now failing.

@codspeed

codspeed Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 319 untouched benchmarks
⏩ 46 skipped benchmarks1


Comparing abendrothj:fix/install-d-follow-symlinks (dbe8864) with main (19c7f64)

Open in CodSpeed

Footnotes

  1. 46 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@abendrothj
abendrothj force-pushed the fix/install-d-follow-symlinks branch 2 times, most recently from ebc0ba6 to cd070c4 Compare March 26, 2026 20:34
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/misc/io-errors. tests/misc/io-errors is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/cut/bounded-memory (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/pr/bounded-memory (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.

@abendrothj
abendrothj force-pushed the fix/install-d-follow-symlinks branch from cd070c4 to ed62194 Compare March 28, 2026 12:03
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/date/resolution (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/tail/tail-n0f is now being skipped but was previously passing.

@zhw2101024

Copy link
Copy Markdown
Contributor

The test fails in the mkfifo test, anyway hope this PR can be merged, and the related issue not left to the next released uutils.

@abendrothj

Copy link
Copy Markdown
Contributor Author

The test fails in the mkfifo test, anyway hope this PR can be merged, and the related issue not left to the next released uutils.

agreed. seems like flakiness, since mkfifo is unrelated.

@github-actions

github-actions Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/cut/bounded-memory (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/tail/tail-n0f (fails in this run but passes in the 'main' branch)
Note: The gnu test tests/tail/pipe-f is now being skipped but was previously passing.

install -D was replacing pre-existing symlinks in the destination path
with real directories instead of following them, breaking any workflow
where part of the install prefix is a symlink (BOSH, Homebrew, Nix,
stow, `make install` with a symlinked prefix).
@Ecordonnier
Ecordonnier force-pushed the fix/install-d-follow-symlinks branch from 93383c7 to dbe8864 Compare May 24, 2026 20:03
@Ecordonnier
Ecordonnier self-requested a review May 24, 2026 20:05
Comment thread tests/by-util/test_install.rs Resolved
@Ecordonnier
Ecordonnier merged commit 21fda8c into uutils:main May 25, 2026
172 of 173 checks passed
@Ecordonnier

Copy link
Copy Markdown
Collaborator

Thanks. I've merged the branch and will add the cosmetic changes in a follow-up PR. This should be merged ASAP since it is a critical fix.

arnout pushed a commit to buildroot/buildroot that referenced this pull request May 30, 2026
uutils install 0.8.0 as used in Ubuntu 26.04 has a bug in the install
applet, breaking a number of packages:

uutils/coreutils#11505
https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2151454

The fix has been merged upstream but not yet released or packaged in Ubuntu,
so detect and reject the buggy version and explain how to change to the
coreutils version. Once fixed the version output will hopefully change.

For simplicity, only check for the exact 0.8.0 version string. Hopefully
when it is fixed in Ubuntu, they also update the version string. Note
that earlier versions of uutils have the same issue of course, but those
versions were never the default "install" on Ubuntu (or anywhere else).

Signed-off-by: Peter Korsgaard <[email protected]>
Signed-off-by: Arnout Vandecappelle <[email protected]>
arnout pushed a commit to buildroot/buildroot that referenced this pull request Jun 5, 2026
uutils install 0.8.0 as used in Ubuntu 26.04 has a bug in the install
applet, breaking a number of packages:

uutils/coreutils#11505
https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2151454

The fix has been merged upstream but not yet released or packaged in Ubuntu,
so detect and reject the buggy version and explain how to change to the
coreutils version. Once fixed the version output will hopefully change.

For simplicity, only check for the exact 0.8.0 version string. Hopefully
when it is fixed in Ubuntu, they also update the version string. Note
that earlier versions of uutils have the same issue of course, but those
versions were never the default "install" on Ubuntu (or anywhere else).

Signed-off-by: Peter Korsgaard <[email protected]>
Signed-off-by: Arnout Vandecappelle <[email protected]>
(cherry picked from commit 64cb69f)
Signed-off-by: Thomas Perale <[email protected]>
arnout pushed a commit to buildroot/buildroot that referenced this pull request Jun 5, 2026
uutils install 0.8.0 as used in Ubuntu 26.04 has a bug in the install
applet, breaking a number of packages:

uutils/coreutils#11505
https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2151454

The fix has been merged upstream but not yet released or packaged in Ubuntu,
so detect and reject the buggy version and explain how to change to the
coreutils version. Once fixed the version output will hopefully change.

For simplicity, only check for the exact 0.8.0 version string. Hopefully
when it is fixed in Ubuntu, they also update the version string. Note
that earlier versions of uutils have the same issue of course, but those
versions were never the default "install" on Ubuntu (or anywhere else).

Signed-off-by: Peter Korsgaard <[email protected]>
Signed-off-by: Arnout Vandecappelle <[email protected]>
(cherry picked from commit 64cb69f)
Signed-off-by: Thomas Perale <[email protected]>
@abendrothj
abendrothj deleted the fix/install-d-follow-symlinks branch October 1, 2026 06:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

install: install -D replaces symlink path components with real directories breaks our build system

3 participants