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
148 changes: 42 additions & 106 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,18 +489,32 @@ 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
Expand All @@ -517,13 +531,6 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
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 @@ -543,17 +550,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 @@ -696,6 +692,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 @@ -716,7 +715,14 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
{
// Use DEFAULT_MODE (0o755) for created directories - this matches GNU install
// behavior. The actual mode will be modified by umask at the kernel level.
match create_dir_all_safe(to_create, DEFAULT_MODE) {
let created = create_dir_all_safe(to_create, DEFAULT_MODE);
// 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 @@ -726,16 +732,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 @@ -1488,80 +1484,20 @@ 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 -Z or --context, label the directories created while the returned guard lives.
/// One policy lookup for `path` serves all of them; GNU looks up each component.
#[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
}

/// 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));
/// ```
#[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));
}
}

/// 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));
}
fn directory_creation_context(
path: &Path,
b: &Behavior,
) -> Result<Option<uucore::selinux::FsCreateContext>, InstallError> {
if !should_set_selinux_context(b) || !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), get_context_for_selinux(b))
.map(Some)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))
}

#[cfg(test)]
Expand Down
35 changes: 32 additions & 3 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 @@ -146,8 +146,9 @@ impl FsCreateContext {
SeLinuxError::ContextRetrievalFailure(selinux_error_description(&e))
})?;

let path = absolute_for_policy_lookup(path);
let default_context =
match labeler.look_up_by_path(path, mode.and_then(selinux::FileAccessMode::new)) {
match labeler.look_up_by_path(&path, mode.and_then(selinux::FileAccessMode::new)) {
Ok(context) => context,
Err(e) if is_ignorable_context_error(&e) => {
return Ok(Self {
Expand Down Expand Up @@ -322,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 @@ -333,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 @@ -583,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