From 6e16f3a891412b7133ad8df4a10fdd266f94a190 Mon Sep 17 00:00:00 2001 From: nagendramohan Date: Tue, 15 Sep 2026 14:09:16 +0530 Subject: [PATCH] cp: apply the umask to directories created by cp -r Without a preserve flag, cp -r gave every directory it created the source's mode unmasked, so a 777 source directory became 777 regardless of the umask, while regular files in the same copy were masked correctly. The final permission pass defaulted a freshly-created directory to preserving the source mode. Apply the umask in that defaulted case, as GNU does, while a real preserve (-p/-a) and --no-preserve=mode are unchanged. Fixes #14549 Signed-off-by: nagendramohan --- src/uu/cp/src/cp.rs | 8 ++++++++ tests/by-util/test_cp.rs | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index f5140f42d9b..68111b45910 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1877,6 +1877,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 { .. }); + // 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 @@ -1941,6 +1946,9 @@ pub(crate) fn copy_attributes( let mode = perms.mode() & !0o6000; perms.set_mode(mode); } + if apply_umask_to_mode { + perms.set_mode(perms.mode() & !uucore::mode::get_umask()); + } perms }; #[cfg(not(unix))] diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 35371d2a75f..d5450f020e7 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -1055,6 +1055,22 @@ 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); +} + // 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