Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/uu/install/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ uucore = { workspace = true, default-features = true, features = [
] }
fluent = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true, features = ["fs", "user"] }

[features]
selinux = ["dep:selinux", "uucore/selinux"]

Expand Down
207 changes: 197 additions & 10 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

use clap::{Arg, ArgAction, ArgMatches, Command};
use file_diff::diff;
use filetime::{FileTime, set_file_times};
#[cfg(not(unix))]
use filetime::set_file_times;
use filetime::{FileTime, set_file_handle_times};
#[cfg(unix)]
use nix::unistd::{Gid, Uid, dup, fchown};

Check failure on line 16 in src/uu/install/src/install.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'fchown' (file:'src/uu/install/src/install.rs', line:16)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it able to use rustix instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swapped this over to rustix in c62d1d0 and re-ran the two targeted install tests; both pass.

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use selinux::SecurityContext;
use std::ffi::OsString;
Expand Down Expand Up @@ -39,9 +43,11 @@
use uucore::{format_usage, show, show_error, show_if_err};

#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::prelude::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};

const DEFAULT_MODE: u32 = 0o755;
const DEFAULT_STRIP_PROGRAM: &str = "strip";
Expand Down Expand Up @@ -764,9 +770,9 @@
);
}

copy_file_safe(source, parent_fd, filename.as_os_str())?;
let dest = copy_file_safe(source, parent_fd, filename.as_os_str())?;

finalize_installed_file(source, &target, b, backup_path)
finalize_installed_file(source, &target, b, backup_path, &dest)
} else {
copy(source, &target, b)
}
Expand Down Expand Up @@ -905,7 +911,11 @@
/// - `copy_file_safe` uses fd-based `DirFd::open_file_at()` (openat syscall)
/// - `copy_file` uses path-based `OpenOptions::new().create_new().open()`
#[cfg(unix)]
fn copy_file_safe(from: &Path, to_parent_fd: &DirFd, to_filename: &std::ffi::OsStr) -> UResult<()> {
fn copy_file_safe(
from: &Path,
to_parent_fd: &DirFd,
to_filename: &std::ffi::OsStr,
) -> UResult<File> {
let from_meta = metadata(from)?;

// Check if source and destination are the same file
Expand All @@ -923,7 +933,7 @@
let mut dst = to_parent_fd.open_file_at(to_filename)?;
copy_stream(&mut src, &mut dst)?;

Ok(())
Ok(dst)
}

/// Copy a file from one path to another. Handles the certain cases of special
Expand All @@ -938,7 +948,7 @@
///
/// Returns an empty Result or an error in case of failure.
///
fn copy_file(from: &Path, to: &Path) -> UResult<()> {
fn copy_file(from: &Path, to: &Path) -> UResult<File> {
use std::os::unix::fs::OpenOptionsExt;
if let Ok(to_abs) = to.canonicalize()
&& from.canonicalize()? == to_abs
Expand Down Expand Up @@ -976,9 +986,10 @@
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err.to_string())
})?;

Ok(())
Ok(dest)
}

#[cfg(not(unix))]
/// Strip a file using an external program.
///
/// # Parameters
Expand Down Expand Up @@ -1020,6 +1031,64 @@
Ok(())
}

#[cfg(unix)]
fn fd_operation_path(fd: &File) -> PathBuf {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
return PathBuf::from(format!("/proc/self/fd/{}", fd.as_raw_fd()));
}

#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
PathBuf::from(format!("/dev/fd/{}", fd.as_raw_fd()))
}
}

#[cfg(unix)]
fn path_matches_open_file(path: &Path, file: &File) -> UResult<bool> {
let file_meta = file.metadata().map_err(InstallError::MetadataFailed)?;
let path_meta = match metadata(path) {
Ok(meta) => meta,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};

Ok(file_meta.dev() == path_meta.dev() && file_meta.ino() == path_meta.ino())
}

#[cfg(unix)]
fn discard_installed_path_if_unchanged(path: &Path, file: &File) {
if path_matches_open_file(path, file).unwrap_or(false) {
let _ = fs::remove_file(path);
}
}

#[cfg(unix)]
fn strip_file_fd(to: &Path, file: &File, b: &Behavior) -> UResult<()> {
let strip_fd = dup(file).map_err(|e| InstallError::StripProgramFailed(e.to_string()))?;
let strip_file = File::from(strip_fd);
let strip_arg = fd_operation_path(&strip_file);

match process::Command::new(&b.strip_program).arg(&strip_arg).status() {
Ok(status) => {
if !status.success() {
discard_installed_path_if_unchanged(to, file);
return Err(InstallError::StripProgramFailed(
translate!("install-error-strip-abnormal", "code" => status.code().unwrap()),
)
.into());
}
}
Err(e) => {
discard_installed_path_if_unchanged(to, file);
return Err(InstallError::StripProgramFailed(e.to_string()).into());
}
}

Ok(())
}

#[cfg(not(unix))]
/// Set ownership and permissions on the destination file.
///
/// # Parameters
Expand All @@ -1045,6 +1114,33 @@
Ok(())
}

#[cfg(unix)]
fn chown_optional_user_group_fd(file: &File, path: &Path, b: &Behavior) -> UResult<()> {
let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() {
(b.owner_id, b.group_id)
} else {
return Ok(());
};

fchown(file, owner_id.map(Uid::from_raw), group_id.map(Gid::from_raw))

Check failure on line 1125 in src/uu/install/src/install.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'fchown' (file:'src/uu/install/src/install.rs', line:1125)
.map_err(|e| InstallError::ChownFailed(path.to_path_buf(), e.to_string()))?;

Ok(())
}

#[cfg(unix)]
fn set_ownership_and_permissions_fd(file: &File, to: &Path, b: &Behavior) -> UResult<()> {
file.set_permissions(fs::Permissions::from_mode(b.mode()))
.map_err(|_| InstallError::ChmodFailed(to.to_path_buf()))?;

if b.privileged {
chown_optional_user_group_fd(file, to, b)?;
}

Ok(())
}

#[cfg(not(unix))]
/// Preserve timestamps on the destination file.
///
/// # Parameters
Expand Down Expand Up @@ -1072,7 +1168,91 @@
Ok(())
}

#[cfg(unix)]
fn preserve_timestamps_fd(from: &Path, file: &File) -> UResult<()> {
let meta = match metadata(from) {
Ok(meta) => meta,
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
};

let modified_time = FileTime::from_last_modification_time(&meta);
let accessed_time = FileTime::from_last_access_time(&meta);

if let Err(e) = set_file_handle_times(file, Some(accessed_time), Some(modified_time)) {
show_error!("{e}");
}
Ok(())
}

/// Apply post-copy operations: strip, ownership, permissions, timestamps, SELinux, and verbose output.
#[cfg(unix)]
fn finalize_installed_file(
from: &Path,
to: &Path,
b: &Behavior,
backup_path: Option<PathBuf>,
file: &File,
) -> UResult<()> {
if b.strip {
strip_file_fd(to, file, b)?;
}

set_ownership_and_permissions_fd(file, to, b)?;

if b.preserve_timestamps {
preserve_timestamps_fd(from, file)?;
}

#[cfg(all(feature = "selinux", target_os = "linux"))]
if b.privileged {
if b.preserve_context {
let context = get_selinux_security_context(from, false)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
if !context.is_empty() {
set_selinux_security_context(&fd_operation_path(file), Some(&context))
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
}
} else if b.default_context {
match get_default_context_for_path(to) {
Ok(Some(default_ctx)) => {
set_selinux_security_context(&fd_operation_path(file), Some(&default_ctx))
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?
}
Ok(None) | Err(_) => set_selinux_default_context(to)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?,
}
} else if b.context.is_some() {
let context = get_context_for_selinux(b);
set_selinux_security_context(&fd_operation_path(file), context)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
}
}

if !path_matches_open_file(to, file)? {
return Err(InstallError::InstallFailed(
from.to_path_buf(),
to.to_path_buf(),
String::from("destination path changed during installation"),
)
.into());
}

if b.verbose {
write!(stdout(), "{} -> {}", from.quote(), to.quote())?;
match backup_path {
Some(path) => writeln!(
stdout(),
" {}",
translate!("install-verbose-backup", "backup" => path.quote())
)?,
None => writeln!(stdout())?,
}
}

Ok(())
}

#[cfg(not(unix))]
fn finalize_installed_file(
from: &Path,
to: &Path,
Expand Down Expand Up @@ -1140,9 +1320,16 @@
// Declare the path here as we may need it for the verbose output below.
let backup_path = perform_backup(to, b)?;

copy_file(from, to)?;
let dest = copy_file(from, to)?;

finalize_installed_file(from, to, b, backup_path)
#[cfg(unix)]
{
finalize_installed_file(from, to, b, backup_path, &dest)
}
#[cfg(not(unix))]
{
finalize_installed_file(from, to, b, backup_path)
}
}

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
Expand Down
10 changes: 7 additions & 3 deletions src/uucore/src/lib/features/safe_traversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,16 @@ impl DirFd {
Ok(())
}

/// Open a file for writing relative to this directory
/// Creates the file if it doesn't exist, truncates if it does
/// Open a newly-created file for writing relative to this directory.
/// Fails if the final path component already exists or is a symlink.
pub fn open_file_at(&self, name: &OsStr) -> io::Result<fs::File> {
let name_cstr =
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
let flags = OFlag::O_CREAT | OFlag::O_WRONLY | OFlag::O_TRUNC | OFlag::O_CLOEXEC;
let flags = OFlag::O_CREAT
| OFlag::O_EXCL
| OFlag::O_NOFOLLOW
| OFlag::O_WRONLY
| OFlag::O_CLOEXEC;
let mode = Mode::from_bits_truncate(0o666); // Default file permissions

let fd: OwnedFd = openat(self.fd.as_fd(), name_cstr.as_c_str(), flags, mode)
Expand Down
Loading
Loading