Skip to content

Commit

Permalink
cp: parent-perm-race gnu fix
Browse files Browse the repository at this point in the history
  • Loading branch information
matrixhead committed May 15, 2024
1 parent 23d5235 commit b103d1b
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 11 deletions.
50 changes: 40 additions & 10 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked
// spell-checker:ignore TODO canonicalizes direntry pathbuf symlinked IRWXO IRWXG
//! Recursively copy the contents of a directory.
//!
//! See the [`copy_directory`] function for more information.
Expand All @@ -27,7 +27,7 @@ use walkdir::{DirEntry, WalkDir};

use crate::{
aligned_ancestors, context_for, copy_attributes, copy_file, copy_link, CopyResult, Error,
Options,
Options, Preserve,
};

/// Ensure a Windows path starts with a `\\?`.
Expand Down Expand Up @@ -246,12 +246,7 @@ fn copy_direntry(
if target_is_file {
return Err("cannot overwrite non-directory with directory".into());
} else {
// TODO Since the calling code is traversing from the root
// of the directory structure, I don't think
// `create_dir_all()` will have any benefit over
// `create_dir()`, since all the ancestor directories
// should have already been created.
fs::create_dir_all(&local_to_target)?;
build_dir(options, &local_to_target, false)?;
if options.verbose {
println!("{}", context_for(&source_relative, &local_to_target));
}
Expand Down Expand Up @@ -376,8 +371,7 @@ pub(crate) fn copy_directory(
let tmp = if options.parents {
if let Some(parent) = root.parent() {
let new_target = target.join(parent);
std::fs::create_dir_all(&new_target)?;

build_dir(options, &new_target, true)?;
if options.verbose {
// For example, if copying file `a/b/c` and its parents
// to directory `d/`, then print
Expand Down Expand Up @@ -470,6 +464,42 @@ pub fn path_has_prefix(p1: &Path, p2: &Path) -> io::Result<bool> {
Ok(pathbuf1.starts_with(pathbuf2))
}

/// Builds a directory at the specified path with the given options.
///
/// # Notes
/// - It excludes certain permissions if ownership or special mode bits could
/// potentially change.
/// - The `recursive` flag determines whether parent directories should be created
/// if they do not already exist.
fn build_dir(options: &Options, path: &PathBuf, recursive: bool) -> CopyResult<()> {
let mut builder = fs::DirBuilder::new();
builder.recursive(recursive);
#[cfg(unix)]
{
// To prevent unauthorized access before the folder is ready,
// exclude certain permissions if ownership or special mode bits
// could potentially change.
let mut excluded_perms = 0;
if matches!(options.attributes.ownership, Preserve::Yes { .. }) {
excluded_perms = libc::S_IRWXG | libc::S_IRWXO; // exclude rwx for group and other
} else if matches!(options.attributes.mode, Preserve::Yes { .. }) {
excluded_perms = libc::S_IWGRP | libc::S_IWOTH; //exclude w for group and other
}
excluded_perms |= uucore::mode::get_umask();
let mode = !excluded_perms & 0o777; //use only the last three octets
#[cfg(not(any(target_os = "linux",target_os = "macos")))]
{
std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode as u32);
}
#[cfg(any(target_os = "linux",target_os = "macos"))]
{
std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode);
}
}
builder.create(path)?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::ends_with_slash_dot;
Expand Down
59 changes: 58 additions & 1 deletion tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR procfs outfile uufs xattrs
// spell-checker:ignore bdfl hlsl
// spell-checker:ignore bdfl hlsl IRWXO IRWXG
use crate::common::util::TestScenario;
#[cfg(not(windows))]
use std::fs::set_permissions;
Expand Down Expand Up @@ -5307,3 +5307,60 @@ mod same_file {
assert_eq!(at.read(FILE_NAME), CONTENTS,);
}
}

// The cp command might create directories with excessively permissive permissions temporarily,
// which could be problematic if we aim to preserve ownership or mode. For example, when
// copying a directory, the destination directory could temporarily be setgid on some filesystems.
// This temporary setgid status could grant access to other users who share the same group
// ownership as the newly created directory.To mitigate this issue, when creating a directory we
// disable these excessive permissions.
#[test]
#[cfg(unix)]
fn test_dir_perm_race_with_preserve_mode_and_ownership() {
const SRC_DIR: &str = "src";
const DEST_DIR: &str = "dest";
const FIFO: &str = "src/fifo";
unsafe { libc::umask(0o002) };
for attr in ["mode", "ownership"] {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.mkdir(SRC_DIR);
at.mkdir(DEST_DIR);
at.set_mode(DEST_DIR, 2775);
at.mkfifo(FIFO);
let child = scene
.ucmd()
.args(&[
format!("--preserve={}", attr).as_str(),
"-R",
"--copy-contents",
"--parents",
SRC_DIR,
DEST_DIR,
])
.run_no_wait();
// while cp wait for fifo we could check the dirs created by cp
let timeout = Duration::from_secs(10);
let start_time = std::time::Instant::now();
// wait for cp to create dirs
loop {
if start_time.elapsed() >= timeout {
panic!("timed out: cp took too long to create destination directory")
}
if at.dir_exists(&format!("{}/{}", DEST_DIR, SRC_DIR)) {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
let mode = at.metadata(&format!("{}/{}", DEST_DIR, SRC_DIR)).mode();
let mask = if attr == "mode" {
libc::S_IWGRP | libc::S_IWOTH
} else {
libc::S_IRWXG | libc::S_IRWXO
};
assert_eq!((mode & mask), 0, "unwanted permissions are present");
at.write(FIFO, "done");
child.wait().unwrap().succeeded();
}
unsafe { libc::umask(0o022) };
}

0 comments on commit b103d1b

Please sign in to comment.