selinux: introduce get_getfattr_output to verify the output of getfatt

This commit is contained in:
Sylvestre Ledru
2025-07-31 17:45:34 +02:00
parent ad8fdbdb77
commit 6e930966fb
7 changed files with 93 additions and 138 deletions
+52
View File
@@ -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::*;
+2 -24
View File
@@ -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() {
+9 -19
View File
@@ -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));
}
+5 -18
View File
@@ -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);
}
+5 -17
View File
@@ -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));
}
+5 -19
View File
@@ -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));
}
+15 -41
View File
@@ -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));
}
}