Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,11 @@ pub(crate) fn copy_attributes(
attributes.mode
};

// A created directory only defaults to copying the source mode; unlike an
// explicit preserve (-p/-a), GNU applies the umask to it.
let apply_umask_to_mode =
dest_is_freshly_created_dir && !matches!(attributes.mode, Preserve::Yes { .. });
Comment thread
sylvestre marked this conversation as resolved.

// Track whether `chown` to the source's uid succeeded. If it did not
// (typical case: non-root user copying a root-owned setuid file), the
// mode preservation below must strip setuid/setgid so the destination
Expand Down Expand Up @@ -1956,6 +1961,12 @@ pub(crate) fn copy_attributes(
let mode = perms.mode() & !0o6000;
perms.set_mode(mode);
}
if apply_umask_to_mode {
// The umask never covers setuid/setgid, so clear them
// explicitly: a non-preserving copy must not carry the
// source's set-user/group-ID bits into the new directory.
perms.set_mode(perms.mode() & !0o6000 & !uucore::mode::get_umask());
}
perms
};
#[cfg(not(unix))]
Expand Down
70 changes: 70 additions & 0 deletions tests/by-util/test_cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,76 @@ fn test_cp_umask_stripping_owner_write_bit_reflink_never() {
}
}

// Regression for #14549: `cp -r` (without preserve) must apply the umask to
// directories it creates, matching GNU, instead of copying the source's mode.
#[test]
#[cfg(unix)]
fn test_cp_recursive_dir_applies_umask() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("src");
at.mkdir("src/dir");
at.set_mode("src/dir", 0o777);

ucmd.umask(0o077).args(&["-r", "src", "d"]).succeeds();

// 0o777 & ~0o077 = 0o700, not the source's raw 0o777.
assert_eq!(at.metadata("d/dir").permissions().mode() & 0o777, 0o700);
}

// The umask alone never covers setuid/setgid, so a non-preserving `cp -r`
// must clear them on the directories it creates. The sticky bit survives.
#[test]
#[cfg(unix)]
fn test_cp_recursive_dir_drops_setuid_setgid() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;

at.mkdir("tree");
for (name, mode) in [
("tree/setgid", 0o2731u32),
("tree/setuid", 0o4713),
("tree/sticky", 0o1735),
] {
at.mkdir(name);
at.set_mode(name, mode);
}

scene
.ucmd()
.umask(0o026)
.args(&["-r", "tree", "plain"])
.succeeds();

assert_eq!(
at.metadata("plain/setgid").permissions().mode() & 0o7777,
0o711
);
assert_eq!(
at.metadata("plain/setuid").permissions().mode() & 0o7777,
0o711
);
assert_eq!(
at.metadata("plain/sticky").permissions().mode() & 0o7777,
0o1711
);

// An explicit preserve keeps the mode as-is, umask and special bits alike.
scene
.ucmd()
.umask(0o026)
.args(&["-r", "--preserve=mode", "tree", "kept"])
.succeeds();

assert_eq!(
at.metadata("kept/setgid").permissions().mode() & 0o7777,
0o2731
);
assert_eq!(
at.metadata("kept/setuid").permissions().mode() & 0o7777,
0o4713
);
}

// When --reflink=always fails, GNU cp removes a destination it created
// itself but keeps a pre-existing (truncated) one. Only observable on
// filesystems without clone support; when the clone succeeds there is
Expand Down
Loading