Skip to content

Commit 0211168

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

8 files changed

Lines changed: 485 additions & 68 deletions

File tree

src/uu/cp/src/cp.rs

Lines changed: 49 additions & 8 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, 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};
@@ -1715,6 +1715,8 @@ pub(crate) fn set_selinux_context(path: &Path, context: Option<&String>) -> Copy
17151715
/// or if xattr copying fails.
17161716
#[cfg(all(unix, not(target_os = "android")))]
17171717
fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyResult<()> {
1718+
use std::fs::File;
1719+
use uucore::fsxattr::copy_xattrs;
17181720
let metadata = fs::symlink_metadata(dest)?;
17191721

17201722
// Check if the destination file is currently read-only for the user.
@@ -1734,6 +1736,13 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe
17341736
// When -Z is used, skip copying security.selinux xattr so that
17351737
// the default context can be set instead of preserving from source
17361738
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)
17371746
} else {
17381747
copy_xattrs(source, dest)
17391748
};
@@ -2591,7 +2600,45 @@ fn copy_file(
25912600
fs::set_permissions(dest, dest_permissions).ok();
25922601
}
25932602

2594-
let copy_attributes_result = if options.dereference(source_in_command_line) {
2603+
let copy_attributes_result = if source_is_stream && options.copy_contents {
2604+
// When copying contents from a stream (like a FIFO with --copy-contents),
2605+
// we can't re-access the source as it may block. Use the metadata we
2606+
// already have to preserve ownership and permissions.
2607+
#[cfg(unix)]
2608+
{
2609+
if let Preserve::Yes { .. } = options.attributes.ownership {
2610+
use std::os::unix::prelude::MetadataExt;
2611+
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
2612+
2613+
let dest_uid = source_metadata.uid();
2614+
let dest_gid = source_metadata.gid();
2615+
if let Ok(dest_meta) = dest.symlink_metadata() {
2616+
let try_chown = |uid| {
2617+
wrap_chown(
2618+
dest,
2619+
&dest_meta,
2620+
uid,
2621+
Some(dest_gid),
2622+
false,
2623+
Verbosity {
2624+
groups_only: false,
2625+
level: VerbosityLevel::Silent,
2626+
},
2627+
)
2628+
};
2629+
// gnu compatibility: cp doesn't report an error if it fails to set the
2630+
// ownership, and will fall back to changing only the gid if possible.
2631+
if try_chown(Some(dest_uid)).is_err() {
2632+
let _ = try_chown(None);
2633+
}
2634+
}
2635+
}
2636+
if let Preserve::Yes { .. } = options.attributes.mode {
2637+
fs::set_permissions(dest, source_metadata.permissions()).ok();
2638+
}
2639+
}
2640+
Ok(())
2641+
} else if options.dereference(source_in_command_line) {
25952642
// Try to canonicalize, but if it fails (e.g., due to inaccessible parent directories),
25962643
// fall back to the original source path
25972644
let src_for_attrs = canonicalize(source, MissingHandling::Normal, ResolveMode::Physical)
@@ -2605,12 +2652,6 @@ fn copy_file(
26052652
false,
26062653
options.set_selinux_context,
26072654
)
2608-
} else if source_is_stream && !source.exists() {
2609-
// Some stream files may not exist after we have copied it,
2610-
// like anonymous pipes. Thus, we can't really copy its
2611-
// attributes. However, this is already handled in the stream
2612-
// copy function (see `copy_stream` under platform/linux.rs).
2613-
Ok(())
26142655
} else {
26152656
copy_attributes(
26162657
source,

src/uu/cp/src/platform/linux.rs

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66

77
use libc::{SEEK_DATA, SEEK_HOLE};
88
use std::fs::{File, OpenOptions};
9-
use std::io::Read;
9+
use std::io::{self, Read};
1010
use std::os::unix::fs::FileExt;
1111
use std::os::unix::fs::MetadataExt;
1212
use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};
1313
use std::os::unix::io::AsRawFd;
1414
use std::path::Path;
1515
use uucore::buf_copy;
16-
use uucore::mode::get_umask;
1716
use uucore::translate;
1817

1918
use crate::{
@@ -50,25 +49,64 @@ enum CopyMethod {
5049
SparseCopyWithoutHole,
5150
}
5251

52+
/// Copy file contents with restrictive permissions initially to prevent race conditions.
53+
/// Creates the destination file with mode 0600 & !umask, copies the content,
54+
/// and lets the caller set the final permissions.
55+
fn copy_file_with_secure_permissions<P>(source: P, dest: P) -> io::Result<u64>
56+
where
57+
P: AsRef<Path>,
58+
{
59+
let mut src_file = File::open(&source)?;
60+
// Create destination with restrictive permissions initially (to prevent race conditions)
61+
// Mode 0o600 means read/write for owner only
62+
let mut dst_file = OpenOptions::new()
63+
.create(true)
64+
.write(true)
65+
.truncate(true)
66+
.mode(0o600)
67+
.open(&dest)?;
68+
io::copy(&mut src_file, &mut dst_file)
69+
}
70+
5371
/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
5472
///
5573
/// `fallback` controls what to do if the system call fails.
5674
#[cfg(any(target_os = "linux", target_os = "android"))]
57-
fn clone<P>(source: P, dest: P, fallback: CloneFallback) -> std::io::Result<()>
75+
fn clone<P>(source: P, dest: P, fallback: CloneFallback) -> io::Result<()>
5876
where
5977
P: AsRef<Path>,
6078
{
6179
let src_file = File::open(&source)?;
62-
let dst_file = File::create(&dest)?;
80+
// Create destination with restrictive permissions initially (to prevent race conditions)
81+
// Mode 0o600 means read/write for owner only
82+
let dst_file = OpenOptions::new()
83+
.create(true)
84+
.write(true)
85+
.truncate(false)
86+
.mode(0o600)
87+
.open(&dest)?;
6388
let src_fd = src_file.as_raw_fd();
6489
let dst_fd = dst_file.as_raw_fd();
6590
let result = unsafe { libc::ioctl(dst_fd, libc::FICLONE, src_fd) };
6691
if result == 0 {
6792
return Ok(());
6893
}
94+
let err = io::Error::last_os_error();
6995
match fallback {
70-
CloneFallback::Error => Err(std::io::Error::last_os_error()),
71-
CloneFallback::FSCopy => std::fs::copy(source, dest).map(|_| ()),
96+
CloneFallback::Error => {
97+
// FICLONE fails with EINVAL if the destination is not empty.
98+
// Truncate and retry once before giving up.
99+
if err.raw_os_error() == Some(libc::EINVAL) {
100+
dst_file.set_len(0)?;
101+
let retry = unsafe { libc::ioctl(dst_fd, libc::FICLONE, src_fd) };
102+
if retry == 0 {
103+
return Ok(());
104+
}
105+
return Err(io::Error::last_os_error());
106+
}
107+
Err(err)
108+
}
109+
CloneFallback::FSCopy => copy_file_with_secure_permissions(source, dest).map(|_| ()),
72110
CloneFallback::SparseCopy => sparse_copy(source, dest),
73111
CloneFallback::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest),
74112
}
@@ -78,7 +116,7 @@ where
78116
/// This function returns a tuple of (bool, u64, u64) signifying a tuple of (whether a file has
79117
/// data, its size, no of blocks it has allocated in disk)
80118
#[cfg(any(target_os = "linux", target_os = "android"))]
81-
fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> {
119+
fn check_for_data(source: &Path) -> Result<(bool, u64, u64), io::Error> {
82120
let mut src_file = File::open(source)?;
83121
let metadata = src_file.metadata()?;
84122

@@ -98,14 +136,14 @@ fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> {
98136
match result {
99137
-1 => Ok((false, size, blocks)), // No data found or end of file
100138
_ if result >= 0 => Ok((true, size, blocks)), // Data found
101-
_ => Err(std::io::Error::last_os_error()),
139+
_ => Err(io::Error::last_os_error()),
102140
}
103141
}
104142

105143
#[cfg(any(target_os = "linux", target_os = "android"))]
106144
/// Checks whether a file is sparse i.e. it contains holes, uses the crude heuristic blocks < size / 512
107145
/// Reference:`<https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.blocks>`
108-
fn check_sparse_detection(source: &Path) -> Result<bool, std::io::Error> {
146+
fn check_sparse_detection(source: &Path) -> Result<bool, io::Error> {
109147
let src_file = File::open(source)?;
110148
let metadata = src_file.metadata()?;
111149
let size = metadata.size();
@@ -120,17 +158,24 @@ fn check_sparse_detection(source: &Path) -> Result<bool, std::io::Error> {
120158
/// Optimized [`sparse_copy`] doesn't create holes for large sequences of zeros in non `sparse_files`
121159
/// Used when `--sparse=auto`
122160
#[cfg(any(target_os = "linux", target_os = "android"))]
123-
fn sparse_copy_without_hole<P>(source: P, dest: P) -> std::io::Result<()>
161+
fn sparse_copy_without_hole<P>(source: P, dest: P) -> io::Result<()>
124162
where
125163
P: AsRef<Path>,
126164
{
127165
let src_file = File::open(source)?;
128-
let dst_file = File::create(dest)?;
166+
// Create destination with restrictive permissions initially (to prevent race conditions)
167+
// Mode 0o600 means read/write for owner only
168+
let dst_file = OpenOptions::new()
169+
.create(true)
170+
.write(true)
171+
.truncate(true)
172+
.mode(0o600)
173+
.open(dest)?;
129174
let dst_fd = dst_file.as_raw_fd();
130175

131176
let size = src_file.metadata()?.size();
132177
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
133-
return Err(std::io::Error::last_os_error());
178+
return Err(io::Error::last_os_error());
134179
}
135180
let src_fd = src_file.as_raw_fd();
136181
let mut current_offset: isize = 0;
@@ -152,7 +197,7 @@ where
152197
break;
153198
}
154199
if result <= -2 || hole <= -2 {
155-
return Err(std::io::Error::last_os_error());
200+
return Err(io::Error::last_os_error());
156201
}
157202
let len: isize = hole - current_offset;
158203
// Read and write data in chunks of `step` while reusing the same buffer
@@ -170,17 +215,24 @@ where
170215
/// Perform a sparse copy from one file to another.
171216
/// Creates a holes for large sequences of zeros in `non_sparse_files`, used for `--sparse=always`
172217
#[cfg(any(target_os = "linux", target_os = "android"))]
173-
fn sparse_copy<P>(source: P, dest: P) -> std::io::Result<()>
218+
fn sparse_copy<P>(source: P, dest: P) -> io::Result<()>
174219
where
175220
P: AsRef<Path>,
176221
{
177222
let mut src_file = File::open(source)?;
178-
let dst_file = File::create(dest)?;
223+
// Create destination with restrictive permissions initially (to prevent race conditions)
224+
// Mode 0o600 means read/write for owner only
225+
let dst_file = OpenOptions::new()
226+
.create(true)
227+
.write(true)
228+
.truncate(true)
229+
.mode(0o600)
230+
.open(dest)?;
179231
let dst_fd = dst_file.as_raw_fd();
180232

181233
let size: usize = src_file.metadata()?.size().try_into().unwrap();
182234
if unsafe { libc::ftruncate(dst_fd, size.try_into().unwrap()) } < 0 {
183-
return Err(std::io::Error::last_os_error());
235+
return Err(io::Error::last_os_error());
184236
}
185237

186238
let blksize = dst_file.metadata()?.blksize();
@@ -214,7 +266,7 @@ fn check_dest_is_fifo(dest: &Path) -> bool {
214266
}
215267

216268
/// Copy the contents of a stream from `source` to `dest`.
217-
fn copy_stream<P>(source: P, dest: P) -> std::io::Result<u64>
269+
fn copy_stream<P>(source: P, dest: P) -> io::Result<u64>
218270
where
219271
P: AsRef<Path>,
220272
{
@@ -237,21 +289,21 @@ where
237289
// TODO Update the code below to respect the case where
238290
// `--preserve=ownership` is not true.
239291
let mut src_file = File::open(&source)?;
240-
let mode = 0o622 & !get_umask();
292+
// Create with restrictive permissions initially to prevent race conditions
293+
// Mode 0o600 means read/write for owner only
294+
// Use truncate(true) to ensure we overwrite existing content
241295
let mut dst_file = OpenOptions::new()
242296
.create(true)
243297
.write(true)
244-
.mode(mode)
298+
.truncate(true)
299+
.mode(0o600)
245300
.open(&dest)?;
246301

247-
let dest_is_stream = is_stream(&dst_file.metadata()?);
248-
if !dest_is_stream {
249-
// `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually.
250-
dst_file.set_len(0)?;
251-
}
302+
let _dest_is_stream = is_stream(&dst_file.metadata()?);
303+
// No need to set_len(0) since we're already using truncate(true)
252304

253305
let num_bytes_copied = buf_copy::copy_stream(&mut src_file, &mut dst_file)
254-
.map_err(|e| std::io::Error::other(format!("{e}")))?;
306+
.map_err(|e| io::Error::other(format!("{e}")))?;
255307

256308
Ok(num_bytes_copied)
257309
}
@@ -287,7 +339,9 @@ pub(crate) fn copy_on_write(
287339
}
288340

289341
match copy_method {
290-
CopyMethod::FSCopy => std::fs::copy(source, dest).map(|_| ()),
342+
CopyMethod::FSCopy => {
343+
copy_file_with_secure_permissions(source, dest).map(|_| ())
344+
}
291345
_ => sparse_copy(source, dest),
292346
}
293347
}
@@ -303,7 +357,7 @@ pub(crate) fn copy_on_write(
303357
if let Ok(debug) = result {
304358
copy_debug = debug;
305359
}
306-
std::fs::copy(source, dest).map(|_| ())
360+
copy_file_with_secure_permissions(source, dest).map(|_| ())
307361
}
308362
}
309363
(ReflinkMode::Never, SparseMode::Auto) => {
@@ -322,7 +376,7 @@ pub(crate) fn copy_on_write(
322376

323377
match copy_method {
324378
CopyMethod::SparseCopyWithoutHole => sparse_copy_without_hole(source, dest),
325-
_ => std::fs::copy(source, dest).map(|_| ()),
379+
_ => copy_file_with_secure_permissions(source, dest).map(|_| ()),
326380
}
327381
}
328382
}
@@ -401,7 +455,7 @@ pub(crate) fn copy_on_write(
401455
fn handle_reflink_auto_sparse_always(
402456
source: &Path,
403457
dest: &Path,
404-
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
458+
) -> Result<(CopyDebug, CopyMethod), io::Error> {
405459
let mut copy_debug = CopyDebug {
406460
offload: OffloadReflinkDebug::Unknown,
407461
reflink: OffloadReflinkDebug::Unsupported,
@@ -438,7 +492,7 @@ fn handle_reflink_auto_sparse_always(
438492

439493
/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what
440494
/// type of copy should be used
441-
fn handle_reflink_never_sparse_never(source: &Path) -> Result<CopyDebug, std::io::Error> {
495+
fn handle_reflink_never_sparse_never(source: &Path) -> Result<CopyDebug, io::Error> {
442496
let mut copy_debug = CopyDebug {
443497
offload: OffloadReflinkDebug::Unknown,
444498
reflink: OffloadReflinkDebug::No,
@@ -459,7 +513,7 @@ fn handle_reflink_never_sparse_never(source: &Path) -> Result<CopyDebug, std::io
459513

460514
/// Handles debug results when flags are "--reflink=auto" and "--sparse=never", files will be copied
461515
/// through cloning them with fallback switching to [`std::fs::copy`]
462-
fn handle_reflink_auto_sparse_never(source: &Path) -> Result<CopyDebug, std::io::Error> {
516+
fn handle_reflink_auto_sparse_never(source: &Path) -> Result<CopyDebug, io::Error> {
463517
let mut copy_debug = CopyDebug {
464518
offload: OffloadReflinkDebug::Unknown,
465519
reflink: OffloadReflinkDebug::No,
@@ -484,7 +538,7 @@ fn handle_reflink_auto_sparse_never(source: &Path) -> Result<CopyDebug, std::io:
484538
fn handle_reflink_auto_sparse_auto(
485539
source: &Path,
486540
dest: &Path,
487-
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
541+
) -> Result<(CopyDebug, CopyMethod), io::Error> {
488542
let mut copy_debug = CopyDebug {
489543
offload: OffloadReflinkDebug::Unknown,
490544
reflink: OffloadReflinkDebug::Unsupported,
@@ -527,7 +581,7 @@ fn handle_reflink_auto_sparse_auto(
527581
fn handle_reflink_never_sparse_auto(
528582
source: &Path,
529583
dest: &Path,
530-
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
584+
) -> Result<(CopyDebug, CopyMethod), io::Error> {
531585
let mut copy_debug = CopyDebug {
532586
offload: OffloadReflinkDebug::Unknown,
533587
reflink: OffloadReflinkDebug::No,
@@ -563,7 +617,7 @@ fn handle_reflink_never_sparse_auto(
563617
fn handle_reflink_never_sparse_always(
564618
source: &Path,
565619
dest: &Path,
566-
) -> Result<(CopyDebug, CopyMethod), std::io::Error> {
620+
) -> Result<(CopyDebug, CopyMethod), io::Error> {
567621
let mut copy_debug = CopyDebug {
568622
offload: OffloadReflinkDebug::Unknown,
569623
reflink: OffloadReflinkDebug::No,

0 commit comments

Comments
 (0)