Skip to content

Commit e3b063d

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

3 files changed

Lines changed: 163 additions & 8 deletions

File tree

src/uu/cp/src/cp.rs

Lines changed: 18 additions & 4 deletions
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;
19+
use uucore::fsxattr::copy_xattrs_fd;
2020
use uucore::translate;
2121

2222
use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser};
@@ -1675,8 +1675,13 @@ fn handle_preserve<F: Fn() -> CopyResult<()>>(p: &Preserve, f: F) -> CopyResult<
16751675
/// user-writable if needed and restoring its original permissions afterward. This avoids "Operation
16761676
/// not permitted" errors on read-only files. Returns an error if permission or metadata operations fail,
16771677
/// or if xattr copying fails.
1678+
///
1679+
/// Uses file descriptor-based operations to avoid TOCTOU races during xattr copying.
16781680
#[cfg(all(unix, not(target_os = "android")))]
16791681
fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
1682+
use std::fs::File;
1683+
use uucore::fsxattr::copy_xattrs;
1684+
16801685
let metadata = fs::symlink_metadata(dest)?;
16811686

16821687
// Check if the destination file is currently read-only for the user.
@@ -1690,9 +1695,18 @@ fn copy_extended_attrs(source: &Path, dest: &Path) -> CopyResult<()> {
16901695
fs::set_permissions(dest, perms)?;
16911696
}
16921697

1693-
// Perform the xattr copy and capture any potential error,
1694-
// so we can restore permissions before returning.
1695-
let copy_xattrs_result = copy_xattrs(source, dest);
1698+
// Use file descriptor-based operations for regular files to avoid TOCTOU races.
1699+
// For directories and symlinks, we must use path-based operations since:
1700+
// - Directories cannot be opened with write mode for xattr operations
1701+
// - Symlinks (especially dangling ones) cannot be opened via File::open
1702+
let copy_xattrs_result = if metadata.is_file() {
1703+
// Open both files to pin their inodes, avoiding TOCTOU races during xattr operations
1704+
let source_file = File::open(source)?;
1705+
let dest_file = OpenOptions::new().write(true).open(dest)?;
1706+
copy_xattrs_fd(&source_file, &dest_file)
1707+
} else {
1708+
copy_xattrs(source, dest)
1709+
};
16961710

16971711
// Restore read-only if we changed it.
16981712
if was_readonly {

src/uu/mv/src/mv.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -975,8 +975,14 @@ fn rename_dir_fallback(
975975
(_, _) => None,
976976
};
977977

978+
// Retrieve xattrs using file descriptor to avoid TOCTOU races
978979
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
979-
let xattrs = fsxattr::retrieve_xattrs(from).unwrap_or_else(|_| HashMap::new());
980+
let xattrs = {
981+
use std::fs::File;
982+
File::open(from)
983+
.and_then(|f| fsxattr::retrieve_xattrs_fd(&f))
984+
.unwrap_or_else(|_| HashMap::new())
985+
};
980986

981987
// Use directory copying (with or without hardlink support)
982988
let result = copy_dir_contents(
@@ -991,8 +997,14 @@ fn rename_dir_fallback(
991997
display_manager,
992998
);
993999

1000+
// Apply xattrs using file descriptor to avoid TOCTOU races
9941001
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
995-
fsxattr::apply_xattrs(to, xattrs)?;
1002+
{
1003+
use std::fs::OpenOptions;
1004+
if let Ok(f) = OpenOptions::new().write(true).open(to) {
1005+
fsxattr::apply_xattrs_fd(&f, xattrs)?;
1006+
}
1007+
}
9961008

9971009
result?;
9981010

@@ -1212,12 +1224,19 @@ fn rename_file_fallback(
12121224
Ok(())
12131225
}
12141226

1215-
/// Copy xattrs from source to destination, ignoring ENOTSUP/EOPNOTSUPP errors.
1227+
/// Copy xattrs from source to destination using file descriptors, ignoring ENOTSUP/EOPNOTSUPP errors.
1228+
/// This version avoids TOCTOU races by operating on file descriptors rather than paths.
12161229
/// These errors indicate the filesystem doesn't support extended attributes,
12171230
/// which is acceptable when moving files across filesystems.
12181231
#[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))]
12191232
fn copy_xattrs_if_supported(from: &Path, to: &Path) -> io::Result<()> {
1220-
match fsxattr::copy_xattrs(from, to) {
1233+
use std::fs::{File, OpenOptions};
1234+
1235+
// Open both files to pin their inodes, avoiding TOCTOU races during xattr operations
1236+
let source_file = File::open(from)?;
1237+
let dest_file = OpenOptions::new().write(true).open(to)?;
1238+
1239+
match fsxattr::copy_xattrs_fd(&source_file, &dest_file) {
12211240
Ok(()) => Ok(()),
12221241
Err(e) if e.raw_os_error() == Some(libc::EOPNOTSUPP) => Ok(()),
12231242
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 std::collections::HashMap;
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
///
@@ -32,6 +34,29 @@ pub fn copy_xattrs<P: AsRef<Path>>(source: P, dest: P) -> std::io::Result<()> {
3234
Ok(())
3335
}
3436

37+
/// Copies extended attributes (xattrs) from one file to another using file descriptors.
38+
///
39+
/// This version avoids TOCTOU (time-of-check to time-of-use) races by operating
40+
/// on open file descriptors rather than paths, ensuring all operations target
41+
/// the same inodes throughout.
42+
///
43+
/// # Arguments
44+
///
45+
/// * `source` - A reference to the source file (open file descriptor).
46+
/// * `dest` - A reference to the destination file (open file descriptor).
47+
///
48+
/// # Returns
49+
///
50+
/// A result indicating success or failure.
51+
pub fn copy_xattrs_fd(source: &File, dest: &File) -> std::io::Result<()> {
52+
for attr_name in source.list_xattr()? {
53+
if let Some(value) = source.get_xattr(&attr_name)? {
54+
dest.set_xattr(&attr_name, &value)?;
55+
}
56+
}
57+
Ok(())
58+
}
59+
3560
/// Retrieves the extended attributes (xattrs) of a given file or directory.
3661
///
3762
/// # Arguments
@@ -51,6 +76,28 @@ pub fn retrieve_xattrs<P: AsRef<Path>>(source: P) -> std::io::Result<HashMap<OsS
5176
Ok(attrs)
5277
}
5378

79+
/// Retrieves the extended attributes (xattrs) of a given file using a file descriptor.
80+
///
81+
/// This version avoids TOCTOU races by operating on an open file descriptor
82+
/// rather than a path, ensuring all operations target the same inode.
83+
///
84+
/// # Arguments
85+
///
86+
/// * `source` - A reference to the file (open file descriptor).
87+
///
88+
/// # Returns
89+
///
90+
/// A result containing a HashMap of attribute names and values, or an error.
91+
pub fn retrieve_xattrs_fd(source: &File) -> std::io::Result<HashMap<OsString, Vec<u8>>> {
92+
let mut attrs = HashMap::new();
93+
for attr_name in source.list_xattr()? {
94+
if let Some(value) = source.get_xattr(&attr_name)? {
95+
attrs.insert(attr_name, value);
96+
}
97+
}
98+
Ok(attrs)
99+
}
100+
54101
/// Applies extended attributes (xattrs) to a given file or directory.
55102
///
56103
/// # Arguments
@@ -71,6 +118,26 @@ pub fn apply_xattrs<P: AsRef<Path>>(
71118
Ok(())
72119
}
73120

121+
/// Applies extended attributes (xattrs) to a given file using a file descriptor.
122+
///
123+
/// This version avoids TOCTOU races by operating on an open file descriptor
124+
/// rather than a path, ensuring all operations target the same inode.
125+
///
126+
/// # Arguments
127+
///
128+
/// * `dest` - A reference to the file (open file descriptor).
129+
/// * `xattrs` - A HashMap containing attribute names and their corresponding values.
130+
///
131+
/// # Returns
132+
///
133+
/// A result indicating success or failure.
134+
pub fn apply_xattrs_fd(dest: &File, xattrs: HashMap<OsString, Vec<u8>>) -> std::io::Result<()> {
135+
for (attr, value) in xattrs {
136+
dest.set_xattr(&attr, &value)?;
137+
}
138+
Ok(())
139+
}
140+
74141
/// Checks if a file has an Access Control List (ACL) based on its extended attributes.
75142
///
76143
/// # Arguments
@@ -286,4 +353,59 @@ mod tests {
286353
assert!(has_security_cap_acl(&file_path));
287354
}
288355
}
356+
357+
#[test]
358+
fn test_copy_xattrs_fd() {
359+
use std::fs::OpenOptions;
360+
361+
let temp_dir = tempdir().unwrap();
362+
let source_path = temp_dir.path().join("source.txt");
363+
let dest_path = temp_dir.path().join("dest.txt");
364+
365+
File::create(&source_path).unwrap();
366+
File::create(&dest_path).unwrap();
367+
368+
let test_attr = "user.test_fd";
369+
let test_value = b"test value fd";
370+
xattr::set(&source_path, test_attr, test_value).unwrap();
371+
372+
let source_file = File::open(&source_path).unwrap();
373+
let dest_file = OpenOptions::new().write(true).open(&dest_path).unwrap();
374+
375+
copy_xattrs_fd(&source_file, &dest_file).unwrap();
376+
377+
let copied_value = xattr::get(&dest_path, test_attr).unwrap().unwrap();
378+
assert_eq!(copied_value, test_value);
379+
}
380+
381+
#[test]
382+
fn test_apply_and_retrieve_xattrs_fd() {
383+
use std::fs::OpenOptions;
384+
385+
let temp_dir = tempdir().unwrap();
386+
let file_path = temp_dir.path().join("test_file.txt");
387+
388+
File::create(&file_path).unwrap();
389+
390+
let mut test_xattrs = HashMap::new();
391+
let test_attr = "user.test_attr_fd";
392+
let test_value = b"test value fd";
393+
test_xattrs.insert(OsString::from(test_attr), test_value.to_vec());
394+
395+
// Apply using file descriptor
396+
let file = OpenOptions::new().write(true).open(&file_path).unwrap();
397+
apply_xattrs_fd(&file, test_xattrs).unwrap();
398+
drop(file);
399+
400+
// Retrieve using file descriptor
401+
let file = File::open(&file_path).unwrap();
402+
let retrieved_xattrs = retrieve_xattrs_fd(&file).unwrap();
403+
assert!(retrieved_xattrs.contains_key(OsString::from(test_attr).as_os_str()));
404+
assert_eq!(
405+
retrieved_xattrs
406+
.get(OsString::from(test_attr).as_os_str())
407+
.unwrap(),
408+
test_value
409+
);
410+
}
289411
}

0 commit comments

Comments
 (0)