Skip to content

Commit b3bcb05

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

3 files changed

Lines changed: 157 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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
use itertools::Itertools;
1010
use rustc_hash::FxHashMap;
1111
use std::ffi::{OsStr, OsString};
12+
use std::fs::File;
1213
#[cfg(unix)]
1314
use std::os::unix::ffi::OsStrExt;
1415
use std::path::Path;
16+
use xattr::FileExt;
1517

1618
/// Copies extended attributes (xattrs) from one file or directory to another.
1719
///
@@ -45,6 +47,29 @@ pub fn copy_xattrs_skip_selinux<P: AsRef<Path>>(source: P, dest: P) -> std::io::
4547
Ok(())
4648
}
4749

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

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

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

0 commit comments

Comments
 (0)