1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-27 19:17:43 +00:00

Merge pull request #7635 from sylvestre/temp-selinux-impl

mkdir: add the selinux support
This commit is contained in:
Daniel Hofstetter 2025-04-13 14:49:52 +02:00 committed by GitHub
commit 80b6a2155c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 315 additions and 23 deletions

View file

@ -1188,7 +1188,7 @@ jobs:
- run: rsync -v -a -e ssh . lima-default:~/work/
- name: Setup Rust and other build deps in VM
run: |
lima sudo dnf install gcc g++ git rustup libselinux-devel clang-devel -y
lima sudo dnf install gcc g++ git rustup libselinux-devel clang-devel attr -y
lima rustup-init -y --default-toolchain stable
- name: Verify SELinux Status
run: |

1
Cargo.lock generated
View file

@ -3585,6 +3585,7 @@ dependencies = [
"number_prefix",
"os_display",
"regex",
"selinux",
"sha1",
"sha2",
"sha3",

View file

@ -50,6 +50,7 @@ feat_selinux = [
"cp/selinux",
"id/selinux",
"ls/selinux",
"mkdir/selinux",
"selinux",
"feat_require_selinux",
]

View file

@ -21,6 +21,8 @@ path = "src/mkdir.rs"
clap = { workspace = true }
uucore = { workspace = true, features = ["fs", "mode", "fsxattr"] }
[features]
selinux = ["uucore/selinux"]
[[bin]]
name = "mkdir"

View file

@ -29,6 +29,26 @@ mod options {
pub const PARENTS: &str = "parents";
pub const VERBOSE: &str = "verbose";
pub const DIRS: &str = "dirs";
pub const SELINUX: &str = "z";
pub const CONTEXT: &str = "context";
}
/// Configuration for directory creation.
pub struct Config<'a> {
/// Create parent directories as needed.
pub recursive: bool,
/// File permissions (octal).
pub mode: u32,
/// Print message for each created directory.
pub verbose: bool,
/// Set SELinux security context.
pub set_selinux_context: bool,
/// Specific SELinux context.
pub context: Option<&'a String>,
}
#[cfg(windows)]
@ -91,8 +111,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let verbose = matches.get_flag(options::VERBOSE);
let recursive = matches.get_flag(options::PARENTS);
// Extract the SELinux related flags and options
let set_selinux_context = matches.get_flag(options::SELINUX);
let context = matches.get_one::<String>(options::CONTEXT);
match get_mode(&matches, mode_had_minus_prefix) {
Ok(mode) => exec(dirs, recursive, mode, verbose),
Ok(mode) => {
let config = Config {
recursive,
mode,
verbose,
set_selinux_context: set_selinux_context || context.is_some(),
context,
};
exec(dirs, &config)
}
Err(f) => Err(USimpleError::new(1, f)),
}
}
@ -124,6 +157,15 @@ pub fn uu_app() -> Command {
.help("print a message for each printed directory")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SELINUX)
.short('Z')
.help("set SELinux security context of each created directory to the default type")
.action(ArgAction::SetTrue),
)
.arg(Arg::new(options::CONTEXT).long(options::CONTEXT).value_name("CTX").help(
"like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX",
))
.arg(
Arg::new(options::DIRS)
.action(ArgAction::Append)
@ -137,12 +179,12 @@ pub fn uu_app() -> Command {
/**
* Create the list of new directories
*/
fn exec(dirs: ValuesRef<OsString>, recursive: bool, mode: u32, verbose: bool) -> UResult<()> {
fn exec(dirs: ValuesRef<OsString>, config: &Config) -> UResult<()> {
for dir in dirs {
let path_buf = PathBuf::from(dir);
let path = path_buf.as_path();
show_if_err!(mkdir(path, recursive, mode, verbose));
show_if_err!(mkdir(path, config));
}
Ok(())
}
@ -160,7 +202,7 @@ fn exec(dirs: ValuesRef<OsString>, recursive: bool, mode: u32, verbose: bool) ->
///
/// To match the GNU behavior, a path with the last directory being a single dot
/// (like `some/path/to/.`) is created (with the dot stripped).
pub fn mkdir(path: &Path, recursive: bool, mode: u32, verbose: bool) -> UResult<()> {
pub fn mkdir(path: &Path, config: &Config) -> UResult<()> {
if path.as_os_str().is_empty() {
return Err(USimpleError::new(
1,
@ -173,7 +215,7 @@ pub fn mkdir(path: &Path, recursive: bool, mode: u32, verbose: bool) -> UResult<
// std::fs::create_dir("foo/."); fails in pure Rust
let path_buf = dir_strip_dot_for_creation(path);
let path = path_buf.as_path();
create_dir(path, recursive, verbose, false, mode)
create_dir(path, false, config)
}
#[cfg(any(unix, target_os = "redox"))]
@ -194,15 +236,9 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> {
// Return true if the directory at `path` has been created by this call.
// `is_parent` argument is not used on windows
#[allow(unused_variables)]
fn create_dir(
path: &Path,
recursive: bool,
verbose: bool,
is_parent: bool,
mode: u32,
) -> UResult<()> {
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
let path_exists = path.exists();
if path_exists && !recursive {
if path_exists && !config.recursive {
return Err(USimpleError::new(
1,
format!("{}: File exists", path.display()),
@ -212,9 +248,9 @@ fn create_dir(
return Ok(());
}
if recursive {
if config.recursive {
match path.parent() {
Some(p) => create_dir(p, recursive, verbose, true, mode)?,
Some(p) => create_dir(p, true, config)?,
None => {
USimpleError::new(1, "failed to create whole tree");
}
@ -223,7 +259,7 @@ fn create_dir(
match std::fs::create_dir(path) {
Ok(()) => {
if verbose {
if config.verbose {
println!(
"{}: created directory {}",
uucore::util_name(),
@ -233,7 +269,7 @@ fn create_dir(
#[cfg(all(unix, target_os = "linux"))]
let new_mode = if path_exists {
mode
config.mode
} else {
// TODO: Make this macos and freebsd compatible by creating a function to get permission bits from
// acl in extended attributes
@ -242,19 +278,33 @@ fn create_dir(
if is_parent {
(!mode::get_umask() & 0o777) | 0o300 | acl_perm_bits
} else {
mode | acl_perm_bits
config.mode | acl_perm_bits
}
};
#[cfg(all(unix, not(target_os = "linux")))]
let new_mode = if is_parent {
(!mode::get_umask() & 0o777) | 0o300
} else {
mode
config.mode
};
#[cfg(windows)]
let new_mode = mode;
let new_mode = config.mode;
chmod(path, new_mode)?;
// Apply SELinux context if requested
#[cfg(feature = "selinux")]
if config.set_selinux_context && uucore::selinux::check_selinux_enabled().is_ok() {
if let Err(e) = uucore::selinux::set_selinux_security_context(path, config.context)
{
let _ = std::fs::remove_dir(path);
return Err(USimpleError::new(
1,
format!("failed to set SELinux security context: {}", e),
));
}
}
Ok(())
}

View file

@ -58,6 +58,7 @@ crc32fast = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
bigdecimal = { workspace = true, optional = true }
num-traits = { workspace = true, optional = true }
selinux = { workspace = true, optional = true }
[target.'cfg(unix)'.dependencies]
walkdir = { workspace = true, optional = true }
@ -105,13 +106,14 @@ format = [
mode = ["libc"]
perms = ["entries", "libc", "walkdir"]
buf-copy = []
parser = ["extendedbigdecimal", "glob", "num-traits"]
pipes = []
process = ["libc"]
proc-info = ["tty", "walkdir"]
quoting-style = []
ranges = []
ringbuffer = []
parser = ["extendedbigdecimal", "glob", "num-traits"]
selinux = ["dep:selinux"]
signals = []
sum = [
"digest",

View file

@ -66,6 +66,8 @@ pub mod tty;
#[cfg(all(unix, feature = "fsxattr"))]
pub mod fsxattr;
#[cfg(all(target_os = "linux", feature = "selinux"))]
pub mod selinux;
#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))]
pub mod signals;
#[cfg(all(

View file

@ -0,0 +1,174 @@
// This file is part of the uutils uucore package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::path::Path;
use selinux::SecurityContext;
#[derive(Debug)]
pub enum Error {
SELinuxNotEnabled,
}
/// Checks if SELinux is enabled on the system.
///
/// This function verifies whether the kernel has SELinux support enabled.
///
/// # Returns
///
/// * `Ok(())` - If SELinux is enabled on the system.
/// * `Err(Error::SELinuxNotEnabled)` - If SELinux is not enabled.
///
/// # Examples
///
/// ```
/// use uucore::selinux::check_selinux_enabled;
///
/// match check_selinux_enabled() {
/// Ok(_) => println!("SELinux is enabled"),
/// Err(_) => println!("SELinux is not enabled"),
/// }
/// ```
pub fn check_selinux_enabled() -> Result<(), Error> {
if selinux::kernel_support() == selinux::KernelSupport::Unsupported {
Err(Error::SELinuxNotEnabled)
} else {
Ok(())
}
}
/// Sets the SELinux security context for the given filesystem path.
///
/// If a specific context is provided, it attempts to set this context explicitly.
/// Otherwise, it applies the default SELinux context for the provided path.
///
/// # Arguments
///
/// * `path` - Filesystem path on which to set the SELinux context.
/// * `context` - Optional SELinux context string to explicitly set.
///
/// # Errors
///
/// Returns an error if:
/// - SELinux is not enabled on the system.
/// - The provided context is invalid or cannot be applied.
/// - The default SELinux context cannot be set.
///
/// # Examples
///
/// Setting default context:
/// ```
/// use std::path::Path;
/// use uucore::selinux::set_selinux_security_context;
///
/// // Set the default SELinux context for a file
/// let result = set_selinux_security_context(Path::new("/path/to/file"), None);
/// if let Err(err) = result {
/// eprintln!("Failed to set default context: {}", err);
/// }
/// ```
///
/// Setting specific context:
/// ```
/// use std::path::Path;
/// use uucore::selinux::set_selinux_security_context;
///
/// // Set a specific SELinux context for a file
/// let context = String::from("unconfined_u:object_r:user_home_t:s0");
/// let result = set_selinux_security_context(Path::new("/path/to/file"), Some(&context));
/// if let Err(err) = result {
/// eprintln!("Failed to set context: {}", err);
/// }
/// ```
pub fn set_selinux_security_context(path: &Path, context: Option<&String>) -> Result<(), String> {
// Check if SELinux is enabled on the system
check_selinux_enabled().map_err(|e| format!("{:?}", e))?;
if let Some(ctx_str) = context {
// Create a CString from the provided context string
let c_context = std::ffi::CString::new(ctx_str.as_str())
.map_err(|_| "Invalid context string (contains null bytes)".to_string())?;
// Convert the CString into an SELinux security context
let security_context = selinux::OpaqueSecurityContext::from_c_str(&c_context)
.map_err(|e| format!("Failed to create security context: {}", e))?;
// Set the provided security context on the specified path
SecurityContext::from_c_str(
&security_context.to_c_string().map_err(|e| e.to_string())?,
false,
)
.set_for_path(path, false, false)
.map_err(|e| format!("Failed to set context: {}", e))
} else {
// If no context provided, set the default SELinux context for the path
SecurityContext::set_default_for_path(path)
.map_err(|e| format!("Failed to set default context: {}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_selinux_context_setting() {
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
let path = tmpfile.path();
let result = set_selinux_security_context(path, None);
if result.is_ok() {
// SELinux enabled and successfully set default context
assert!(true, "Successfully set SELinux context");
} else {
let err = result.unwrap_err();
let valid_errors = [
"SELinux is not enabled on this system",
&format!(
"Failed to set default context: selinux_lsetfilecon_default() failed on path '{}'",
path.display()
),
];
assert!(
valid_errors.contains(&err.as_str()),
"Unexpected error message: {}",
err
);
}
}
#[test]
fn test_invalid_context_string_error() {
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
let path = tmpfile.path();
// Pass a context string containing a null byte to trigger CString::new error
let invalid_context = String::from("invalid\0context");
let result = set_selinux_security_context(path, Some(&invalid_context));
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid context string (contains null bytes)"
);
}
#[test]
fn test_check_selinux_enabled_runtime_behavior() {
let result = check_selinux_enabled();
match selinux::kernel_support() {
selinux::KernelSupport::Unsupported => {
assert!(matches!(result, Err(Error::SELinuxNotEnabled)));
}
_ => {
assert!(result.is_ok(), "Expected Ok(()) when SELinux is supported");
}
}
}
}

View file

@ -103,6 +103,9 @@ pub use crate::features::fsext;
#[cfg(all(unix, feature = "fsxattr"))]
pub use crate::features::fsxattr;
#[cfg(all(target_os = "linux", feature = "selinux"))]
pub use crate::features::selinux;
//## core functions
#[cfg(unix)]

View file

@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore bindgen
// spell-checker:ignore bindgen getfattr testtest
#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
@ -358,3 +358,60 @@ fn test_empty_argument() {
.fails()
.stderr_only("mkdir: cannot create directory '': No such file or directory\n");
}
#[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";
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(dest))
.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");
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{}",
"unconfined_u",
stdout
);
at.rmdir(dest);
}
}
#[test]
#[cfg(feature = "feat_selinux")]
fn test_selinux_invalid() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let dest = "test_dir_a";
new_ucmd!()
.arg("--context=testtest")
.arg(at.plus_as_string(dest))
.fails()
.no_stdout()
.stderr_contains("failed to set SELinux security context:");
// invalid context, so, no directory
assert!(!at.dir_exists(dest));
}