From aebada4cd469783137a318e5e3f263c5f8cdbe4f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 21 Apr 2025 23:43:29 +0200 Subject: [PATCH 1/2] mknod: implement selinux support + improve the option management a bit --- Cargo.toml | 1 + src/uu/mknod/Cargo.toml | 3 + src/uu/mknod/src/mknod.rs | 106 +++++++++++++++++++++++++++++------- tests/by-util/test_mknod.rs | 77 ++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a4ce71cf..4ea6537dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ feat_selinux = [ "id/selinux", "ls/selinux", "mkdir/selinux", + "mknod/selinux", "stat/selinux", "selinux", "feat_require_selinux", diff --git a/src/uu/mknod/Cargo.toml b/src/uu/mknod/Cargo.toml index 415056d04..49f539eb8 100644 --- a/src/uu/mknod/Cargo.toml +++ b/src/uu/mknod/Cargo.toml @@ -23,6 +23,9 @@ clap = { workspace = true } libc = { workspace = true } uucore = { workspace = true, features = ["mode"] } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "mknod" path = "src/main.rs" diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index b93e42457..77d7f3617 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO -use clap::{Arg, ArgMatches, Command, value_parser}; +use clap::{Arg, ArgAction, Command, value_parser}; use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR}; use libc::{dev_t, mode_t}; use std::ffi::CString; @@ -20,6 +20,15 @@ const AFTER_HELP: &str = help_section!("after help", "mknod.md"); const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; +mod options { + pub const MODE: &str = "mode"; + pub const TYPE: &str = "type"; + pub const MAJOR: &str = "major"; + pub const MINOR: &str = "minor"; + pub const SELINUX: &str = "z"; + pub const CONTEXT: &str = "context"; +} + #[inline(always)] fn makedev(maj: u64, min: u64) -> dev_t { // pick up from @@ -33,17 +42,30 @@ enum FileType { Fifo, } -fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 { +/// Configuration for directory creation. +pub struct Config<'a> { + pub mode: mode_t, + + pub dev: dev_t, + + /// Set SELinux security context. + pub set_selinux_context: bool, + + /// Specific SELinux context. + pub context: Option<&'a String>, +} + +fn _mknod(file_name: &str, config: Config) -> i32 { let c_str = CString::new(file_name).expect("Failed to convert to CString"); // the user supplied a mode - let set_umask = mode & MODE_RW_UGO != MODE_RW_UGO; + let set_umask = config.mode & MODE_RW_UGO != MODE_RW_UGO; unsafe { // store prev umask let last_umask = if set_umask { libc::umask(0) } else { 0 }; - let errno = libc::mknod(c_str.as_ptr(), mode, dev); + let errno = libc::mknod(c_str.as_ptr(), config.mode, config.dev); // set umask back to original value if set_umask { @@ -56,16 +78,27 @@ fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 { // shows the error from the mknod syscall libc::perror(c_str.as_ptr()); } + + // Apply SELinux context if requested + #[cfg(feature = "selinux")] + if config.set_selinux_context { + if let Err(e) = uucore::selinux::set_selinux_security_context( + std::path::Path::new(file_name), + config.context, + ) { + // if it fails, delete the file + let _ = std::fs::remove_dir(file_name); + eprintln!("failed to set SELinux security context: {}", e); + return 1; + } + } + errno } } #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - // Linux-specific options, not implemented - // opts.optflag("Z", "", "set the SELinux security context to default type"); - // opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX"); - let matches = uu_app().try_get_matches_from(args)?; let mode = get_mode(&matches).map_err(|e| USimpleError::new(1, e))?; @@ -75,22 +108,33 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .expect("Missing argument 'NAME'"); let file_type = matches.get_one::("type").unwrap(); + // Extract the SELinux related flags and options + let set_selinux_context = matches.get_flag(options::SELINUX); + let context = matches.get_one::(options::CONTEXT); + + let mut config = Config { + mode, + dev: 0, + set_selinux_context: set_selinux_context || context.is_some(), + context, + }; if *file_type == FileType::Fifo { - if matches.contains_id("major") || matches.contains_id("minor") { + if matches.contains_id(options::MAJOR) || matches.contains_id(options::MINOR) { Err(UUsageError::new( 1, "Fifos do not have major and minor device numbers.", )) } else { - let exit_code = _mknod(file_name, S_IFIFO | mode, 0); + config.mode = S_IFIFO | mode; + let exit_code = _mknod(file_name, config); set_exit_code(exit_code); Ok(()) } } else { match ( - matches.get_one::("major"), - matches.get_one::("minor"), + matches.get_one::(options::MAJOR), + matches.get_one::(options::MINOR), ) { (_, None) | (None, _) => Err(UUsageError::new( 1, @@ -98,9 +142,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )), (Some(&major), Some(&minor)) => { let dev = makedev(major, minor); + config.dev = dev; let exit_code = match file_type { - FileType::Block => _mknod(file_name, S_IFBLK | mode, dev), - FileType::Character => _mknod(file_name, S_IFCHR | mode, dev), + FileType::Block => { + config.mode |= S_IFBLK; + _mknod(file_name, config) + } + FileType::Character => { + config.mode |= S_IFCHR; + _mknod(file_name, config) + } FileType::Fifo => { unreachable!("file_type was validated to be only block or character") } @@ -120,7 +171,7 @@ pub fn uu_app() -> Command { .about(ABOUT) .infer_long_args(true) .arg( - Arg::new("mode") + Arg::new(options::MODE) .short('m') .long("mode") .value_name("MODE") @@ -134,24 +185,39 @@ pub fn uu_app() -> Command { .value_hint(clap::ValueHint::AnyPath), ) .arg( - Arg::new("type") + Arg::new(options::TYPE) .value_name("TYPE") .help("type of the new file (b, c, u or p)") .required(true) .value_parser(parse_type), ) .arg( - Arg::new("major") - .value_name("MAJOR") + Arg::new(options::MAJOR) + .value_name(options::MAJOR) .help("major file type") .value_parser(value_parser!(u64)), ) .arg( - Arg::new("minor") - .value_name("MINOR") + Arg::new(options::MINOR) + .value_name(options::MINOR) .help("minor file type") .value_parser(value_parser!(u64)), ) + .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") + .value_parser(value_parser!(String)) + .num_args(0..=1) + .require_equals(true) + .help("like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX") + ) } fn get_mode(matches: &ArgMatches) -> Result { diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index 5dd154c07..dbc0e63ba 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -2,6 +2,9 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +// spell-checker:ignore getfattr nconfined + use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; @@ -121,3 +124,77 @@ fn test_mknod_invalid_mode() { .code_is(1) .stderr_contains("invalid mode"); } + +#[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"; + let args = [ + "-Z", + "--context", + "--context=unconfined_u:object_r:user_tmp_t:s0", + ]; + for arg in args { + ts.ucmd() + .arg(arg) + .arg("-m") + .arg("a=r") + .arg(dest) + .arg("p") + .succeeds(); + 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); + 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 + ); + at.remove(&at.plus_as_string(dest)); + } +} + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_mknod_selinux_invalid() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let dest = "orig"; + + 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("-m") + .arg("a=r") + .arg(dest) + .arg("p") + .fails() + .stderr_contains("Failed to"); + if at.file_exists(dest) { + at.remove(dest); + } + } +} From c9babe4e6f5d2509c00d9c2b33ab281f57590dc9 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Mon, 21 Apr 2025 23:47:08 +0200 Subject: [PATCH 2/2] mknod: pass a string instead of matches --- src/uu/mknod/src/mknod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index 77d7f3617..561fe1acc 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -101,7 +101,7 @@ fn _mknod(file_name: &str, config: Config) -> i32 { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; - let mode = get_mode(&matches).map_err(|e| USimpleError::new(1, e))?; + let mode = get_mode(matches.get_one::("mode")).map_err(|e| USimpleError::new(1, e))?; let file_name = matches .get_one::("name") @@ -220,8 +220,8 @@ pub fn uu_app() -> Command { ) } -fn get_mode(matches: &ArgMatches) -> Result { - match matches.get_one::("mode") { +fn get_mode(str_mode: Option<&String>) -> Result { + match str_mode { None => Ok(MODE_RW_UGO), Some(str_mode) => uucore::mode::parse_mode(str_mode) .map_err(|e| format!("invalid mode ({e})"))