1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

Merge pull request #7818 from sylvestre/selinux-mknod

mknod: implement selinux support
This commit is contained in:
Daniel Hofstetter 2025-04-23 09:38:09 +02:00 committed by GitHub
commit 67400abd70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 170 additions and 23 deletions

View file

@ -51,6 +51,7 @@ feat_selinux = [
"id/selinux", "id/selinux",
"ls/selinux", "ls/selinux",
"mkdir/selinux", "mkdir/selinux",
"mknod/selinux",
"stat/selinux", "stat/selinux",
"selinux", "selinux",
"feat_require_selinux", "feat_require_selinux",

View file

@ -23,6 +23,9 @@ clap = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
uucore = { workspace = true, features = ["mode"] } uucore = { workspace = true, features = ["mode"] }
[features]
selinux = ["uucore/selinux"]
[[bin]] [[bin]]
name = "mknod" name = "mknod"
path = "src/main.rs" path = "src/main.rs"

View file

@ -5,7 +5,7 @@
// spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO // 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::{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 libc::{dev_t, mode_t};
use std::ffi::CString; 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; 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)] #[inline(always)]
fn makedev(maj: u64, min: u64) -> dev_t { fn makedev(maj: u64, min: u64) -> dev_t {
// pick up from <sys/sysmacros.h> // pick up from <sys/sysmacros.h>
@ -33,17 +42,30 @@ enum FileType {
Fifo, 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"); let c_str = CString::new(file_name).expect("Failed to convert to CString");
// the user supplied a mode // 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 { unsafe {
// store prev umask // store prev umask
let last_umask = if set_umask { libc::umask(0) } else { 0 }; 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 // set umask back to original value
if set_umask { if set_umask {
@ -56,41 +78,63 @@ fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
// shows the error from the mknod syscall // shows the error from the mknod syscall
libc::perror(c_str.as_ptr()); 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 errno
} }
} }
#[uucore::main] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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 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::<String>("mode")).map_err(|e| USimpleError::new(1, e))?;
let file_name = matches let file_name = matches
.get_one::<String>("name") .get_one::<String>("name")
.expect("Missing argument 'NAME'"); .expect("Missing argument 'NAME'");
let file_type = matches.get_one::<FileType>("type").unwrap(); let file_type = matches.get_one::<FileType>("type").unwrap();
// Extract the SELinux related flags and options
let set_selinux_context = matches.get_flag(options::SELINUX);
let context = matches.get_one::<String>(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 *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( Err(UUsageError::new(
1, 1,
"Fifos do not have major and minor device numbers.", "Fifos do not have major and minor device numbers.",
)) ))
} else { } 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); set_exit_code(exit_code);
Ok(()) Ok(())
} }
} else { } else {
match ( match (
matches.get_one::<u64>("major"), matches.get_one::<u64>(options::MAJOR),
matches.get_one::<u64>("minor"), matches.get_one::<u64>(options::MINOR),
) { ) {
(_, None) | (None, _) => Err(UUsageError::new( (_, None) | (None, _) => Err(UUsageError::new(
1, 1,
@ -98,9 +142,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
)), )),
(Some(&major), Some(&minor)) => { (Some(&major), Some(&minor)) => {
let dev = makedev(major, minor); let dev = makedev(major, minor);
config.dev = dev;
let exit_code = match file_type { let exit_code = match file_type {
FileType::Block => _mknod(file_name, S_IFBLK | mode, dev), FileType::Block => {
FileType::Character => _mknod(file_name, S_IFCHR | mode, dev), config.mode |= S_IFBLK;
_mknod(file_name, config)
}
FileType::Character => {
config.mode |= S_IFCHR;
_mknod(file_name, config)
}
FileType::Fifo => { FileType::Fifo => {
unreachable!("file_type was validated to be only block or character") unreachable!("file_type was validated to be only block or character")
} }
@ -120,7 +171,7 @@ pub fn uu_app() -> Command {
.about(ABOUT) .about(ABOUT)
.infer_long_args(true) .infer_long_args(true)
.arg( .arg(
Arg::new("mode") Arg::new(options::MODE)
.short('m') .short('m')
.long("mode") .long("mode")
.value_name("MODE") .value_name("MODE")
@ -134,28 +185,43 @@ pub fn uu_app() -> Command {
.value_hint(clap::ValueHint::AnyPath), .value_hint(clap::ValueHint::AnyPath),
) )
.arg( .arg(
Arg::new("type") Arg::new(options::TYPE)
.value_name("TYPE") .value_name("TYPE")
.help("type of the new file (b, c, u or p)") .help("type of the new file (b, c, u or p)")
.required(true) .required(true)
.value_parser(parse_type), .value_parser(parse_type),
) )
.arg( .arg(
Arg::new("major") Arg::new(options::MAJOR)
.value_name("MAJOR") .value_name(options::MAJOR)
.help("major file type") .help("major file type")
.value_parser(value_parser!(u64)), .value_parser(value_parser!(u64)),
) )
.arg( .arg(
Arg::new("minor") Arg::new(options::MINOR)
.value_name("MINOR") .value_name(options::MINOR)
.help("minor file type") .help("minor file type")
.value_parser(value_parser!(u64)), .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<mode_t, String> { fn get_mode(str_mode: Option<&String>) -> Result<mode_t, String> {
match matches.get_one::<String>("mode") { match str_mode {
None => Ok(MODE_RW_UGO), None => Ok(MODE_RW_UGO),
Some(str_mode) => uucore::mode::parse_mode(str_mode) Some(str_mode) => uucore::mode::parse_mode(str_mode)
.map_err(|e| format!("invalid mode ({e})")) .map_err(|e| format!("invalid mode ({e})"))

View file

@ -2,6 +2,9 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // file that was distributed with this source code.
// spell-checker:ignore getfattr nconfined
use uutests::new_ucmd; use uutests::new_ucmd;
use uutests::util::TestScenario; use uutests::util::TestScenario;
use uutests::util_name; use uutests::util_name;
@ -121,3 +124,77 @@ fn test_mknod_invalid_mode() {
.code_is(1) .code_is(1)
.stderr_contains("invalid mode"); .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);
}
}
}