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

Closes: #10014
This commit is contained in:
Sylvestre Ledru
2026-01-28 22:04:23 +01:00
parent 92f1739a25
commit 41cd49fc1b
3 changed files with 157 additions and 5 deletions
+12 -1
View File
@@ -16,7 +16,7 @@ use std::os::unix::net::UnixListener;
use std::path::{Path, PathBuf, StripPrefixError};
use std::{fmt, io};
#[cfg(all(unix, not(target_os = "android")))]
use uucore::fsxattr::{copy_xattrs, copy_xattrs_skip_selinux};
use uucore::fsxattr::{copy_xattrs_fd, copy_xattrs_skip_selinux};
use uucore::translate;
use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser};
@@ -1713,8 +1713,12 @@ pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> Copy
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
/// or if xattr copying fails.
///
/// Uses file descriptor-based operations to avoid TOCTOU races during xattr copying.
#[cfg(all(unix, not(target_os = "android")))]
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
use std::fs::File;
use uucore::fsxattr::copy_xattrs;
let metadata = fs::symlink_metadata(dest)?;
// Check if the destination file is currently read-only for the user.
@@ -1734,6 +1738,13 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe
// When -Z is used, skip copying security.selinux xattr so that
// the default context can be set instead of preserving from source
copy_xattrs_skip_selinux(source, dest)
} else if metadata.is_file() {
// Use file descriptor-based operations for regular files to avoid TOCTOU races.
// Directories cannot be opened with write mode for xattr operations
// Symlinks (especially dangling ones) cannot be opened via File::open
let source_file = File::open(source)?;
let dest_file = OpenOptions::new().write(true).open(dest)?;
copy_xattrs_fd(&source_file, &dest_file)
} else {
copy_xattrs(source, dest)
};
+23 -4
View File
@@ -967,8 +967,14 @@ fn rename_dir_fallback(
(_, _) => None,
};
// Retrieve xattrs using file descriptor to avoid TOCTOU races
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
let xattrs = fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| FxHashMap::default());
let xattrs = {
use std::fs::File;
File::open(from)
.and_then(|f| fsxattr::retrieve_xattrs_fd(&f))
.unwrap_or_else(|_| FxHashMap::default())
};
// Use directory copying (with or without hardlink support)
let result = copy_dir_contents(
@@ -983,8 +989,14 @@ fn rename_dir_fallback(
display_manager,
);
// Apply xattrs using file descriptor to avoid TOCTOU races
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
fsxattr::apply_xattrs(to, xattrs)?;
{
use std::fs::OpenOptions;
if let Ok(f) = OpenOptions::new().write(true).open(to) {
fsxattr::apply_xattrs_fd(&f, xattrs)?;
}
}
result?;
@@ -1215,12 +1227,19 @@ fn rename_file_fallback(
Ok(())
}
/// Copy xattrs from source to destination, ignoring ENOTSUP/EOPNOTSUPP errors.
/// Copy xattrs from source to destination using file descriptors, ignoring ENOTSUP/EOPNOTSUPP errors.
/// This version avoids TOCTOU races by operating on file descriptors rather than paths.
/// These errors indicate the filesystem doesn't support extended attributes,
/// which is acceptable when moving files across filesystems.
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
fn copy_xattrs_if_supported(from: &Path, to: &Path) -> io::Result<()> {
match fsxattr::copy_xattrs(from, to) {
use std::fs::{File, OpenOptions};
// Open both files to pin their inodes, avoiding TOCTOU races during xattr operations
let source_file = File::open(from)?;
let dest_file = OpenOptions::new().write(true).open(to)?;
match fsxattr::copy_xattrs_fd(&source_file, &dest_file) {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => Ok(()),
Err(e) => Err(e),
+122
View File
@@ -9,9 +9,11 @@
use itertools::Itertools;
use rustc_hash::FxHashMap;
use std::ffi::{OsStr, OsString};
use std::fs::File;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use xattr::FileExt;
/// Copies extended attributes (xattrs) from one file or directory to another.
///
@@ -45,6 +47,29 @@ pub fn copy_xattrs_skip_selinux<P: AsRef<Path>>(source: P, dest: P) -> std::io::
Ok(())
}
/// Copies extended attributes (xattrs) from one file to another using file descriptors.
///
/// This version avoids TOCTOU (time-of-check to time-of-use) races by operating
/// on open file descriptors rather than paths, ensuring all operations target
/// the same inodes throughout.
///
/// # Arguments
///
/// * `source` - A reference to the source file (open file descriptor).
/// * `dest` - A reference to the destination file (open file descriptor).
///
/// # Returns
///
/// A result indicating success or failure.
pub fn copy_xattrs_fd(source: &File, dest: &File) -> std::io::Result<()> {
for attr_name in source.list_xattr()? {
if let Some(value) = source.get_xattr(&attr_name)? {
dest.set_xattr(&attr_name, &value)?;
}
}
Ok(())
}
/// Retrieves the extended attributes (xattrs) of a given file or directory.
///
/// # Arguments
@@ -64,6 +89,28 @@ pub fn retrieve_xattrs<P: AsRef<Path>>(source: P) -> std::io::Result<FxHashMap<O
Ok(attrs)
}
/// 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.
///
/// # Arguments
///
/// * `source` - A reference to the file (open file descriptor).
///
/// # Returns
///
/// A result containing a HashMap of attribute names and values, or an error.
pub fn retrieve_xattrs_fd(source: &File) -> std::io::Result<FxHashMap<OsString, Vec<u8>>> {
let mut attrs = FxHashMap::default();
for attr_name in source.list_xattr()? {
if let Some(value) = source.get_xattr(&attr_name)? {
attrs.insert(attr_name, value);
}
}
Ok(attrs)
}
/// Applies extended attributes (xattrs) to a given file or directory.
///
/// # Arguments
@@ -84,6 +131,26 @@ pub fn apply_xattrs<P: AsRef<Path>>(
Ok(())
}
/// 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.
///
/// # Arguments
///
/// * `dest` - A reference to the file (open file descriptor).
/// * `xattrs` - A HashMap containing attribute names and their corresponding values.
///
/// # Returns
///
/// A result indicating success or failure.
pub fn apply_xattrs_fd(dest: &File, xattrs: FxHashMap<OsString, Vec<u8>>) -> std::io::Result<()> {
for (attr, value) in xattrs {
dest.set_xattr(&attr, &value)?;
}
Ok(())
}
/// Checks if a file has an Access Control List (ACL) based on its extended attributes.
///
/// # Arguments
@@ -299,4 +366,59 @@ mod tests {
assert!(has_security_cap_acl(&file_path));
}
}
#[test]
fn test_copy_xattrs_fd() {
use std::fs::OpenOptions;
let temp_dir = tempdir().unwrap();
let source_path = temp_dir.path().join("source.txt");
let dest_path = temp_dir.path().join("dest.txt");
File::create(&source_path).unwrap();
File::create(&dest_path).unwrap();
let test_attr = "user.test_fd";
let test_value = b"test value fd";
xattr::set(&source_path, test_attr, test_value).unwrap();
let source_file = File::open(&source_path).unwrap();
let dest_file = OpenOptions::new().write(true).open(&dest_path).unwrap();
copy_xattrs_fd(&source_file, &dest_file).unwrap();
let copied_value = xattr::get(&dest_path, test_attr).unwrap().unwrap();
assert_eq!(copied_value, test_value);
}
#[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();
let mut test_xattrs = FxHashMap::default();
let test_attr = "user.test_attr_fd";
let test_value = b"test value fd";
test_xattrs.insert(OsString::from(test_attr), test_value.to_vec());
// Apply using file descriptor
let file = OpenOptions::new().write(true).open(&file_path).unwrap();
apply_xattrs_fd(&file, test_xattrs).unwrap();
drop(file);
// Retrieve using file descriptor
let file = File::open(&file_path).unwrap();
let retrieved_xattrs = retrieve_xattrs_fd(&file).unwrap();
assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str()));
assert_eq!(
retrieved_xattrs
.get(OsString::from(test_attr).as_os_str())
.unwrap(),
test_value
);
}
}