Skip to content

fix(cp): open source files with O_NOFOLLOW to prevent TOCTOU symlink swap#11995

Open
mattsu2020 wants to merge 4 commits intouutils:mainfrom
mattsu2020:cp_fix
Open

fix(cp): open source files with O_NOFOLLOW to prevent TOCTOU symlink swap#11995
mattsu2020 wants to merge 4 commits intouutils:mainfrom
mattsu2020:cp_fix

Conversation

@mattsu2020
Copy link
Copy Markdown
Contributor

@mattsu2020 mattsu2020 commented Apr 25, 2026

Summary

Fixes #10017

  • Problem: cp was vulnerable to a TOCTOU (Time-of-Check-Time-of-Use) attack. It checked whether a source is a symlink using lstat-style metadata, but then opened the source by path without O_NOFOLLOW. An attacker who could mutate the source tree concurrently could replace a regular file with a symlink between the check and the open, causing cp to copy the symlink target's contents.
  • Fix: Open source files with O_NOFOLLOW via OpenOptions::custom_flags() when symlink dereferencing is not requested (-L not specified). When -L/--dereference is used, symlinks are still followed as intended.
  • Impact: All platform modules updated (Linux, macOS, other Unix). std::fs::copy() fallbacks replaced with open_source() + buf_copy::copy_stream() to maintain O_NOFOLLOW protection throughout the copy pipeline.

Changes

  • src/uu/cp/src/platform/linux.rs — Added open_source() helper and copy_source_to_dest(). Threaded follow_symlinks through all internal functions (clone, sparse_copy, copy_stream, handle_reflink_*, etc.)
  • src/uu/cp/src/platform/macos.rs — Added open_source() helper. Replaced File::open() and fs::copy() with O_NOFOLLOW-aware alternatives.
  • src/uu/cp/src/platform/other_unix.rs — Same as macOS.
  • src/uu/cp/src/platform/other.rs — Signature update only (non-Unix, O_NOFOLLOW not available).
  • src/uu/cp/src/cp.rs — Threaded source_in_command_line through copy_helper to derive dereference flag for copy_on_write.

GNU cp parity

# GNU cp (before this fix):
openat(AT_FDCWD, "/tmp/src/file", O_RDONLY|O_NOFOLLOW)

# uutils cp (before):
openat(AT_FDCWD, "/tmp/src/file", O_RDONLY|O_CLOEXEC)  # no O_NOFOLLOW

# uutils cp (after this fix):
openat(AT_FDCWD, "/tmp/src/file", O_RDONLY|O_NOFOLLOW)  # matches GNU

Test plan

  • cargo build -p uu_cp — compiles successfully
  • cargo test -p uu_cp — existing unit tests pass
  • 6 new regression tests in tests/by-util/test_cp.rs:
    • copy_regular_file_works — basic copy still works
    • copy_dereference_follows_symlink-L follows symlinks
    • copy_symlink_as_link_no_dereference-P copies symlink as link
    • copy_reflink_never_regular_file--reflink=never path works
    • recursive_copy_regular_files — recursive copy works
    • copy_no_dereference_regular_file-P with regular file works
  • All existing cp integration tests pass (281 tests)

…swap (uutils#10017)

cp previously checked whether a source is a symlink using lstat-style
metadata, but then opened the source by path without O_NOFOLLOW. An
attacker who could mutate the source tree concurrently could replace a
regular file with a symlink between the check and the open, causing cp
to copy the symlink target's contents.

This fix adds O_NOFOLLOW when opening source files in all platform
modules (linux, macos, other_unix) when symlink dereferencing is not
requested. When -L/--dereference is used, symlinks are still followed
as the user intended.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 25, 2026

GNU testsuite comparison:

GNU test failed: tests/tail/pid-pipe. tests/tail/pid-pipe is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/cut/bounded-memory (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)

- Adjusted line breaks and formatting in `src/uu/cp/src/platform/linux.rs`
- Adjusted line breaks and formatting in `src/uu/cp/src/platform/macos.rs`
- Updated spell-checker ignore list to include `ELOOP` and `dstdir`
- Reformatted test code in `tests/by-util/test_cp.rs` for consistency
@oech3
Copy link
Copy Markdown
Contributor

oech3 commented Apr 25, 2026

Is copy_stream() used for normal cross-device copy after this PR?
If so, the name copy_stream is no longer a valid name.

The function is now used for both stream and regular file copies after
the O_NOFOLLOW fix, so the name should reflect its general purpose.
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.

cp TOCTOU: symlink swap bypasses no-dereference intent

2 participants