Skip to content

mv,cp: fix xattr TOCTOU by using file descriptor-based operations - #10545

Merged
sylvestre merged 1 commit into
uutils:mainfrom
sylvestre:issue-10014
Aug 14, 2026
Merged

mv,cp: fix xattr TOCTOU by using file descriptor-based operations#10545
sylvestre merged 1 commit into
uutils:mainfrom
sylvestre:issue-10014

Conversation

@sylvestre

Copy link
Copy Markdown
Contributor

Closes: #10014

@uutils uutils deleted a comment from github-actions Bot Jan 28, 2026
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
Note: The gnu test tests/cp/sparse-2 is now being skipped but was previously passing.

@sylvestre
sylvestre force-pushed the issue-10014 branch 2 times, most recently from 6dda6b4 to 3f06bad Compare February 21, 2026 21:03
@codspeed-hq

codspeed-hq Bot commented Feb 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 3.75%

⚡ 1 improved benchmark
✅ 352 untouched benchmarks
⏩ 50 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation complex_relative_date 330.5 µs 318.5 µs +3.75%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing sylvestre:issue-10014 (5b4f730) with main (c07e61f)

Open in CodSpeed

Footnotes

  1. 50 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.

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/date/date-locale-hour. tests/date/date-locale-hour is passing on 'main'. Maybe you have to rebase?

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/date/date-locale-hour. tests/date/date-locale-hour is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/date/resolution. tests/date/resolution is passing on 'main'. Maybe you have to rebase?

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
Skip an intermittent issue tests/tty/tty-eof (fails in this run but passes in the 'main' branch)

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/date/resolution. tests/date/resolution is passing on 'main'. Maybe you have to rebase?

@sylvestre
sylvestre marked this pull request as ready for review February 26, 2026 20:36
@sylvestre
sylvestre force-pushed the issue-10014 branch 2 times, most recently from cf75720 to 1514ccd Compare March 16, 2026 21:59
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/pr/bounded-memory (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/expand/bounded-memory is now passing!

@sylvestre
sylvestre force-pushed the issue-10014 branch 2 times, most recently from 35596ca to 5fa39b8 Compare March 17, 2026 10:45
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/file-perm-race. tests/cp/file-perm-race is passing on 'main'. Maybe you have to rebase?
Congrats! The gnu test tests/expand/bounded-memory is now passing!

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/existing-perm-race. tests/cp/existing-perm-race is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/sparse. tests/cp/sparse is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/sparse-2. tests/cp/sparse-2 is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/cp/sparse-extents-2. tests/cp/sparse-extents-2 is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/tail/symlink (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/tail/pipe-f is now passing!

@sylvestre
sylvestre force-pushed the issue-10014 branch 3 times, most recently from 867b6a0 to 09664a9 Compare March 18, 2026 22:16
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/misc/io-errors. tests/misc/io-errors is passing on 'main'. Maybe you have to rebase?
GNU test failed: tests/tac/tac-2-nonseekable. tests/tac/tac-2-nonseekable is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/date/date-locale-hour (passes in this run but fails in the 'main' branch)
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/rm/many-dir-entries-vs-OOM is now being skipped but was previously passing.
Note: The gnu test tests/unexpand/bounded-memory is now being skipped but was previously passing.
Congrats! The gnu test tests/tail/tail-n0f is now passing!

@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

GNU test failed: tests/cp/cp-mv-enotsup-xattr. tests/cp/cp-mv-enotsup-xattr is passing on 'main'. Maybe you have to rebase?
Skipping an intermittent issue tests/tty/tty-eof (passes in this run but fails in the 'main' branch)
Note: The gnu test tests/expand/bounded-memory is now being skipped but was previously passing.
Congrats! The gnu test tests/basenc/bounded-memory is now passing!
Congrats! The gnu test tests/dd/no-allocate is now passing!
Congrats! The gnu test tests/unexpand/bounded-memory is now passing!

@sylvestre
sylvestre force-pushed the issue-10014 branch 3 times, most recently from 0211168 to 7f35960 Compare March 23, 2026 06:39
@github-actions

Copy link
Copy Markdown

GNU testsuite comparison:

Skip an intermittent issue tests/tty/tty-eof (fails in this run but passes in the 'main' branch)
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)
Congrats! The gnu test tests/basenc/bounded-memory is now passing!
Congrats! The gnu test tests/dd/no-allocate is now passing!

@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/date/resolution (fails in this run but passes in the 'main' branch)
Note: The gnu test tests/expand/bounded-memory is now being skipped but was previously passing.
Congrats! The gnu test tests/printf/printf-surprise is now passing!

@github-actions

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)
Congrats! The gnu test tests/misc/io-errors is no longer failing!
Congrats! The gnu test tests/cp/link-heap is now passing!
Congrats! The gnu test tests/rm/many-dir-entries-vs-OOM is now passing!

Copilot AI lite review requested due to automatic review settings August 13, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR hardens mv/cp xattr preservation against TOCTOU races by switching to file-descriptor-based xattr operations, and adds regression tests around directory xattrs on cross-device moves.

Changes:

  • Add fd-based xattr retrieve/apply helpers in uucore::fsxattr (plus unit test).
  • Update mv directory fallback path to retrieve/apply xattrs via open file descriptors.
  • Update cp to use fd-based xattr copying for regular files and add an integration test for cross-device dir xattrs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
tests/by-util/test_mv.rs Adds Linux integration test ensuring cross-device directory xattrs are preserved.
src/uucore/src/lib/features/fsxattr.rs Introduces *_fd xattr APIs and unit test coverage.
src/uu/mv/src/mv.rs Switches directory fallback xattr handling to fd-based operations and ignores unsupported FS errors.
src/uu/cp/src/cp.rs Uses fd-based xattr copying for regular files to reduce TOCTOU exposure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/by-util/test_mv.rs
"directory xattr was not preserved across devices: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(out.stdout, b"dirvalue");
Comment thread tests/by-util/test_mv.rs
Comment on lines +3089 to +3091
let other_fs_tempdir =
TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm");
let dst_path = other_fs_tempdir.path().join("dst_dir");
Comment on lines +458 to +465
#[test]
fn test_apply_and_retrieve_xattrs_fd() {
use std::fs::OpenOptions;

let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test_file.txt");

File::create(&file_path).unwrap();
Comment thread src/uu/mv/src/mv.rs Outdated
fsxattr::apply_xattrs(to, xattrs)?;
{
use std::fs::File;
fsxattr::apply_xattrs_fd_ignore_unsupported(&File::open(to)?, xattrs)?;
Comment thread src/uucore/src/lib/features/fsxattr.rs Outdated
Comment on lines +143 to +145
/// A result containing a HashMap of attribute names and values, or an error.
#[cfg(unix)]
pub fn retrieve_xattrs_fd(source: &std::fs::File) -> std::io::Result<FxHashMap<OsString, Vec<u8>>> {
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Binary size comparison:

Individual binary size comparison VS main (threshold: >=5% AND >=4 KB).

Total size of compared binaries: 148.97 MB (+2.05 MB, +1.39%)

Significant per-binary changes:
  ls          2.11 MB ->    3.25 MB  (+1.14 MB, +54.36%)
  [           1.09 MB ->    1.18 MB  (+88 KB, +7.89%)
  test        1.09 MB ->    1.18 MB  (+88 KB, +7.89%)
  chmod       1.14 MB ->    1.22 MB  (+84 KB, +7.22%)
  mkdir       1.09 MB ->    1.17 MB  (+84 KB, +7.53%)
  install     1.24 MB ->    1.32 MB  (+80 KB, +6.29%)
  mkfifo      1.08 MB ->    1.16 MB  (+80 KB, +7.22%)
  mknod       1.09 MB ->    1.17 MB  (+80 KB, +7.17%)

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/date/resolution (passes in this run but fails in the 'main' branch)
Congrats! The gnu test tests/cp/parent-perm-race is no longer failing!
Congrats! The gnu test tests/cp/sparse-to-pipe is no longer failing!
Note: The gnu test tests/dd/no-allocate is now being skipped but was previously passing.
Congrats! The gnu test tests/basenc/bounded-memory is now passing!
Congrats! The gnu test tests/rm/many-dir-entries-vs-OOM is now passing!
Congrats! The gnu test tests/tail/tail-n0f is now passing!
Skip an intermittent issue tests/pr/bounded-memory (was skipped on 'main', now failing)

Copilot AI review requested due to automatic review settings August 14, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (3)

tests/by-util/test_mv.rs:3091

  • This test will hard-fail on environments where /dev/shm is missing/unwritable, and it also doesn't verify that the move is actually cross-device (so it may silently test the rename fast-path instead of the copy+delete fallback). Consider skipping gracefully if /dev/shm can't be used, and skip if source and destination are on the same device.
    let other_fs_tempdir =
        TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm");
    let dst_path = other_fs_tempdir.path().join("dst_dir");

src/uucore/src/lib/features/fsxattr.rs:474

  • This unit test currently unwrap()s xattr operations, which will fail on filesystems/environments where user xattrs are unsupported/disabled. Other xattr tests in this module already skip in that situation; this one should too. Also, since the tested APIs are #[cfg(unix)], the test should be gated similarly.
        // Apply using file descriptor
        let file = OpenOptions::new().write(true).open(&file_path).unwrap();
        apply_xattrs_fd(&file, test_xattrs).unwrap();

tests/by-util/test_mv.rs:3114

  • getfattr --only-values typically includes a trailing newline, so comparing out.stdout to b"dirvalue" is likely to be flaky. Also, this test currently panics if getfattr isn't available. Consider skipping gracefully on spawn failure and trimming stdout before comparing.
        .output()
        .expect("failed to run getfattr on the moved directory");
    assert!(
        out.status.success(),
        "directory xattr was not preserved across devices: {}",

Copilot AI review requested due to automatic review settings August 14, 2026 08:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/uucore/src/lib/features/fsxattr.rs:475

  • The new fd-based xattr test can fail on filesystems that don't support user xattrs (common in some CI/container setups). Other xattr tests in this module skip when xattr operations return ENOTSUP/EOPNOTSUPP; this one should do the same to avoid flaky failures unrelated to the logic being tested.
        // Apply using file descriptor
        let file = OpenOptions::new().write(true).open(&file_path).unwrap();
        apply_xattrs_fd(&file, test_xattrs).unwrap();
        drop(file);

tests/by-util/test_mv.rs:3061

  • This comment is misleading: attempting to open a directory for writing typically fails (e.g., EISDIR), rather than "silently" dropping xattrs. Rewording avoids confusion about what failure mode is being guarded against.
/// Cross-device mv of a directory must preserve the directory's own xattrs.
/// The fd-based xattr path has to open the destination read-only: a directory
/// cannot be opened for writing, so a write-mode open would silently drop them.

Path-based xattr calls in cp's copy_extended_attrs and mv's
rename_dir_fallback can be redirected to a different inode by a
concurrent renamer between the list and the set. Pin the inodes by
doing the list/get/set through open file descriptors instead.

The destination fd is opened read-only: a directory cannot be opened
for writing, and fsetxattr checks write permission on the inode rather
than the open mode, so read-only works for both files and directories.

Closes: uutils#10014
Copilot AI review requested due to automatic review settings August 14, 2026 09:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (5)

tests/by-util/test_mv.rs:3092

  • The test intends to exercise the cross-device (EXDEV) fallback, but it doesn’t verify that the fixtures directory and /dev/shm are actually on different devices. If they’re the same filesystem in a given environment, this test can pass without covering the fallback path. Consider skipping when the device IDs match.
    let other_fs_tempdir =
        TempDir::new_in("/dev/shm/").expect("Unable to create temp directory in /dev/shm");
    let dst_path = other_fs_tempdir.path().join("dst_dir");

src/uucore/src/lib/features/fsxattr.rs:180

  • apply_xattrs_fd works on directory file descriptors too (it’s used that way in mv’s directory fallback), but the doc comment describes only “file”. Aligning the documentation with actual usage helps prevent incorrect assumptions by future callers.
/// Applies extended attributes (xattrs) to a given file using a file descriptor.
///
/// This version avoids TOCTOU races by operating on an open file descriptor
/// rather than a path, ensuring all operations target the same inode.
///

tests/by-util/test_mv.rs:3108

  • This test skips when setfattr cannot be executed, but it will panic if getfattr is missing or cannot be executed. Handle Command::new("getfattr").output() errors the same way and skip the test, so CI environments without getfattr don’t fail spuriously.
    let out = Command::new("getfattr")
        .args([
            "-n",
            "user.dirattr",
            "--only-values",
            dst_path.to_str().unwrap(),
        ])
        .output()
        .expect("failed to run getfattr on the moved directory");

src/uucore/src/lib/features/fsxattr.rs:145

  • retrieve_xattrs_fd is used for directories as well (e.g. mv cross-device directory fallback), but the doc comment currently says it retrieves xattrs for a “file”. Updating the docs avoids misleading callers about supported handle types.

This issue also appears on line 176 of the same file.

/// Retrieves the extended attributes (xattrs) of a given file using a file descriptor.
///
/// This version avoids TOCTOU races by operating on an open file descriptor
/// rather than a path, ensuring all operations target the same inode.
///

src/uu/cp/src/cp.rs:1742

  • The comment says copy_extended_attrs “uses file descriptor-based operations”, but the implementation only does so for regular files (metadata.is_file()); directories and other types still use path-based xattr operations. Clarifying the comment avoids overstating the TOCTOU mitigation coverage.
///
/// Uses file descriptor-based operations to avoid TOCTOU races during xattr copying.
#[cfg(all(unix, not(target_os = "android")))]

@sylvestre
sylvestre merged commit 6372fd3 into uutils:main Aug 14, 2026
168 checks passed
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.

mv xattr preservation TOCTOU on cross-device moves

2 participants