Merge pull request #8333 from sylvestre/selinux-mv

mv: add support for selinux and add minor refactor
This commit is contained in:
Daniel Hofstetter
2025-08-01 09:36:06 +02:00
committed by GitHub
10 changed files with 182 additions and 97 deletions
+3
View File
@@ -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"
+2
View File
@@ -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}
+40
View File
@@ -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<String>,
}
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::<String>(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) => {
+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 prwx doesnotexist
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};
@@ -6372,30 +6374,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));
}
+59
View File
@@ -10,6 +10,8 @@ use rstest::rstest;
use std::io::Write;
#[cfg(not(windows))]
use std::path::Path;
#[cfg(feature = "feat_selinux")]
use uucore::selinux::get_getfattr_output;
use uutests::new_ucmd;
use uutests::util::TestScenario;
use uutests::{at_and_ucmd, util_name};
@@ -2488,3 +2490,60 @@ 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() {
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);
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 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));
}
}