From 0e29cca8954d054c4b26e8bcbe54e3c590eab44b Mon Sep 17 00:00:00 2001 From: burak yorulmaz Date: Mon, 22 Dec 2025 18:03:17 +0100 Subject: [PATCH] cp: strip setuid & setgid when preserving ownership or group fails --- src/uu/cp/src/cp.rs | 18 +++++++- tests/by-util/test_cp.rs | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index c1df9ed139a..f06748b482c 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -1708,7 +1708,23 @@ pub(crate) fn copy_attributes( // do nothing, since every symbolic link has the same // permissions. if !dest.is_symlink() { - fs::set_permissions(dest, source_metadata.permissions()) + // gnu compatibility: cp strips both setuid & setgid bits if preserving either ownership or group fails + let mut perms = source_metadata.permissions(); + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + + let dest_metadata = fs::symlink_metadata(dest) + .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; + + if dest_metadata.uid() != source_metadata.uid() + || dest_metadata.gid() != source_metadata.gid() + { + let mode = perms.mode() & !0o6000; + perms.set_mode(mode); + } + } + fs::set_permissions(dest, perms) .map_err(|e| CpError::IoErrContext(e, context.to_owned()))?; // FIXME: Implement this for windows as well #[cfg(feature = "feat_acl")] diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 2563e533ae4..04a9799b6a9 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7400,3 +7400,98 @@ fn test_cp_recurse_verbose_output_with_symlink_already_exists() { .no_stderr() .stdout_is(output); } + +#[test] +#[cfg(unix)] +fn test_cp_strip_uid_gid_preserve_ownership_fails() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let ucmd = &mut scene.ucmd(); + + let src = "src"; + let dest_dir = "dir"; + let dest = "dir/dest"; + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout_str() != "root\n" { + return; + } + + at.touch(src); + at.set_mode(src, 0o6755); + + at.mkdir(dest_dir); + at.set_mode(dest_dir, 0o777); + + // Need to drop privileges for preserving ownership to fail + unsafe { + libc::seteuid(1000); + } + + ucmd.arg("-p").arg(src).arg(dest).succeeds(); + + // When preserving ownership fails, setuid and setgid bits should be stripped + let dest_metadata = at.metadata(dest); + assert_eq!( + dest_metadata.mode() & 0o6755, + 0o755, + "setuid or setgid not stripped" + ); +} + +#[test] +#[cfg(unix)] +fn test_cp_strip_uid_gid_preserve_group_fails() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let ucmd = &mut scene.ucmd(); + + let dir = "dir"; + let src = "dir/src"; + let dest = "dir/dest"; + + // Test must be run as root (or with `sudo -E`) + if scene.cmd("whoami").run().stdout_str() != "root\n" { + return; + } + + at.mkdir(dir); + at.set_mode(dir, 0o777); + + // Drop privileges before creating file + // Will need root to set group later + let orig_euid = unsafe { libc::geteuid() }; + let orig_egid = unsafe { libc::getegid() }; + + unsafe { + libc::seteuid(1000); + libc::setegid(1000); + } + + at.touch(src); + at.set_mode(src, 0o6755); + + // Escalate privileges to set group + unsafe { + libc::seteuid(orig_euid); + libc::setegid(orig_egid); + } + + scene.cmd("chgrp").arg("0").arg(src).succeeds(); + + // Drop again before running cp + unsafe { + libc::seteuid(1000); + libc::setegid(1000); + } + + ucmd.arg("-p").arg(src).arg(dest).succeeds(); + + // When preserving group fails, setuid and setgid bits should be stripped + let dest_metadata = at.metadata(dest); + assert_eq!( + dest_metadata.mode() & 0o6755, + 0o755, + "setuid or setgid not stripped" + ); +}