Skip to content

Conversation

@abendrothj
Copy link
Contributor

This PR fixes a TOCTOU (Time-of-Check-Time-of-Use) race condition in the install -D command where an attacker could replace a directory component with a symlink between directory creation and file installation, redirecting writes to an arbitrary location.

Changes

  • Added mkdir_at() and open_file_at() methods to DirFd for safe operations
  • Added open_no_follow() to prevent following symlinks when opening directories
  • Added create_dir_all_safe() function that uses directory file descriptors
  • Modified install -D to use safe traversal functions instead of pathname-based ops
  • Added copy_file_safe() function for safe file copying using directory fds
  • Added tests to verify the fix prevents symlink race conditions

How the Fix Works

The fix prevents the race condition by:

  1. Using directory file descriptors (mkdirat/openat) instead of pathnames
  2. Keeping directory fds open throughout the operation
  3. Detecting and removing symlinks before directory creation
  4. Anchoring all file operations to directory file descriptors

This eliminates the race window by ensuring all critical operations use directory file descriptors that cannot be replaced by symlinks.

Testing

Fixes #10013

@github-actions
Copy link

github-actions bot commented Jan 9, 2026

GNU testsuite comparison:

GNU test failed: tests/install/basic-1. tests/install/basic-1 is passing on 'main'. Maybe you have to rebase?

@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from fc7eade to 0b91865 Compare January 15, 2026 09:10
@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/install/basic-1. tests/install/basic-1 is passing on 'main'. Maybe you have to rebase?

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 15, 2026

Merging this PR will not alter performance

✅ 284 untouched benchmarks
⏩ 38 skipped benchmarks1


Comparing abendrothj:fix/install-symlink-race-condition-10013 (89c2d2b) with main (bb91a5b)

Open in CodSpeed

Footnotes

  1. 38 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
Copy link
Contributor Author

The 3.46% performance regression is an acceptable trade-off for fixing the symlink race condition vulnerability. The safe directory traversal using file descriptors adds unavoidable overhead, but prevents potential security exploits.

@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/tail/follow-name. tests/tail/follow-name is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/shuf/shuf-reservoir (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/sort/sort-stale-thread-mem (fails in this run but passes in the 'main' branch)
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)

@sylvestre
Copy link
Contributor

sorry but could you please rebase it ? thanks

@sylvestre
Copy link
Contributor

The 3.46% performance regression is an acceptable trade-off for fixing the symlink race condition vulnerability. The safe directory traversal using file descriptors adds unavoidable overhead, but prevents potential security exploits.

agreed

@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch 2 times, most recently from fe7aec2 to 4fb0779 Compare January 19, 2026 07:29
@sylvestre
Copy link
Contributor

some conflicts, sorry

@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from 4fb0779 to f95bed7 Compare January 20, 2026 03:12
@abendrothj
Copy link
Contributor Author

There, that should resolve the conflicts :)

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/follow-name (fails in this run but passes in the 'main' branch)
Congrats! The gnu test tests/dd/stderr is no longer failing!
Congrats! The gnu test tests/tac/tac-2-nonseekable is no longer failing!
Congrats! The gnu test tests/tail/follow-stdin is no longer failing!

@abendrothj
Copy link
Contributor Author

abendrothj commented Jan 20, 2026

The sort regression is benchmark noise - this PR only touches install and safe_traversal, which sort doesn't use. Seems like the android emulator failure is unrelated too.

@sylvestre
Copy link
Contributor

This is a lot of added complexity and I think you could have a bit more polish before submitting for review :/

@abendrothj
Copy link
Contributor Author

This is a lot of added complexity and I think you could have a bit more polish before submitting for review :/

Yeah that was rough, won't happen again

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/shuf/shuf-reservoir (fails in this run but passes in the 'main' branch)
Skip an intermittent issue tests/sort/sort-stale-thread-mem (fails in this run but passes in the 'main' branch)

@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from b804df6 to d138468 Compare January 21, 2026 07:12
@github-actions
Copy link

GNU testsuite comparison:

Congrats! The gnu test tests/factor/t10 is no longer failing!
Congrats! The gnu test tests/factor/t26 is no longer failing!
Congrats! The gnu test tests/factor/t27 is no longer failing!
Congrats! The gnu test tests/factor/t28 is no longer failing!
Congrats! The gnu test tests/factor/t29 is no longer failing!
Congrats! The gnu test tests/factor/t30 is no longer failing!
Congrats! The gnu test tests/factor/t31 is no longer failing!
Congrats! The gnu test tests/factor/t32 is no longer failing!
Congrats! The gnu test tests/factor/t36 is no longer failing!

@github-actions
Copy link

GNU testsuite comparison:

Skipping an intermittent issue tests/shuf/shuf-reservoir (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/sort/sort-stale-thread-mem (passes in this run but fails in the 'main' branch)

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/tail/inotify-dir-recreate (fails in this run but passes in the 'main' branch)

/// Copy a file using directory file descriptor for safe traversal.
/// This prevents symlink race conditions when creating target files.
#[cfg(unix)]
fn copy_file_safe(from: &Path, to_parent_fd: &DirFd, to_filename: &std::ffi::OsStr) -> UResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some code duplication with copy_file it seems

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can't be easily consolidated as they have different semantics:
copy_file_safe: fd-based (open_file_at), truncates existing files
copy_file: path-based (create_new), removes first, includes directory override check
Both are used: copy_file_safe at line 774 (safe -D path), copy_file at line 1148 (regular installs). Consolidating would require multiple match branches and obscure the distinct behaviors.

@abendrothj abendrothj requested a review from sylvestre February 4, 2026 22:33
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

GNU testsuite comparison:

Skipping an intermittent issue tests/misc/usage_vs_getopt (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/shuf/shuf-reservoir (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/sort/sort-stale-thread-mem (passes in this run but fails in the 'main' branch)

@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from b06f16f to 268e92d Compare February 6, 2026 10:19
@abendrothj
Copy link
Contributor Author

abendrothj commented Feb 6, 2026

Failures seem unrelated to my PR and it looks like theres a decision to make on upstream- cargo-deny: time v0.3.47 requires Rust 1.88.0 but the project's MSRV is 1.85.0. This will affect all PRs and main too. Upstream needs to either bump MSRV or add an ignore for it, definitely out of my bounds in this PR.

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

GNU testsuite comparison:

Skipping an intermittent issue tests/misc/usage_vs_getopt (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/shuf/shuf-reservoir (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/sort/sort-stale-thread-mem (passes in this run but fails in the 'main' branch)

…ils#10013)

This commit fixes a TOCTOU (Time-of-Check-Time-of-Use) race condition
in the install -D command where an attacker could replace a directory
component with a symlink between directory creation and file installation,
redirecting writes to an arbitrary location.

Changes:
- Added mkdir_at() and open_file_at() methods to DirFd for safe operations
- Added open_no_follow() to prevent following symlinks when opening directories
- Added create_dir_all_safe() function that uses directory file descriptors
- Modified install -D to use safe traversal functions instead of pathname-based ops
- Added copy_file_safe() function for safe file copying using directory fds
- Added tests to verify the fix prevents symlink race conditions

The fix works by:
1. Using directory file descriptors (mkdirat/openat) instead of pathnames
2. Keeping directory fds open throughout the operation
3. Detecting and removing symlinks before directory creation
4. Anchoring all file operations to directory file descriptors

This eliminates the race window by ensuring all critical operations use
directory file descriptors that cannot be replaced by symlinks.
…x context setting to Linux platforms

This commit updates the conditional compilation for SELinux context settings in the install module to only apply when the target OS is Linux. This change ensures that SELinux-related functionality is not erroneously included on non-Linux platforms, improving compatibility and preventing potential issues.

Changes:
- Modified `#[cfg(feature = "selinux")]` to `#[cfg(all(feature = "selinux", target_os = "linux"))]` in multiple locations to enforce the OS-specific behavior.
…s a file

When -t is used with -D and the target exists as a file (not a directory),
we should fail immediately without printing verbose directory creation
messages. This matches GNU behavior and fixes the failing GNU test
tests/install/basic-1.
- Replace std::io::copy() with copy_stream() for consistency and splice optimization
- Consolidate DirFd::open() and open_subdir() to take follow_symlinks parameter
- Extract finalize_installed_file() helper to reduce code duplication
- Add documentation for security behavior (symlink removal) and mode handling
- Clean up verbose comments
@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from 268e92d to d694c9e Compare February 11, 2026 21:10
@abendrothj abendrothj force-pushed the fix/install-symlink-race-condition-10013 branch from d694c9e to a1ef268 Compare February 11, 2026 21:13
@github-actions
Copy link

GNU testsuite comparison:

GNU test failed: tests/pr/bounded-memory. tests/pr/bounded-memory is passing on 'main'. Maybe you have to rebase?

@sylvestre
Copy link
Contributor

it looks great, i will wait for the ci to finish

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.

Symlink race in install -D directory creation

2 participants