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
218 changes: 196 additions & 22 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use uucore::error::{FromIo, UError, UResult, UUsageError};
use uucore::fs::dir_strip_dot_for_creation;
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
use uucore::process::{getegid, geteuid};
#[cfg(unix)]
use uucore::safe_traversal::{DirFd, create_dir_all_safe};
#[cfg(all(feature = "selinux", target_os = "linux"))]
use uucore::selinux::{
SeLinuxError, contexts_differ, get_selinux_security_context, is_selinux_enabled,
Expand Down Expand Up @@ -489,7 +491,7 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
}

// Set SELinux context for all created directories if needed
#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(path_to_create.as_path(), context);
Expand All @@ -513,7 +515,7 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
show_if_err!(chown_optional_user_group(path, b));

// Set SELinux context for directory if needed
#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
if b.default_context {
show_if_err!(set_selinux_default_context(path));
} else if b.context.is_some() {
Expand Down Expand Up @@ -592,6 +594,11 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {

let sources = &paths.iter().map(PathBuf::from).collect::<Vec<_>>();

#[cfg(unix)]
let mut target_parent_fd: Option<DirFd> = None;
#[cfg(unix)]
let mut target_filename: Option<std::ffi::OsString> = None;

if b.create_leading {
// if -t is used in combination with -D, create whole target because it does not include filename
let to_create: Option<&Path> = if b.target_dir.is_some() {
Expand All @@ -603,8 +610,13 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
None
};

// If -t is used, check if target exists as a file before trying to create directories
if b.target_dir.is_some() && target.exists() && !target.is_dir() {
return Err(InstallError::NotADirectory(target.clone()).into());
}

if let Some(to_create) = to_create {
// if the path ends in /, remove it
let to_create_original = to_create;
let to_create_owned;
let to_create = match uucore::os_str_as_bytes(to_create.as_os_str()) {
Ok(path_bytes) if path_bytes.ends_with(b"/") => {
Expand All @@ -619,7 +631,30 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
_ => to_create,
};

if !to_create.exists() {
let dir_exists = if to_create.exists() {
fs::symlink_metadata(to_create)
.map(|m| m.is_dir() && !m.file_type().is_symlink())
.unwrap_or(false)
} else {
false
};

if dir_exists {
#[cfg(unix)]
{
if b.target_dir.is_none()
&& sources.len() == 1
&& !is_potential_directory_path(&target)
{
if let Ok(dir_fd) = DirFd::open_no_follow(to_create) {
if let Some(filename) = target.file_name() {
target_parent_fd = Some(dir_fd);
target_filename = Some(filename.to_os_string());
}
}
}
}
} else {
if b.verbose {
let mut result = PathBuf::new();
// When creating directories with -Dv, show directory creations step by step
Expand All @@ -635,23 +670,61 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
}
}

if let Err(e) = fs::create_dir_all(to_create) {
return Err(InstallError::CreateDirFailed(to_create.to_path_buf(), e).into());
#[cfg(unix)]
{
match create_dir_all_safe(to_create, DEFAULT_MODE) {
Ok(dir_fd) => {
if b.target_dir.is_none()
&& sources.len() == 1
&& !is_potential_directory_path(&target)
{
if let Some(filename) = target.file_name() {
target_parent_fd = Some(dir_fd);
target_filename = Some(filename.to_os_string());
}
}

// Set SELinux context for all created directories if needed
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(to_create, context);
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::AlreadyExists
&& to_create.exists()
&& !to_create.is_dir()
{
return Err(InstallError::NotADirectory(
to_create_original.to_path_buf(),
)
.into());
}
return Err(InstallError::CreateDirFailed(
to_create_original.to_path_buf(),
e,
)
.into());
}
}
}

// Set SELinux context for all created directories if needed
#[cfg(feature = "selinux")]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(to_create, context);
}
}
}
if b.target_dir.is_some() {
let p = to_create.unwrap();
#[cfg(not(unix))]
{
if let Err(e) = fs::create_dir_all(to_create) {
return Err(
InstallError::CreateDirFailed(to_create.to_path_buf(), e).into()
);
}

if !p.exists() || !p.is_dir() {
return Err(InstallError::NotADirectory(p.to_path_buf()).into());
// Set SELinux context for all created directories if needed
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(to_create, context);
}
}
}
}
}
Expand All @@ -676,7 +749,73 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
}

if target.is_file() || is_new_file_path(&target) {
copy(source, &target, b)
#[cfg(unix)]
if let (Some(ref parent_fd), Some(ref filename)) = (target_parent_fd, target_filename) {
if b.compare && !need_copy(source, &target, b) {
return Ok(());
}

let backup_path = perform_backup(&target, b)?;

if let Err(e) = parent_fd.unlink_at(filename.as_os_str(), false) {
if e.kind() != std::io::ErrorKind::NotFound {
show_error!(
"{}",
translate!("install-error-failed-to-remove", "path" => target.quote(), "error" => format!("{e:?}"))
);
}
}

copy_file_safe(source, parent_fd, filename.as_os_str())?;

#[cfg(not(windows))]
if b.strip {
strip_file(&target, b)?;
}

set_ownership_and_permissions(&target, b)?;

if b.preserve_timestamps {
preserve_timestamps(source, &target)?;
}

#[cfg(all(feature = "selinux", target_os = "linux"))]
if !b.unprivileged {
if b.preserve_context {
uucore::selinux::preserve_security_context(source, &target)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
} else if b.default_context {
set_selinux_default_context(&target)
.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(&target, context)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
}
}

if b.verbose {
print!(
"{}",
translate!("install-verbose-copy", "from" => source.quote(), "to" => target.quote())
);
match backup_path {
Some(path) => println!(
" {}",
translate!("install-verbose-backup", "backup" => path.quote())
),
None => println!(),
}
}

Ok(())
} else {
copy(source, &target, b)
}
#[cfg(not(unix))]
{
copy(source, &target, b)
}
} else {
Err(InstallError::InvalidTarget(target).into())
}
Expand Down Expand Up @@ -796,6 +935,41 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
}
}

/// Copy a file using directory file descriptor for safe traversal.
/// This prevents symlink race conditions when creating target files.
#[cfg(unix)]
fn copy_file_safe(from: &Path, to_parent_fd: &DirFd, to_filename: &std::ffi::OsStr) -> UResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

The safe copy path duplicates much of the regular copy() function logic (backup, strip, ownership, permissions, timestamps, SELinux). This increases maintenance burden

use std::os::unix::fs::FileTypeExt;

let from_meta = metadata(from)?;
let ft = from_meta.file_type();

// Check if source and destination are the same file
if let Ok(to_stat) = to_parent_fd.stat_at(to_filename, true) {
// st_dev and st_ino types vary by platform (i32/u32 on macOS, u64 on Linux)
#[allow(clippy::unnecessary_cast)]
if from_meta.dev() == to_stat.st_dev as u64 && from_meta.ino() == to_stat.st_ino as u64 {
return Err(
InstallError::SameFile(from.to_path_buf(), PathBuf::from(to_filename)).into(),
);
}
}

if ft.is_char_device() || ft.is_block_device() || ft.is_fifo() {
let mut handle = File::open(from)?;
let mut dest = to_parent_fd.open_file_at(to_filename)?;
copy_stream(&mut handle, &mut dest)?;
return Ok(());
}

let mut src = File::open(from)?;
let mut dst = to_parent_fd.open_file_at(to_filename)?;
std::io::copy(&mut src, &mut dst)?;
dst.sync_all()?;

Ok(())
}

/// Copy a file from one path to another. Handles the certain cases of special
/// files (e.g character specials).
///
Expand Down Expand Up @@ -972,7 +1146,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
preserve_timestamps(from, to)?;
}

#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
if !b.unprivileged {
if b.preserve_context {
uucore::selinux::preserve_security_context(from, to)
Expand Down Expand Up @@ -1013,7 +1187,7 @@ fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
}
}

#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
fn should_set_selinux_context(b: &Behavior) -> bool {
!b.unprivileged && (b.context.is_some() || b.default_context)
}
Expand Down Expand Up @@ -1108,7 +1282,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
return true;
}

#[cfg(feature = "selinux")]
#[cfg(all(feature = "selinux", target_os = "linux"))]
if !b.unprivileged && b.preserve_context && contexts_differ(from, to) {
return true;
}
Expand Down
Loading
Loading