Skip to content

Commit eb38e8a

Browse files
committed
mv,cp: fix xattr TOCTOU by using file descriptor-based operations
Closes: #10014
1 parent 2f38180 commit eb38e8a

3 files changed

Lines changed: 158 additions & 5 deletions

File tree

src/uu/cp/src/cp.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::os::unix::net::UnixListener;
1616
use std::path::{Path, PathBuf, StripPrefixError};
1717
use std::{fmt, io};
1818
#[cfg(all(unix, not(target_os = "android")))]
19-
use uucore::fsxattr::{copy_xattrs, copy_xattrs_skip_selinux};
19+
use uucore::fsxattr::{copy_xattrs_fd, copy_xattrs_skip_selinux};
2020
use uucore::translate;
2121

2222
use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser};
@@ -1711,8 +1711,12 @@ pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> Copy
17111711
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
17121712
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
17131713
/// or if xattr copying fails.
1714+
///
1715+
/// Uses file descriptor-based operations to avoid TOCTOU races during xattr copying.
17141716
#[cfg(all(unix, not(target_os = "android")))]
17151717
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
1718+
use std::fs::File;
1719+
use uucore::fsxattr::copy_xattrs;
17161720
let metadata = fs::symlink_metadata(dest)?;
17171721

17181722
// Check if the destination file is currently read-only for the user.
@@ -1732,6 +1736,13 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe
17321736
// When -Z is used, skip copying security.selinux xattr so that
17331737
// the default context can be set instead of preserving from source
17341738
copy_xattrs_skip_selinux(source, dest)
1739+
} else if metadata.is_file() {
1740+
// Use file descriptor-based operations for regular files to avoid TOCTOU races.
1741+
// Directories cannot be opened with write mode for xattr operations
1742+
// Symlinks (especially dangling ones) cannot be opened via File::open
1743+
let source_file = File::open(source)?;
1744+
let dest_file = OpenOptions::new().write(true).open(dest)?;
1745+
copy_xattrs_fd(&source_file, &dest_file)
17351746
} else {
17361747
copy_xattrs(source, dest)
17371748
};

src/uu/mv/src/mv.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -967,8 +967,14 @@ fn rename_dir_fallback(
967967
(_, _) => None,
968968
};
969969

970+
// Retrieve xattrs using file descriptor to avoid TOCTOU races
970971
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
971-
let xattrs = fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| FxHashMap::default());
972+
let xattrs = {
973+
use std::fs::File;
974+
File::open(from)
975+
.and_then(|f| fsxattr::retrieve_xattrs_fd(&f))
976+
.unwrap_or_else(|_| FxHashMap::default())
977+
};
972978

973979
// Use directory copying (with or without hardlink support)
974980
let result = copy_dir_contents(
@@ -983,8 +989,14 @@ fn rename_dir_fallback(
983989
display_manager,
984990
);
985991

992+
// Apply xattrs using file descriptor to avoid TOCTOU races
986993
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
987-
fsxattr::apply_xattrs(to, xattrs)?;
994+
{
995+
use std::fs::OpenOptions;
996+
if let Ok(f) = OpenOptions::new().write(true).open(to) {
997+
fsxattr::apply_xattrs_fd(&f, xattrs)?;
998+
}
999+
}
9881000

9891001
result?;
9901002

@@ -1206,12 +1218,19 @@ fn rename_file_fallback(
12061218
Ok(())
12071219
}
12081220

1209-
/// Copy xattrs from source to destination, ignoring ENOTSUP/EOPNOTSUPP errors.
1221+
/// Copy xattrs from source to destination using file descriptors, ignoring ENOTSUP/EOPNOTSUPP errors.
1222+
/// This version avoids TOCTOU races by operating on file descriptors rather than paths.
12101223
/// These errors indicate the filesystem doesn't support extended attributes,
12111224
/// which is acceptable when moving files across filesystems.
12121225
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
12131226
fn copy_xattrs_if_supported(from: &Path, to: &Path) -> io::Result<()> {
1214-
match fsxattr::copy_xattrs(from, to) {
1227+
use std::fs::{File, OpenOptions};
1228+
1229+
// Open both files to pin their inodes, avoiding TOCTOU races during xattr operations
1230+
let source_file = File::open(from)?;
1231+
let dest_file = OpenOptions::new().write(true).open(to)?;
1232+
1233+
match fsxattr::copy_xattrs_fd(&source_file, &dest_file) {
12151234
Ok(()) => Ok(()),
12161235
Err(e) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => Ok(()),
12171236
Err(e) => Err(e),

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
//! Set of functions to manage xattr on files and dirs
99
use itertools::Itertools;
1010
use rustc_hash::FxHashMap;
11+
use std::collections::HashMap;
1112
use std::ffi::{OsStr, OsString};
13+
use std::fs::File;
1214
#[cfg(unix)]
1315
use std::os::unix::ffi::OsStrExt;
1416
use std::path::Path;
17+
use xattr::FileExt;
1518

1619
/// Copies extended attributes (xattrs) from one file or directory to another.
1720
///
@@ -45,6 +48,29 @@ pub fn copy_xattrs_skip_selinux<P: AsRef<Path>>(source: P, dest: P) -> std::io::
4548
Ok(())
4649
}
4750

51+
/// Copies extended attributes (xattrs) from one file to another using file descriptors.
52+
///
53+
/// This version avoids TOCTOU (time-of-check to time-of-use) races by operating
54+
/// on open file descriptors rather than paths, ensuring all operations target
55+
/// the same inodes throughout.
56+
///
57+
/// # Arguments
58+
///
59+
/// * `source` - A reference to the source file (open file descriptor).
60+
/// * `dest` - A reference to the destination file (open file descriptor).
61+
///
62+
/// # Returns
63+
///
64+
/// A result indicating success or failure.
65+
pub fn copy_xattrs_fd(source: &File, dest: &File) -> std::io::Result<()> {
66+
for attr_name in source.list_xattr()? {
67+
if let Some(value) = source.get_xattr(&attr_name)? {
68+
dest.set_xattr(&attr_name, &value)?;
69+
}
70+
}
71+
Ok(())
72+
}
73+
4874
/// Retrieves the extended attributes (xattrs) of a given file or directory.
4975
///
5076
/// # Arguments
@@ -64,6 +90,28 @@ pub fn retrieve_xattrs<P: AsRef<Path>>(source: P) -> std::io::Result<FxHashMap<O
6490
Ok(attrs)
6591
}
6692

93+
/// Retrieves the extended attributes (xattrs) of a given file using a file descriptor.
94+
///
95+
/// This version avoids TOCTOU races by operating on an open file descriptor
96+
/// rather than a path, ensuring all operations target the same inode.
97+
///
98+
/// # Arguments
99+
///
100+
/// * `source` - A reference to the file (open file descriptor).
101+
///
102+
/// # Returns
103+
///
104+
/// A result containing a HashMap of attribute names and values, or an error.
105+
pub fn retrieve_xattrs_fd(source: &File) -> std::io::Result<FxHashMap<OsString, Vec<u8>>> {
106+
let mut attrs = FxHashMap::default();
107+
for attr_name in source.list_xattr()? {
108+
if let Some(value) = source.get_xattr(&attr_name)? {
109+
attrs.insert(attr_name, value);
110+
}
111+
}
112+
Ok(attrs)
113+
}
114+
67115
/// Applies extended attributes (xattrs) to a given file or directory.
68116
///
69117
/// # Arguments
@@ -84,6 +132,26 @@ pub fn apply_xattrs<P: AsRef<Path>>(
84132
Ok(())
85133
}
86134

135+
/// Applies extended attributes (xattrs) to a given file using a file descriptor.
136+
///
137+
/// This version avoids TOCTOU races by operating on an open file descriptor
138+
/// rather than a path, ensuring all operations target the same inode.
139+
///
140+
/// # Arguments
141+
///
142+
/// * `dest` - A reference to the file (open file descriptor).
143+
/// * `xattrs` - A HashMap containing attribute names and their corresponding values.
144+
///
145+
/// # Returns
146+
///
147+
/// A result indicating success or failure.
148+
pub fn apply_xattrs_fd(dest: &File, xattrs: FxHashMap<OsString, Vec<u8>>) -> std::io::Result<()> {
149+
for (attr, value) in xattrs {
150+
dest.set_xattr(&attr, &value)?;
151+
}
152+
Ok(())
153+
}
154+
87155
/// Checks if a file has an Access Control List (ACL) based on its extended attributes.
88156
///
89157
/// # Arguments
@@ -299,4 +367,59 @@ mod tests {
299367
assert!(has_security_cap_acl(&file_path));
300368
}
301369
}
370+
371+
#[test]
372+
fn test_copy_xattrs_fd() {
373+
use std::fs::OpenOptions;
374+
375+
let temp_dir = tempdir().unwrap();
376+
let source_path = temp_dir.path().join("source.txt");
377+
let dest_path = temp_dir.path().join("dest.txt");
378+
379+
File::create(&source_path).unwrap();
380+
File::create(&dest_path).unwrap();
381+
382+
let test_attr = "user.test_fd";
383+
let test_value = b"test value fd";
384+
xattr::set(&source_path, test_attr, test_value).unwrap();
385+
386+
let source_file = File::open(&source_path).unwrap();
387+
let dest_file = OpenOptions::new().write(true).open(&dest_path).unwrap();
388+
389+
copy_xattrs_fd(&source_file, &dest_file).unwrap();
390+
391+
let copied_value = xattr::get(&dest_path, test_attr).unwrap().unwrap();
392+
assert_eq!(copied_value, test_value);
393+
}
394+
395+
#[test]
396+
fn test_apply_and_retrieve_xattrs_fd() {
397+
use std::fs::OpenOptions;
398+
399+
let temp_dir = tempdir().unwrap();
400+
let file_path = temp_dir.path().join("test_file.txt");
401+
402+
File::create(&file_path).unwrap();
403+
404+
let mut test_xattrs = HashMap::new();
405+
let test_attr = "user.test_attr_fd";
406+
let test_value = b"test value fd";
407+
test_xattrs.insert(OsString::from(test_attr), test_value.to_vec());
408+
409+
// Apply using file descriptor
410+
let file = OpenOptions::new().write(true).open(&file_path).unwrap();
411+
apply_xattrs_fd(&file, test_xattrs).unwrap();
412+
drop(file);
413+
414+
// Retrieve using file descriptor
415+
let file = File::open(&file_path).unwrap();
416+
let retrieved_xattrs = retrieve_xattrs_fd(&file).unwrap();
417+
assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str()));
418+
assert_eq!(
419+
retrieved_xattrs
420+
.get(OsString::from(test_attr).as_os_str())
421+
.unwrap(),
422+
test_value
423+
);
424+
}
302425
}

0 commit comments

Comments
 (0)