Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

install: Manages permissions when run as root #5735

Merged
merged 5 commits into from
Dec 28, 2023
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
80 changes: 53 additions & 27 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@
use clap::{crate_version, Arg, ArgAction, ArgMatches, Command};
use file_diff::diff;
use filetime::{set_file_times, FileTime};
use uucore::backup_control::{self, BackupMode};
use uucore::display::Quotable;
use uucore::entries::{grp2gid, usr2uid};
use uucore::error::{FromIo, UError, UIoError, UResult, UUsageError};
use uucore::fs::dir_strip_dot_for_creation;
use uucore::mode::get_umask;
use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel};
use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err, uio_error};

use std::error::Error;
use std::fmt::{Debug, Display};
use std::fs;
Expand All @@ -28,8 +19,15 @@
use std::os::unix::prelude::OsStrExt;
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::process;
#[cfg(not(target_os = "windows"))]
use uucore::backup_control::{self, BackupMode};
use uucore::display::Quotable;
use uucore::entries::{grp2gid, usr2uid};
use uucore::error::{FromIo, UError, UIoError, UResult, UUsageError};
use uucore::fs::dir_strip_dot_for_creation;
use uucore::mode::get_umask;
use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel};
use uucore::process::{getegid, geteuid};
use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err, uio_error};

const DEFAULT_MODE: u32 = 0o755;
const DEFAULT_STRIP_PROGRAM: &str = "strip";
Expand Down Expand Up @@ -665,6 +663,7 @@
/// Handle incomplete user/group parings for chown.
///
/// Returns a Result type with the Err variant containing the error message.
/// If the user is root, revert the uid & gid
///
/// # Parameters
///
Expand All @@ -676,23 +675,31 @@
/// return an empty error value.
///
fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> {
if b.owner_id.is_some() || b.group_id.is_some() {
let meta = match fs::metadata(path) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};
// GNU coreutils doesn't print chown operations during install with verbose flag.
let verbosity = Verbosity {
groups_only: b.owner_id.is_none(),
level: VerbosityLevel::Normal,
};

// GNU coreutils doesn't print chown operations during install with verbose flag.
let verbosity = Verbosity {
groups_only: b.owner_id.is_none(),
level: VerbosityLevel::Normal,
};
// Determine the owner and group IDs to be used for chown.
let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() {
(b.owner_id, b.group_id)
} else if geteuid() == 0 {
// Special case for root user.
(Some(0), Some(0))

Check warning on line 689 in src/uu/install/src/install.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/install/src/install.rs#L689

Added line #L689 was not covered by tests
} else {
// No chown operation needed.
return Ok(());
};

match wrap_chown(path, &meta, b.owner_id, b.group_id, false, verbosity) {
Ok(msg) if b.verbose && !msg.is_empty() => println!("chown: {msg}"),
Ok(_) => {}
Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()),
}
let meta = match fs::metadata(path) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),

Check warning on line 697 in src/uu/install/src/install.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/install/src/install.rs#L697

Added line #L697 was not covered by tests
};
match wrap_chown(path, &meta, owner_id, group_id, false, verbosity) {
Ok(msg) if b.verbose && !msg.is_empty() => println!("chown: {msg}"),
Ok(_) => {}
Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()),

Check warning on line 702 in src/uu/install/src/install.rs

View check run for this annotation

Codecov / codecov/patch

src/uu/install/src/install.rs#L702

Added line #L702 was not covered by tests
}

Ok(())
Expand Down Expand Up @@ -916,55 +923,74 @@
/// Crashes the program if a nonexistent owner or group is specified in _b_.
///
fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult<bool> {
// Attempt to retrieve metadata for the source file.
// If this fails, assume the file needs to be copied.
let from_meta = match fs::metadata(from) {
Ok(meta) => meta,
Err(_) => return Ok(true),
};

// Attempt to retrieve metadata for the destination file.
// If this fails, assume the file needs to be copied.
let to_meta = match fs::metadata(to) {
Ok(meta) => meta,
Err(_) => return Ok(true),
};

// setuid || setgid || sticky
// Define special file mode bits (setuid, setgid, sticky).
let extra_mode: u32 = 0o7000;
// Define all file mode bits (including permissions).
// setuid || setgid || sticky || permissions
let all_modes: u32 = 0o7777;

// Check if any special mode bits are set in the specified mode,
// source file mode, or destination file mode.
if b.specified_mode.unwrap_or(0) & extra_mode != 0
|| from_meta.mode() & extra_mode != 0
|| to_meta.mode() & extra_mode != 0
{
return Ok(true);
}

// Check if the mode of the destination file differs from the specified mode.
if b.mode() != to_meta.mode() & all_modes {
return Ok(true);
}

// Check if either the source or destination is not a file.
if !from_meta.is_file() || !to_meta.is_file() {
return Ok(true);
}

// Check if the file sizes differ.
if from_meta.len() != to_meta.len() {
return Ok(true);
}

// TODO: if -P (#1809) and from/to contexts mismatch, return true.

// Check if the owner ID is specified and differs from the destination file's owner.
if let Some(owner_id) = b.owner_id {
if owner_id != to_meta.uid() {
return Ok(true);
}
} else if let Some(group_id) = b.group_id {
}

// Check if the group ID is specified and differs from the destination file's group.
if let Some(group_id) = b.group_id {
if group_id != to_meta.gid() {
return Ok(true);
}
} else {
#[cfg(not(target_os = "windows"))]
// Check if the destination file's owner or group
// differs from the effective user/group ID of the process.
if to_meta.uid() != geteuid() || to_meta.gid() != getegid() {
return Ok(true);
}
}

// Check if the contents of the source and destination files differ.
if !diff(from.to_str().unwrap(), to.to_str().unwrap()) {
return Ok(true);
}
Expand Down
34 changes: 32 additions & 2 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
// file that was distributed with this source code.
// spell-checker:ignore (words) helloworld nodir objdump n'source

use crate::common::util::{is_ci, TestScenario};
use crate::common::util::{is_ci, run_ucmd_as_root, TestScenario};
use filetime::FileTime;
use std::os::unix::fs::PermissionsExt;
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[cfg(not(any(windows, target_os = "freebsd")))]
use std::process::Command;
#[cfg(any(target_os = "linux", target_os = "android"))]
Expand Down Expand Up @@ -1613,3 +1614,32 @@
.fails()
.stderr_contains("failed to access 'dir/target_file/': Not a directory");
}

#[test]
fn test_install_root_combined() {
let ts = TestScenario::new(util_name!());
let at = ts.fixtures.clone();
at.touch("a");
at.touch("c");

let run_and_check = |args: &[&str], target: &str, expected_uid: u32, expected_gid: u32| {
if let Ok(result) = run_ucmd_as_root(&ts, args) {
result.success();
assert!(at.file_exists(target));

Check warning on line 1628 in tests/by-util/test_install.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_install.rs#L1627-L1628

Added lines #L1627 - L1628 were not covered by tests

let metadata = fs::metadata(at.plus(target)).unwrap();
assert_eq!(metadata.uid(), expected_uid);
assert_eq!(metadata.gid(), expected_gid);
} else {

Check warning on line 1633 in tests/by-util/test_install.rs

View check run for this annotation

Codecov / codecov/patch

tests/by-util/test_install.rs#L1630-L1633

Added lines #L1630 - L1633 were not covered by tests
print!("Test skipped; requires root user");
}
};

run_and_check(&["-Cv", "-o1", "-g1", "a", "b"], "b", 1, 1);
run_and_check(&["-Cv", "-o2", "-g1", "a", "b"], "b", 2, 1);
run_and_check(&["-Cv", "-o2", "-g2", "a", "b"], "b", 2, 2);

run_and_check(&["-Cv", "-o2", "c", "d"], "d", 2, 0);
run_and_check(&["-Cv", "c", "d"], "d", 0, 0);
run_and_check(&["-Cv", "c", "d"], "d", 0, 0);
}
Loading