From ad8fdbdb7759e63c64e0a4f5adfb0b06f211ea00 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 11 Jul 2025 09:41:48 +0900 Subject: [PATCH 1/2] mv: implement selinux options (-Z/--context) --- src/uu/mv/Cargo.toml | 3 ++ src/uu/mv/locales/en-US.ftl | 2 + src/uu/mv/src/mv.rs | 40 +++++++++++++++++ tests/by-util/test_mv.rs | 85 +++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 995862609..329bb78ba 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -41,6 +41,9 @@ windows-sys = { workspace = true, features = [ [target.'cfg(unix)'.dependencies] libc = { workspace = true } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "mv" path = "src/main.rs" diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl index 07d1155b8..9f3f5fd08 100644 --- a/src/uu/mv/locales/en-US.ftl +++ b/src/uu/mv/locales/en-US.ftl @@ -48,6 +48,8 @@ mv-help-verbose = explain what is being done mv-help-progress = Display a progress bar. Note: this feature is not supported by GNU coreutils. mv-help-debug = explain how a file is copied. Implies -v +mv-help-selinux = set SELinux security context of destination file to default type +mv-help-context = like -Z, or if CTX is specified then set the SELinux security context to CTX # Verbose messages mv-verbose-renamed = renamed {$from} -> {$to} diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 6576d9717..bcc8de2c3 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -44,6 +44,8 @@ use uucore::fs::{ }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; +#[cfg(feature = "selinux")] +use uucore::selinux::set_selinux_security_context; use uucore::translate; use uucore::update_control; @@ -99,6 +101,9 @@ pub struct Options { /// `--debug` pub debug: bool, + + /// `-Z, --context` + pub context: Option, } impl Default for Options { @@ -114,6 +119,7 @@ impl Default for Options { strip_slashes: false, progress_bar: false, debug: false, + context: None, } } } @@ -140,6 +146,8 @@ static OPT_VERBOSE: &str = "verbose"; static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; static OPT_DEBUG: &str = "debug"; +static OPT_CONTEXT: &str = "context"; +static OPT_SELINUX: &str = "selinux"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -189,6 +197,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } + // Handle -Z and --context options + // If -Z is used, use the default context (empty string) + // If --context=value is used, use that specific value + let context = if matches.get_flag(OPT_SELINUX) { + Some(String::new()) + } else { + matches.get_one::(OPT_CONTEXT).cloned() + }; + let opts = Options { overwrite: overwrite_mode, backup: backup_mode, @@ -200,6 +217,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), progress_bar: matches.get_flag(OPT_PROGRESS), debug: matches.get_flag(OPT_DEBUG), + context, }; mv(&files[..], &opts) @@ -282,6 +300,22 @@ pub fn uu_app() -> Command { .help(translate!("mv-help-progress")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_SELINUX) + .short('Z') + .help(translate!("mv-help-selinux")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(OPT_CONTEXT) + .long(OPT_CONTEXT) + .value_name("CTX") + .value_parser(clap::value_parser!(String)) + .help(translate!("mv-help-context")) + .num_args(0..=1) + .require_equals(true) + .default_missing_value(""), + ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) @@ -733,6 +767,12 @@ fn rename( rename_with_fallback(from, to, multi_progress, None, None)?; } + #[cfg(feature = "selinux")] + if let Some(ref context) = opts.context { + set_selinux_security_context(to, Some(context)) + .map_err(|e| io::Error::other(e.to_string()))?; + } + if opts.verbose { let message = match backup_path { Some(path) => { diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 62c12c1d2..00cba71b4 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -10,6 +10,11 @@ use rstest::rstest; use std::io::Write; #[cfg(not(windows))] use std::path::Path; +<<<<<<< HEAD +======= +#[cfg(feature = "selinux")] +use uucore::selinux::get_getfattr_output; +>>>>>>> 947680a6d (fix build) use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::{at_and_ucmd, util_name}; @@ -2488,3 +2493,83 @@ fn test_mv_cross_device_permission_denied() { set_permissions(other_fs_tempdir.path(), PermissionsExt::from_mode(0o755)) .expect("Unable to restore directory permissions"); } + +#[test] +#[cfg(feature = "selinux")] +fn test_mv_selinux_context() { + use std::process::Command; + + let test_cases = [ + ("-Z", None), + ( + "--context=unconfined_u:object_r:user_tmp_t:s0", + Some("unconfined_u"), + ), + ]; + + for (arg, expected_context) in test_cases { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "source.txt"; + let dest = "dest.txt"; + + at.touch(&src); + + for (arg, context_value, expected_context) in test_cases { + + let mut cmd = scene.ucmd(); + cmd.arg(arg); + + let result = cmd + .arg(at.plus_as_string(&src)) + .arg(at.plus_as_string(&dest)) + .run(); + + // Skip test if SELinux is not enabled + if result + .stderr_str() + .contains("SELinux is not enabled on this system") + { + println!("Skipping SELinux test: SELinux is not enabled"); + return; + } + + result.success(); + assert!(at.file_exists(&dest)); + assert!(!at.file_exists(&src)); + + // Verify SELinux context was set using getfattr + let getfattr_output = Command::new("getfattr") + .arg(at.plus_as_string(&dest)) + .arg("-n") + .arg("security.selinux") + .output(); + + if let Ok(output) = getfattr_output { + let selinux_context = String::from_utf8_lossy(&output.stdout); + if !selinux_context.is_empty() { + match expected_context { + Some(expected) => { + let context_value = + selinux_context.split('"').nth(1).unwrap_or("").to_string(); + assert!( + context_value.contains(expected), + "Expected context to contain '{}', got: {}", + expected, + context_value + ); + } + None => { + if selinux_context.contains("security.selinux") { + println!("SELinux context successfully set with {} flag", arg); + } + } + } + } + } + + // Clean up files + let _ = std::fs::remove_file(at.plus_as_string(&dest)); + let _ = std::fs::remove_file(at.plus_as_string(&src)); + } +} From 6e930966fb97ddc0ffce600847f7b9c9338cd250 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 11 Jul 2025 23:11:24 +0900 Subject: [PATCH 2/2] selinux: introduce get_getfattr_output to verify the output of getfatt --- src/uucore/src/lib/features/selinux.rs | 52 ++++++++++++++++++++++++ tests/by-util/test_cp.rs | 26 +----------- tests/by-util/test_install.rs | 28 +++++-------- tests/by-util/test_mkdir.rs | 23 +++-------- tests/by-util/test_mkfifo.rs | 22 +++------- tests/by-util/test_mknod.rs | 24 +++-------- tests/by-util/test_mv.rs | 56 +++++++------------------- 7 files changed, 93 insertions(+), 138 deletions(-) diff --git a/src/uucore/src/lib/features/selinux.rs b/src/uucore/src/lib/features/selinux.rs index 0b260dd75..939210ae8 100644 --- a/src/uucore/src/lib/features/selinux.rs +++ b/src/uucore/src/lib/features/selinux.rs @@ -328,6 +328,58 @@ pub fn preserve_security_context(from_path: &Path, to_path: &Path) -> Result<(), set_selinux_security_context(to_path, Some(&context)) } +/// Gets the SELinux security context for a file using getfattr. +/// +/// This function is primarily used for testing purposes to verify that SELinux +/// contexts have been properly set on files. It uses the `getfattr` command +/// to retrieve the security.selinux extended attribute. +/// +/// # Arguments +/// +/// * `f` - The file path as a string. +/// +/// # Returns +/// +/// Returns the SELinux context string extracted from the getfattr output. +/// If the context cannot be retrieved, the function will panic. +/// +/// # Panics +/// +/// This function will panic if: +/// - The `getfattr` command fails to execute +/// - The `getfattr` command returns a non-zero exit status +/// +/// # Examples +/// +/// ```no_run +/// use uucore::selinux::get_getfattr_output; +/// +/// let context = get_getfattr_output("/path/to/file"); +/// println!("SELinux context: {}", context); +/// ``` +pub fn get_getfattr_output(f: &str) -> String { + use std::process::Command; + + let getfattr_output = Command::new("getfattr") + .arg(f) + .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) + ); + + String::from_utf8_lossy(&getfattr_output.stdout) + .split('"') + .nth(1) + .unwrap_or("") + .to_string() +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 8575d1959..7c6c14f48 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -6,6 +6,8 @@ // spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR outfile uufs xattrs // spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel use uucore::display::Quotable; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::util::TestScenario; use uutests::{at_and_ucmd, new_ucmd, path_concat, util_name}; @@ -6296,30 +6298,6 @@ fn test_cp_from_stream_permission() { assert_eq!(at.metadata(target).permissions().mode(), 0o100_777); } -#[cfg(feature = "feat_selinux")] -fn get_getfattr_output(f: &str) -> String { - use std::process::Command; - - let getfattr_output = Command::new("getfattr") - .arg(f) - .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) - ); - - String::from_utf8_lossy(&getfattr_output.stdout) - .split('"') - .nth(1) - .unwrap_or("") - .to_string() -} - #[test] #[cfg(feature = "feat_selinux")] fn test_cp_selinux() { diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 436e5a424..900e88a32 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -13,6 +13,8 @@ use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; use uucore::process::{getegid, geteuid}; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::{TestScenario, is_ci, run_ucmd_as_root}; @@ -2235,8 +2237,6 @@ fn test_install_no_target_basic() { #[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"; @@ -2265,29 +2265,19 @@ fn test_selinux() { result.success().stdout_contains("orig' -> '"); - let getfattr_output = Command::new("getfattr") - .arg(at.plus_as_string(dest)) - .arg("-n") - .arg("security.selinux") - .output(); + // Try to get SELinux context, skip test if getfattr is not available + let context_value = + std::panic::catch_unwind(|| get_getfattr_output(&at.plus_as_string(dest))); - // Skip test if getfattr is not available - let Ok(getfattr_output) = getfattr_output else { - println!("Skipping SELinux test: getfattr not available"); + let Ok(context_value) = context_value else { + println!("Skipping SELinux test: getfattr not available or failed"); at.remove(&at.plus_as_string(dest)); continue; }; - 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}" + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index e6bd6b2c1..1c734449f 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -11,6 +11,8 @@ use libc::mode_t; #[cfg(not(windows))] use std::os::unix::fs::PermissionsExt; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; #[cfg(not(windows))] use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -390,8 +392,6 @@ fn test_empty_argument() { #[test] #[cfg(feature = "feat_selinux")] fn test_selinux() { - use std::process::Command; - let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let dest = "test_dir_a"; @@ -404,25 +404,12 @@ fn test_selinux() { .succeeds() .stdout_contains("created directory"); - 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"); - + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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"), + context_value.contains("unconfined_u"), "Expected '{}' not found in getfattr output:\n{}", "unconfined_u", - stdout + context_value ); at.rmdir(dest); } diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 6bc8f3dd4..7ed97b3dd 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -5,6 +5,8 @@ // spell-checker:ignore nconfined +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; @@ -108,7 +110,6 @@ fn test_create_fifo_with_umask() { #[test] #[cfg(feature = "feat_selinux")] fn test_mkfifo_selinux() { - use std::process::Command; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dest = "test_file"; @@ -121,23 +122,10 @@ fn test_mkfifo_selinux() { ts.ucmd().arg(arg).arg(dest).succeeds(); assert!(at.is_fifo("test_file")); - 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:?}"); + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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}" + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index 1c558dfd3..34136b828 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -7,6 +7,8 @@ use std::os::unix::fs::PermissionsExt; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util::run_ucmd_as_root; @@ -155,7 +157,6 @@ fn test_mknod_mode_permissions() { #[test] #[cfg(feature = "feat_selinux")] fn test_mknod_selinux() { - use std::process::Command; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dest = "test_file"; @@ -175,25 +176,10 @@ fn test_mknod_selinux() { assert!(ts.fixtures.is_fifo("test_file")); assert!(ts.fixtures.metadata("test_file").permissions().readonly()); - 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:?}"); + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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 '{}' not found in getfattr output:\n{}", - "foo", - stdout + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 00cba71b4..8da94e864 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -10,11 +10,8 @@ use rstest::rstest; use std::io::Write; #[cfg(not(windows))] use std::path::Path; -<<<<<<< HEAD -======= -#[cfg(feature = "selinux")] +#[cfg(feature = "feat_selinux")] use uucore::selinux::get_getfattr_output; ->>>>>>> 947680a6d (fix build) use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::{at_and_ucmd, util_name}; @@ -2497,8 +2494,6 @@ fn test_mv_cross_device_permission_denied() { #[test] #[cfg(feature = "selinux")] fn test_mv_selinux_context() { - use std::process::Command; - let test_cases = [ ("-Z", None), ( @@ -2513,16 +2508,14 @@ fn test_mv_selinux_context() { let src = "source.txt"; let dest = "dest.txt"; - at.touch(&src); - - for (arg, context_value, expected_context) in test_cases { + at.touch(src); let mut cmd = scene.ucmd(); cmd.arg(arg); let result = cmd - .arg(at.plus_as_string(&src)) - .arg(at.plus_as_string(&dest)) + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) .run(); // Skip test if SELinux is not enabled @@ -2535,41 +2528,22 @@ fn test_mv_selinux_context() { } result.success(); - assert!(at.file_exists(&dest)); - assert!(!at.file_exists(&src)); + assert!(at.file_exists(dest)); + assert!(!at.file_exists(src)); // Verify SELinux context was set using getfattr - let getfattr_output = Command::new("getfattr") - .arg(at.plus_as_string(&dest)) - .arg("-n") - .arg("security.selinux") - .output(); - - if let Ok(output) = getfattr_output { - let selinux_context = String::from_utf8_lossy(&output.stdout); - if !selinux_context.is_empty() { - match expected_context { - Some(expected) => { - let context_value = - selinux_context.split('"').nth(1).unwrap_or("").to_string(); - assert!( - context_value.contains(expected), - "Expected context to contain '{}', got: {}", - expected, - context_value - ); - } - None => { - if selinux_context.contains("security.selinux") { - println!("SELinux context successfully set with {} flag", arg); - } - } - } + let context_value = get_getfattr_output(&at.plus_as_string(dest)); + if !context_value.is_empty() { + if let Some(expected) = expected_context { + assert!( + context_value.contains(expected), + "Expected context to contain '{expected}', got: {context_value}" + ); } } // Clean up files - let _ = std::fs::remove_file(at.plus_as_string(&dest)); - let _ = std::fs::remove_file(at.plus_as_string(&src)); + let _ = std::fs::remove_file(at.plus_as_string(dest)); + let _ = std::fs::remove_file(at.plus_as_string(src)); } }