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
185 changes: 82 additions & 103 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,41 +505,47 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
}

for path in paths.iter().map(Path::new) {
// Special case to match GNU's behavior:
// install -d foo/. should work and just create foo/
// std::fs::create_dir("foo/."); fails in pure Rust
// See also mkdir.rs for another occurrence of this
let path_to_create = dir_strip_dot_for_creation(path);
// Set up first: like GNU, an unusable context fails even when nothing is created.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
let _selinux_guard = match directory_creation_context(&path_to_create, b) {
Ok(guard) => guard,
Err(e) => {
show!(e);
continue;
}
};
// if the path already exist, check if it's a file
if path.exists() {
if !path.is_dir() {
show!(InstallError::ExistingFileNotADirectory(path.to_path_buf()));
continue;
}
// Like GNU, -Z resets an existing target to its default context; --context doesn't.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
if b.privileged && b.default_context && is_selinux_enabled() {
show_if_err!(set_selinux_security_context(path, None));

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.

why not set_selinux_default_context(path) as before?
and please add a test for this -Z reset of an existing dir

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

set_selinux_default_context doesn't ask the policy: it keeps the dir's own context, so nothing gets reset. set_selinux_security_context(path, None) looks the default up. Added test_selinux_default_context_resets_existing_directory, which fails with the old call.

}
} else {
// Special case to match GNU's behavior:
// install -d foo/. should work and just create foo/
// std::fs::create_dir("foo/."); fails in pure Rust
// See also mkdir.rs for another occurrence of this
let path_to_create = dir_strip_dot_for_creation(path);
// Differently than the primary functionality
// (MainFunction::Standard), the directory functionality should
// create all ancestors (or components) of a directory
// regardless of the presence of the "-D" flag.
//
// Only the target gets the requested mode; newly created ancestors
// use DEFAULT_MODE, which create_dir_all would request as 0777.
let mut builder = fs::DirBuilder::new();
builder.recursive(true).mode(DEFAULT_MODE);
if let Err(e) = builder.create(&path_to_create).map_err_context(
// use DEFAULT_MODE.
let created = create_dir_all_with(&path_to_create, label_new_directories(b));
if let Err(e) = created.map_err_context(
|| translate!("install-error-create-dir-failed", "path" => path_to_create.quote()),
) {
show!(e);
continue;
}

// Set SELinux context for all created directories if needed
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
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);
}

if b.verbose {
writeln!(
stdout(),
Expand All @@ -559,17 +565,6 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
continue;
}

// Set SELinux context for directory if needed
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
if b.privileged {
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);
Expand Down Expand Up @@ -711,6 +706,9 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
target_filename = Some(filename.to_os_string());
}
} else {
// Before the -v lines, so a context error reports no directories.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
let selinux_guard = directory_creation_context(to_create, b)?;
if b.verbose {
let mut result = PathBuf::new();
// When creating directories with -Dv, show directory creations step by step
Expand All @@ -729,7 +727,15 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {

#[cfg(not(target_os = "redox"))]
{
match create_dir_all_safe(to_create, DEFAULT_MODE) {
let created =
create_dir_all_safe(to_create, DEFAULT_MODE, label_new_directories(b));
// End the creation context before any file is created.
#[cfg(all(
feature = "selinux",
any(target_os = "linux", target_os = "android")
))]
drop(selinux_guard);
match created {
Ok(dir_fd) => {
if b.target_dir.is_none()
&& sources.len() == 1
Expand All @@ -739,16 +745,6 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
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",
any(target_os = "linux", target_os = "android")
))]
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
Expand Down Expand Up @@ -1520,80 +1516,63 @@ fn derive_context_from_parent(parent_context: &str) -> String {
}
}

/// Helper function to collect paths that need `SELinux` context setting.
///
/// Traverses from the given starting path up to existing parent directories.
/// Returns a vector of paths in reverse order (from parent to child).
/// With --context, label the directories created while the returned guard lives.
/// -Z looks up each one instead, in `label_new_directories`.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn collect_paths_for_context_setting(starting_path: &Path) -> Vec<&Path> {
let mut paths: Vec<&Path> = starting_path
.ancestors()
.take_while(|p| p.exists())
.collect();
paths.reverse();
paths
fn directory_creation_context(
path: &Path,
b: &Behavior,
) -> Result<Option<uucore::selinux::FsCreateContext>, InstallError> {
if b.default_context || !should_set_selinux_context(b) || !is_selinux_enabled() {
return Ok(None);
}
uucore::selinux::FsCreateContext::new(path, None, b.context.as_ref())
.map(Some)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))
}

/// Sets the `SELinux` security context for a directory hierarchy.
///
/// This function traverses from the given starting path up to existing parent directories
/// and sets the `SELinux` context on each directory in the hierarchy (from parent to child).
/// This is useful when creating directory structures and needing to set contexts on all
/// created directories.
///
/// # Arguments
///
/// * `target_path` - The target path (typically the deepest directory in a hierarchy)
/// * `context` - Optional `SELinux` context string to set. If None, sets default context.
///
/// # Behavior
///
/// - Traverses from `target_path` upward to find existing parent directories
/// - Sets the context on each directory in reverse order (parent to child)
/// - Uses `show_if_err!` to handle errors gracefully without panicking
/// - Stops at filesystem root ("/") or empty path to prevent infinite loops
/// - Only processes paths that exist on the filesystem
/// - Silently handles `SELinux` context setting failures
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// // Set default context on directory hierarchy
/// // set_selinux_context_for_directories(Path::new("/tmp/new/deep/dir"), None);
///
/// // Set specific context on directory hierarchy
/// // let context = String::from("user_u:object_r:tmp_t:s0");
/// // set_selinux_context_for_directories(Path::new("/tmp/new/deep/dir"), Some(&context));
/// ```
/// The `before_create` hook for directory creation. With -Z, like GNU, each new
/// directory is labelled with the policy's default for its own name.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn set_selinux_context_for_directories(target_path: &Path, context: Option<&String>) {
for path in collect_paths_for_context_setting(target_path) {
show_if_err!(set_selinux_security_context(path, context));
fn label_new_directories(
b: &Behavior,
) -> impl FnMut(&Path) -> std::io::Result<Option<uucore::selinux::FsCreateContext>> + '_ {
move |path| {
if !(b.privileged && b.default_context && is_selinux_enabled()) {
return Ok(None);
}
let mode = uucore::libc::S_IFDIR | DEFAULT_MODE as uucore::libc::mode_t;
uucore::selinux::FsCreateContext::new(path, Some(mode), None)
.map(Some)
.map_err(std::io::Error::other)
}
}

/// Sets `SELinux` context for created directories using install's -Z default behavior.
///
/// Similar to `set_selinux_context_for_directories` but uses install's
/// specific default context derivation when no context is provided.
///
/// # Arguments
///
/// * `target_path` - The target path (typically the deepest directory in a hierarchy)
/// * `context` - Optional `SELinux` context string to set. If None, uses install's default derivation.
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
pub fn set_selinux_context_for_directories_install(target_path: &Path, context: Option<&String>) {
if context.is_some() {
// Use the standard function for explicit contexts
set_selinux_context_for_directories(target_path, context);
} else {
// For default context, we need our custom install behavior
for path in collect_paths_for_context_setting(target_path) {
show_if_err!(set_selinux_default_context(path));
#[cfg(not(all(feature = "selinux", any(target_os = "linux", target_os = "android"))))]
fn label_new_directories(_b: &Behavior) -> impl FnMut(&Path) -> std::io::Result<()> {
|_| Ok(())
}

/// Like [`fs::create_dir_all`], but calls `before_create` with each missing directory
/// first and keeps what it returns until that directory is created.
fn create_dir_all_with<G>(
path: &Path,
mut before_create: impl FnMut(&Path) -> std::io::Result<G>,
) -> std::io::Result<()> {
let missing: Vec<&Path> = path
.ancestors()
.take_while(|dir| !dir.as_os_str().is_empty() && !dir.exists())
.collect();
for dir in missing.into_iter().rev() {
let _created_with = before_create(dir)?;
if let Err(e) = fs::DirBuilder::new().mode(DEFAULT_MODE).create(dir) {
// Like fs::create_dir_all, accept a directory that appeared meanwhile.
if !(e.kind() == std::io::ErrorKind::AlreadyExists && dir.is_dir()) {
return Err(e);
}
}
}
Ok(())
}

#[cfg(test)]
Expand Down
31 changes: 23 additions & 8 deletions src/uucore/src/lib/features/safe_traversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -778,16 +778,24 @@ fn open_or_create_subdir(parent_fd: &DirFd, name: &OsStr, mode: u32) -> io::Resu
/// * `path` - The path to create directories for
/// * `mode` - The mode to use when creating new directories (e.g., 0o755). The actual
/// mode will be modified by the process umask.
/// * `before_create` - Called with the path of each missing directory before it is
/// created; what it returns is dropped once that directory is open.
///
/// # Returns
/// A [`DirFd`] for the final created directory, or the first existing parent if
/// all directories already exist.
#[cfg(unix)]
pub fn create_dir_all_safe(path: &Path, mode: u32) -> io::Result<DirFd> {
let (existing_ancestor, components_to_create) = find_existing_ancestor(path)?;
let mut dir_fd = DirFd::open_anchor(&existing_ancestor)?;
pub fn create_dir_all_safe<G>(
path: &Path,
mode: u32,
mut before_create: impl FnMut(&Path) -> io::Result<G>,
) -> io::Result<DirFd> {
let (mut current, components_to_create) = find_existing_ancestor(path)?;
let mut dir_fd = DirFd::open_anchor(&current)?;

for component in &components_to_create {
current.push(component);
let _created_with = before_create(&current)?;
dir_fd = open_or_create_subdir(&dir_fd, component.as_os_str(), mode)?;
}

Expand Down Expand Up @@ -1565,11 +1573,18 @@ mod tests {
#[test]
fn test_create_dir_all_safe_creates_nested_dirs() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir(temp_dir.path().join("a")).unwrap();
let nested_path = temp_dir.path().join("a/b/c");

let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
let mut seen = Vec::new();
let dir_fd = create_dir_all_safe(&nested_path, 0o755, |p| {
seen.push(p.to_path_buf());
Ok(())
})
.unwrap();
assert!(dir_fd.as_raw_fd() >= 0);
assert!(nested_path.is_dir());
assert_eq!(seen, [temp_dir.path().join("a/b"), nested_path]);
}

#[test]
Expand Down Expand Up @@ -1625,7 +1640,7 @@ mod tests {
let existing_path = temp_dir.path().join("existing");
fs::create_dir(&existing_path).unwrap();

let dir_fd = create_dir_all_safe(&existing_path, 0o755).unwrap();
let dir_fd = create_dir_all_safe(&existing_path, 0o755, |_| Ok(())).unwrap();
assert!(dir_fd.as_raw_fd() >= 0);
}

Expand All @@ -1641,7 +1656,7 @@ mod tests {
assert!(symlink_path.is_symlink());

// create_dir_all_safe should follow the symlink (GNU coreutils behavior)
let dir_fd = create_dir_all_safe(&symlink_path, 0o755).unwrap();
let dir_fd = create_dir_all_safe(&symlink_path, 0o755, |_| Ok(())).unwrap();
assert!(dir_fd.as_raw_fd() >= 0);

// Verify the symlink is preserved (not replaced with a real directory)
Expand All @@ -1655,7 +1670,7 @@ mod tests {
let file_path = temp_dir.path().join("file");
fs::write(&file_path, "content").unwrap();

let result = create_dir_all_safe(&file_path, 0o755);
let result = create_dir_all_safe(&file_path, 0o755, |_| Ok(()));
assert!(result.is_err());
}

Expand All @@ -1675,7 +1690,7 @@ mod tests {

// Try to create parent/link/subdir - the symlink should be followed (GNU behavior)
let nested_path = symlink_in_path.join("subdir");
let dir_fd = create_dir_all_safe(&nested_path, 0o755).unwrap();
let dir_fd = create_dir_all_safe(&nested_path, 0o755, |_| Ok(())).unwrap();
assert!(dir_fd.as_raw_fd() >= 0);

// The symlink should be preserved, not replaced
Expand Down
32 changes: 30 additions & 2 deletions src/uucore/src/lib/features/selinux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
use std::borrow::Cow;
use std::error::Error;
use std::marker::PhantomData;
use std::path::Path;
use std::path::{Path, PathBuf};

use crate::translate;
use selinux::SecurityContext;
Expand Down Expand Up @@ -323,7 +323,7 @@ fn absolute_for_policy_lookup(path: &Path) -> Cow<'_, Path> {
} else {
directory
};
std::fs::canonicalize(directory).map(|directory| directory.join(name))
canonicalize_allow_missing(directory).map(|directory| directory.join(name))
}
// No last component to keep: the name ends in `.` or `..`, which names
// a directory and never a symbolic link, so resolve the whole of it.
Expand All @@ -334,6 +334,25 @@ fn absolute_for_policy_lookup(path: &Path) -> Cow<'_, Path> {
resolved.map_or(Cow::Borrowed(path), Cow::Owned)
}

/// Like `canonicalize`, but a directory that does not exist yet, as for one about to be
/// created, is resolved through its parents with its own name kept.
fn canonicalize_allow_missing(directory: &Path) -> std::io::Result<PathBuf> {
match std::fs::canonicalize(directory) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let (Some(parent), Some(name)) = (directory.parent(), directory.file_name()) else {
return Err(e);
};
let parent = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
Ok(canonicalize_allow_missing(parent)?.join(name))
}
resolved => resolved,
}
}

/// Gets the SELinux security context for the given filesystem path.
///
/// Retrieves the security context of the specified filesystem path if SELinux is enabled
Expand Down Expand Up @@ -584,6 +603,15 @@ mod tests {
("branch/../quokka", directory.join("quokka")),
("branch/..", directory.clone()),
(".", directory.clone()),
// Parents that are about to be created, as for `install -d -Z a/b/c`.
(
"missing/deeper/quokka",
directory.join("missing/deeper/quokka"),
),
(
"branch/missing/quokka",
directory.join("branch/missing/quokka"),
),
] {
assert_eq!(absolute_for_policy_lookup(Path::new(given)), expected);
}
Expand Down
Loading
Loading