Skip to content
Open
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
25 changes: 17 additions & 8 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,24 +541,31 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
}
}

if mode::chmod(path, b.mode()).is_err() {
// Error messages are printed by the mode::chmod function!
// Ownership before mode; on failure skip the chmod.
// See set_ownership_and_permissions for why.
if b.privileged
&& let Err(e) = chown_optional_user_group(path, b)
{
show!(e);
uucore::error::set_exit_code(1);
continue;
}

// Set SELinux context for directory if needed
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
if b.privileged {
show_if_err!(chown_optional_user_group(path, b));

// Set SELinux context for directory if needed
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
if b.default_context {
show_if_err!(set_selinux_default_context(path));
} else if b.context.is_some() {
let context = get_context_for_selinux(b);
show_if_err!(set_selinux_security_context(path, context));
}
}

if mode::chmod(path, b.mode()).is_err() {
// Error messages are printed by the mode::chmod function!
uucore::error::set_exit_code(1);
}
}
// If the exit code was set, or show! has been called at least once
// (which sets the exit code as well), function execution will end after
Expand Down Expand Up @@ -1078,11 +1085,13 @@ fn strip_file(to: &Path, b: &Behavior) -> UResult<()> {
/// Returns an empty Result or an error in case of failure.
///
fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> {
// Silent the warning as we want to the error message
mode::chmod(to, b.mode()).map_err(|_| InstallError::ChmodFailed(to.to_path_buf()))?;
// Ownership first, then mode, as GNU does: chown(2) clears setuid/setgid, and
// a failed chown must not leave a mode carrying them already applied.
if b.privileged {
chown_optional_user_group(to, b)?;
}
// Silent the warning as we want to the error message
mode::chmod(to, b.mode()).map_err(|_| InstallError::ChmodFailed(to.to_path_buf()))?;

Ok(())
}
Expand Down
59 changes: 59 additions & 0 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2656,6 +2656,65 @@ fn test_install_non_utf8_paths() {
ucmd.arg("-D").arg(source_file).arg(&target_path).succeeds();
}

/// A failed ownership change must not leave the setuid/setgid mode applied.
#[test]
fn test_install_failed_chown_does_not_leave_setuid() {
// Only meaningful when the chown can actually fail.
if geteuid() == 0 {
return;
}

let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("src");

scene
.ucmd()
.args(&["-m", "4755", "-o", "root", "src", "dst"])
.fails();

if at.file_exists("dst") {
let mode = at.metadata("dst").permissions().mode() & 0o7777;
assert_eq!(
mode & 0o6000,
0,
"install left setuid/setgid set ({mode:o}) after the chown failed"
);
}

// The same for a directory created with -d.
scene
.ucmd()
.args(&["-d", "-m", "4755", "-o", "root", "newdir"])
.fails();

if at.dir_exists("newdir") {
let mode = at.metadata("newdir").permissions().mode() & 0o7777;
assert_eq!(
mode & 0o6000,
0,
"install -d left setuid/setgid set ({mode:o}) after the chown failed"
);
}
}

/// The mode must still be applied in full when no ownership change is requested.
#[test]
fn test_install_setuid_mode_applied_without_chown() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("src");

scene.ucmd().args(&["-m", "4755", "src", "dst"]).succeeds();
assert_eq!(at.metadata("dst").permissions().mode() & 0o7777, 0o4755);

scene
.ucmd()
.args(&["-d", "-m", "2755", "newdir"])
.succeeds();
assert_eq!(at.metadata("newdir").permissions().mode() & 0o7777, 0o2755);
}

#[test]
fn test_install_unprivileged_option_u_skips_chown() {
// This test only makes sense when not running as root.
Expand Down
Loading