Skip to content

Commit eec25a0

Browse files
committed
cp: parent-perm-race gnu fix
1 parent 23d5235 commit eec25a0

File tree

2 files changed

+96
-11
lines changed

2 files changed

+96
-11
lines changed

src/uu/cp/src/copydir.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
5-
// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked
5+
// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked IRWXO IRWXG
66
//! Recursively copy the contents of a directory.
77
//!
88
//! See the [`copy_directory`] function for more information.
@@ -27,7 +27,7 @@ use walkdir::{DirEntry, WalkDir};
2727

2828
use crate::{
2929
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, CopyResult, Error,
30-
Options,
30+
Options, Preserve,
3131
};
3232

3333
/// Ensure a Windows path starts with a `\\?`.
@@ -246,12 +246,7 @@ fn copy_direntry(
246246
if target_is_file {
247247
return Err("cannot overwrite non-directory with directory".into());
248248
} else {
249-
// TODO Since the calling code is traversing from the root
250-
// of the directory structure, I don't think
251-
// `create_dir_all()` will have any benefit over
252-
// `create_dir()`, since all the ancestor directories
253-
// should have already been created.
254-
fs::create_dir_all(&local_to_target)?;
249+
build_dir(options, &local_to_target, false)?;
255250
if options.verbose {
256251
println!("{}", context_for(&source_relative, &local_to_target));
257252
}
@@ -376,8 +371,7 @@ pub(crate) fn copy_directory(
376371
let tmp = if options.parents {
377372
if let Some(parent) = root.parent() {
378373
let new_target = target.join(parent);
379-
std::fs::create_dir_all(&new_target)?;
380-
374+
build_dir(options, &new_target, true)?;
381375
if options.verbose {
382376
// For example, if copying file `a/b/c` and its parents
383377
// to directory `d/`, then print
@@ -470,6 +464,39 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {
470464
Ok(pathbuf1.starts_with(pathbuf2))
471465
}
472466

467+
/// Builds a directory at the specified path with the given options.
468+
///
469+
/// # Notes
470+
/// - It excludes certain permissions if ownership or special mode bits could
471+
/// potentially change.
472+
/// - The `recursive` flag determines whether parent directories should be created
473+
/// if they do not already exist.
474+
fn build_dir(options: &Options, path: &PathBuf, recursive: bool) -> CopyResult<()> {
475+
let mut builder = fs::DirBuilder::new();
476+
builder.recursive(recursive);
477+
// To prevent unauthorized access before the folder is ready,
478+
// exclude certain permissions if ownership or special mode bits
479+
// could potentially change.
480+
#[cfg(unix)]
481+
{
482+
// we need to allow trivial casts here because some systems like linux have u32 constants in
483+
// in libc while others don't.
484+
#[allow(clippy::unnecessary_cast)]
485+
let mut excluded_perms = if matches!(options.attributes.ownership, Preserve::Yes { .. }) {
486+
libc::S_IRWXG | libc::S_IRWXO // exclude rwx for group and other
487+
} else if matches!(options.attributes.mode, Preserve::Yes { .. }) {
488+
libc::S_IWGRP | libc::S_IWOTH //exclude w for group and other
489+
} else {
490+
0
491+
} as u32;
492+
excluded_perms |= uucore::mode::get_umask();
493+
let mode = !excluded_perms & 0o777; //use only the last three octet bits
494+
std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode);
495+
}
496+
builder.create(path)?;
497+
Ok(())
498+
}
499+
473500
#[cfg(test)]
474501
mod tests {
475502
use super::ends_with_slash_dot;

tests/by-util/test_cp.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55
// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR procfs outfile uufs xattrs
6-
// spell-checker:ignore bdfl hlsl
6+
// spell-checker:ignore bdfl hlsl IRWXO IRWXG
77
use crate::common::util::TestScenario;
88
#[cfg(not(windows))]
99
use std::fs::set_permissions;
@@ -5307,3 +5307,61 @@ mod same_file {
53075307
assert_eq!(at.read(FILE_NAME), CONTENTS,);
53085308
}
53095309
}
5310+
5311+
// The cp command might create directories with excessively permissive permissions temporarily,
5312+
// which could be problematic if we aim to preserve ownership or mode. For example, when
5313+
// copying a directory, the destination directory could temporarily be setgid on some filesystems.
5314+
// This temporary setgid status could grant access to other users who share the same group
5315+
// ownership as the newly created directory.To mitigate this issue, when creating a directory we
5316+
// disable these excessive permissions.
5317+
#[test]
5318+
#[cfg(unix)]
5319+
fn test_dir_perm_race_with_preserve_mode_and_ownership() {
5320+
const SRC_DIR: &str = "src";
5321+
const DEST_DIR: &str = "dest";
5322+
const FIFO: &str = "src/fifo";
5323+
unsafe { libc::umask(0o002) };
5324+
for attr in ["mode", "ownership"] {
5325+
let scene = TestScenario::new(util_name!());
5326+
let at = &scene.fixtures;
5327+
at.mkdir(SRC_DIR);
5328+
at.mkdir(DEST_DIR);
5329+
at.set_mode(DEST_DIR, 2775);
5330+
at.mkfifo(FIFO);
5331+
let child = scene
5332+
.ucmd()
5333+
.args(&[
5334+
format!("--preserve={}", attr).as_str(),
5335+
"-R",
5336+
"--copy-contents",
5337+
"--parents",
5338+
SRC_DIR,
5339+
DEST_DIR,
5340+
])
5341+
.run_no_wait();
5342+
// while cp wait for fifo we could check the dirs created by cp
5343+
let timeout = Duration::from_secs(10);
5344+
let start_time = std::time::Instant::now();
5345+
// wait for cp to create dirs
5346+
loop {
5347+
if start_time.elapsed() >= timeout {
5348+
panic!("timed out: cp took too long to create destination directory")
5349+
}
5350+
if at.dir_exists(&format!("{}/{}", DEST_DIR, SRC_DIR)) {
5351+
break;
5352+
}
5353+
std::thread::sleep(Duration::from_millis(100));
5354+
}
5355+
let mode = at.metadata(&format!("{}/{}", DEST_DIR, SRC_DIR)).mode();
5356+
#[allow(clippy::unnecessary_cast)]
5357+
let mask = if attr == "mode" {
5358+
libc::S_IWGRP | libc::S_IWOTH
5359+
} else {
5360+
libc::S_IRWXG | libc::S_IRWXO
5361+
} as u32;
5362+
assert_eq!((mode & mask), 0, "unwanted permissions are present");
5363+
at.write(FIFO, "done");
5364+
child.wait().unwrap().succeeded();
5365+
}
5366+
unsafe { libc::umask(0o022) };
5367+
}

0 commit comments

Comments
 (0)