From 71af6d20892e572a5a48b06960be9ee89d60ac3e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 9 May 2025 23:01:12 +0200 Subject: [PATCH 1/2] selinux/uucore: add two functions: contexts_differ & preserve_security_context --- src/uucore/src/lib/features/selinux.rs | 248 +++++++++++++++++++++++-- 1 file changed, 230 insertions(+), 18 deletions(-) diff --git a/src/uucore/src/lib/features/selinux.rs b/src/uucore/src/lib/features/selinux.rs index 6a4cab927c..062d1c16c8 100644 --- a/src/uucore/src/lib/features/selinux.rs +++ b/src/uucore/src/lib/features/selinux.rs @@ -228,6 +228,106 @@ pub fn get_selinux_security_context(path: &Path) -> Result } } +/// Compares SELinux security contexts of two filesystem paths. +/// +/// This function retrieves and compares the SELinux security contexts of two paths. +/// If the contexts differ or an error occurs during retrieval, it returns true. +/// +/// # Arguments +/// +/// * `from_path` - Source filesystem path. +/// * `to_path` - Destination filesystem path. +/// +/// # Returns +/// +/// * `true` - If contexts differ, cannot be retrieved, or if SELinux is not enabled. +/// * `false` - If contexts are the same. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use uucore::selinux::contexts_differ; +/// +/// // Check if contexts differ between two files +/// let differ = contexts_differ(Path::new("/path/to/source"), Path::new("/path/to/destination")); +/// if differ { +/// println!("Files have different SELinux contexts"); +/// } else { +/// println!("Files have the same SELinux context"); +/// } +/// ``` +pub fn contexts_differ(from_path: &Path, to_path: &Path) -> bool { + if !is_selinux_enabled() { + return true; + } + + // Check if SELinux contexts differ + match ( + selinux::SecurityContext::of_path(from_path, false, false), + selinux::SecurityContext::of_path(to_path, false, false), + ) { + (Ok(Some(from_ctx)), Ok(Some(to_ctx))) => { + // Convert contexts to CString and compare + match (from_ctx.to_c_string(), to_ctx.to_c_string()) { + (Ok(Some(from_c_str)), Ok(Some(to_c_str))) => { + from_c_str.to_string_lossy() != to_c_str.to_string_lossy() + } + // If contexts couldn't be converted to CString or were None, consider them different + _ => true, + } + } + // If either context is None or an error occurred, assume contexts differ + _ => true, + } +} + +/// Preserves the SELinux security context from one filesystem path to another. +/// +/// This function copies the security context from the source path to the destination path. +/// If SELinux is not enabled, or if the source has no context, the function returns success +/// without making any changes. +/// +/// # Arguments +/// +/// * `from_path` - Source filesystem path from which to copy the SELinux context. +/// * `to_path` - Destination filesystem path to which the context should be applied. +/// +/// # Returns +/// +/// * `Ok(())` - If the context was successfully preserved or if SELinux is not enabled. +/// * `Err(SeLinuxError)` - If an error occurred during context retrieval or application. +/// +/// # Examples +/// +/// ``` +/// use std::path::Path; +/// use uucore::selinux::preserve_security_context; +/// +/// // Preserve the SELinux context from source to destination +/// match preserve_security_context(Path::new("/path/to/source"), Path::new("/path/to/destination")) { +/// Ok(_) => println!("Context preserved successfully (or SELinux is not enabled)"), +/// Err(err) => eprintln!("Failed to preserve context: {}", err), +/// } +/// ``` +pub fn preserve_security_context(from_path: &Path, to_path: &Path) -> Result<(), SeLinuxError> { + // If SELinux is not enabled, return success without doing anything + if !is_selinux_enabled() { + return Err(SeLinuxError::SELinuxNotEnabled); + } + + // Get context from the source path + let context = get_selinux_security_context(from_path)?; + + // If no context was found, just return success (nothing to preserve) + if context.is_empty() { + return Ok(()); + } + + // Apply the context to the destination path + set_selinux_security_context(to_path, Some(&context)) +} + #[cfg(test)] mod tests { use super::*; @@ -295,18 +395,6 @@ mod tests { let result = set_selinux_security_context(path, Some(&invalid_context)); assert!(result.is_err()); - if let Err(err) = result { - match err { - SeLinuxError::ContextConversionFailure(ctx, msg) => { - assert_eq!(ctx, "invalid\0context"); - assert!( - msg.contains("nul byte"), - "Error message should mention nul byte" - ); - } - _ => panic!("Expected ContextConversionFailure error but got: {}", err), - } - } } #[test] @@ -402,15 +490,139 @@ mod tests { let result = get_selinux_security_context(path); assert!(result.is_err()); + } + + #[test] + fn test_contexts_differ() { + let file1 = NamedTempFile::new().expect("Failed to create first tempfile"); + let file2 = NamedTempFile::new().expect("Failed to create second tempfile"); + let path1 = file1.path(); + let path2 = file2.path(); + + std::fs::write(path1, b"content for file 1").expect("Failed to write to first tempfile"); + std::fs::write(path2, b"content for file 2").expect("Failed to write to second tempfile"); + + if !is_selinux_enabled() { + assert!( + contexts_differ(path1, path2), + "contexts_differ should return true when SELinux is not enabled" + ); + return; + } + + let test_context = String::from("system_u:object_r:tmp_t:s0"); + let result1 = set_selinux_security_context(path1, Some(&test_context)); + let result2 = set_selinux_security_context(path2, Some(&test_context)); + + if result1.is_ok() && result2.is_ok() { + assert!( + !contexts_differ(path1, path2), + "Contexts should not differ when the same context is set on both files" + ); + + let different_context = String::from("system_u:object_r:user_tmp_t:s0"); + if set_selinux_security_context(path2, Some(&different_context)).is_ok() { + assert!( + contexts_differ(path1, path2), + "Contexts should differ when different contexts are set" + ); + } + } else { + println!( + "Note: Couldn't set SELinux contexts to test differences. This is expected if the test doesn't have sufficient permissions." + ); + assert!( + contexts_differ(path1, path2), + "Contexts should differ when different contexts are set" + ); + } + + let nonexistent_path = Path::new("/nonexistent/file/path"); + assert!( + contexts_differ(path1, nonexistent_path), + "contexts_differ should return true when one path doesn't exist" + ); + } + + #[test] + fn test_preserve_security_context() { + let source_file = NamedTempFile::new().expect("Failed to create source tempfile"); + let dest_file = NamedTempFile::new().expect("Failed to create destination tempfile"); + let source_path = source_file.path(); + let dest_path = dest_file.path(); + + std::fs::write(source_path, b"source content").expect("Failed to write to source tempfile"); + std::fs::write(dest_path, b"destination content") + .expect("Failed to write to destination tempfile"); + + if !is_selinux_enabled() { + let result = preserve_security_context(source_path, dest_path); + assert!( + result.is_err(), + "preserve_security_context should fail when SELinux is not enabled" + ); + return; + } + + let source_context = String::from("system_u:object_r:tmp_t:s0"); + let result = set_selinux_security_context(source_path, Some(&source_context)); + + if result.is_ok() { + let preserve_result = preserve_security_context(source_path, dest_path); + assert!( + preserve_result.is_ok(), + "Failed to preserve context: {:?}", + preserve_result.err() + ); + + assert!( + !contexts_differ(source_path, dest_path), + "Contexts should be the same after preserving" + ); + } else { + println!( + "Note: Couldn't set SELinux context on source file to test preservation. This is expected if the test doesn't have sufficient permissions." + ); + + let preserve_result = preserve_security_context(source_path, dest_path); + assert!(preserve_result.is_err()); + } + + let nonexistent_path = Path::new("/nonexistent/file/path"); + let result = preserve_security_context(nonexistent_path, dest_path); + assert!( + result.is_err(), + "preserve_security_context should fail when source file doesn't exist" + ); + + let result = preserve_security_context(source_path, nonexistent_path); + assert!( + result.is_err(), + "preserve_security_context should fail when destination file doesn't exist" + ); + } + + #[test] + fn test_preserve_security_context_empty_context() { + let source_file = NamedTempFile::new().expect("Failed to create source tempfile"); + let dest_file = NamedTempFile::new().expect("Failed to create destination tempfile"); + let source_path = source_file.path(); + let dest_path = dest_file.path(); + + if !is_selinux_enabled() { + return; + } + + let result = preserve_security_context(source_path, dest_path); + if let Err(err) = result { match err { - SeLinuxError::FileOpenFailure(e) => { - assert!( - e.contains("No such file"), - "Error should mention file not found" - ); + SeLinuxError::ContextSetFailure(_, _) => { + println!("Note: Could not set context due to permissions: {}", err); + } + unexpected => { + panic!("Unexpected error: {}", unexpected); } - _ => panic!("Expected FileOpenFailure error but got: {}", err), } } } From 38861cc7675e282ee02922cc9c191fb2565b82e7 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 9 May 2025 22:17:21 +0200 Subject: [PATCH 2/2] selinux: add support for install --- Cargo.toml | 1 + src/uu/install/Cargo.toml | 3 ++ src/uu/install/src/install.rs | 77 ++++++++++++++++------------- tests/by-util/test_install.rs | 91 +++++++++++++++++++++++++++-------- 4 files changed, 120 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 67c9e4f792..7d53404a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ feat_acl = ["cp/feat_acl"] feat_selinux = [ "cp/selinux", "id/selinux", + "install/selinux", "ls/selinux", "mkdir/selinux", "mkfifo/selinux", diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 5e7c5c5df8..a715902cba 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -33,6 +33,9 @@ uucore = { workspace = true, features = [ "process", ] } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "install" path = "src/main.rs" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 4cad5d1fb5..c4590240be 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -25,6 +25,8 @@ use uucore::fs::dir_strip_dot_for_creation; use uucore::mode::get_umask; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; +#[cfg(feature = "selinux")] +use uucore::selinux::{contexts_differ, set_selinux_security_context}; use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err}; #[cfg(unix)] @@ -51,13 +53,12 @@ pub struct Behavior { create_leading: bool, target_dir: Option, no_target_dir: bool, + preserve_context: bool, + context: Option, } #[derive(Error, Debug)] enum InstallError { - #[error("Unimplemented feature: {0}")] - Unimplemented(String), - #[error("{} with -d requires at least one argument.", uucore::util_name())] DirNeedsArg, @@ -108,14 +109,15 @@ enum InstallError { #[error("extra operand {}\n{}", .0.quote(), .1.quote())] ExtraOperand(String, String), + + #[cfg(feature = "selinux")] + #[error("{}", .0)] + SelinuxContextFailed(String), } impl UError for InstallError { fn code(&self) -> i32 { - match self { - Self::Unimplemented(_) => 2, - _ => 1, - } + 1 } fn usage(&self) -> bool { @@ -172,8 +174,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - check_unimplemented(&matches)?; - let behavior = behavior(&matches)?; match behavior.main_function { @@ -295,21 +295,20 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue), ) .arg( - // TODO implement flag Arg::new(OPT_PRESERVE_CONTEXT) .short('P') .long(OPT_PRESERVE_CONTEXT) - .help("(unimplemented) preserve security context") + .help("preserve security context") .action(ArgAction::SetTrue), ) .arg( - // TODO implement flag Arg::new(OPT_CONTEXT) .short('Z') .long(OPT_CONTEXT) - .help("(unimplemented) set security context of files and directories") + .help("set security context of files and directories") .value_name("CONTEXT") - .action(ArgAction::SetTrue), + .value_parser(clap::value_parser!(String)) + .num_args(0..=1), ) .arg( Arg::new(ARG_FILES) @@ -319,25 +318,6 @@ pub fn uu_app() -> Command { ) } -/// Check for unimplemented command line arguments. -/// -/// Either return the degenerate Ok value, or an Err with string. -/// -/// # Errors -/// -/// Error datum is a string of the unimplemented argument. -/// -/// -fn check_unimplemented(matches: &ArgMatches) -> UResult<()> { - if matches.get_flag(OPT_PRESERVE_CONTEXT) { - Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into()) - } else if matches.get_flag(OPT_CONTEXT) { - Err(InstallError::Unimplemented(String::from("--context, -Z")).into()) - } else { - Ok(()) - } -} - /// Determine behavior, given command line arguments. /// /// If successful, returns a filled-out Behavior struct. @@ -415,6 +395,8 @@ fn behavior(matches: &ArgMatches) -> UResult { } }; + let context = matches.get_one::(OPT_CONTEXT).cloned(); + Ok(Behavior { main_function, specified_mode, @@ -435,6 +417,8 @@ fn behavior(matches: &ArgMatches) -> UResult { create_leading: matches.get_flag(OPT_CREATE_LEADING), target_dir, no_target_dir, + preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT), + context, }) } @@ -485,6 +469,10 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> { } show_if_err!(chown_optional_user_group(path, b)); + + // Set SELinux context for directory if needed + #[cfg(feature = "selinux")] + show_if_err!(set_selinux_context(path, b)); } // If the exit code was set, or show! has been called at least once // (which sets the exit code as well), function execution will end after @@ -941,6 +929,14 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { preserve_timestamps(from, to)?; } + #[cfg(feature = "selinux")] + if b.preserve_context { + uucore::selinux::preserve_security_context(from, to) + .map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?; + } else if b.context.is_some() { + set_selinux_context(to, b)?; + } + if b.verbose { print!("{} -> {}", from.quote(), to.quote()); match backup_path { @@ -1012,6 +1008,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { return Ok(true); } + #[cfg(feature = "selinux")] + if b.preserve_context && contexts_differ(from, to) { + 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. @@ -1042,3 +1043,13 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { Ok(false) } + +#[cfg(feature = "selinux")] +fn set_selinux_context(path: &Path, behavior: &Behavior) -> UResult<()> { + if !behavior.preserve_context && behavior.context.is_some() { + // Use the provided context set by -Z/--context + set_selinux_security_context(path, behavior.context.as_ref()) + .map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?; + } + Ok(()) +} diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index fdb66639fa..c402f35378 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) helloworld nodir objdump n'source +// spell-checker:ignore (words) helloworld nodir objdump n'source nconfined #[cfg(not(target_os = "openbsd"))] use filetime::FileTime; @@ -70,24 +70,6 @@ fn test_install_failing_not_dir() { .stderr_contains("not a directory"); } -#[test] -fn test_install_unimplemented_arg() { - let (at, mut ucmd) = at_and_ucmd!(); - let dir = "target_dir"; - let file = "source_file"; - let context_arg = "--context"; - - at.touch(file); - at.mkdir(dir); - ucmd.arg(context_arg) - .arg(file) - .arg(dir) - .fails() - .stderr_contains("Unimplemented"); - - assert!(!at.file_exists(format!("{dir}/{file}"))); -} - #[test] fn test_install_ancestors_directories() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1964,3 +1946,74 @@ fn test_install_no_target_basic() { assert!(at.file_exists(file)); assert!(at.file_exists(format!("{dir}/{file}"))); } + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_selinux() { + use std::process::Command; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "orig"; + at.touch(src); + + let dest = "orig.2"; + + let args = ["-Z", "--context=unconfined_u:object_r:user_tmp_t:s0"]; + for arg in args { + new_ucmd!() + .arg(arg) + .arg("-v") + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) + .succeeds() + .stdout_contains("orig' -> '"); + + let getfattr_output = Command::new("getfattr") + .arg(at.plus_as_string(dest)) + .arg("-n") + .arg("security.selinux") + .output() + .expect("Failed to run `getfattr` on the destination file"); + println!("{:?}", getfattr_output); + assert!( + getfattr_output.status.success(), + "getfattr did not run successfully: {}", + String::from_utf8_lossy(&getfattr_output.stderr) + ); + + let stdout = String::from_utf8_lossy(&getfattr_output.stdout); + assert!( + stdout.contains("unconfined_u"), + "Expected 'foo' not found in getfattr output:\n{stdout}" + ); + at.remove(&at.plus_as_string(dest)); + } +} + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_selinux_invalid_args() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "orig"; + at.touch(src); + let dest = "orig.2"; + + let args = [ + "--context=a", + "--context=unconfined_u:object_r:user_tmp_t:s0:a", + "--context=nconfined_u:object_r:user_tmp_t:s0", + ]; + for arg in args { + new_ucmd!() + .arg(arg) + .arg("-v") + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) + .fails() + .stderr_contains("failed to set default file creation"); + + at.remove(&at.plus_as_string(dest)); + } +}