Skip to content

mknod: implement selinux support #7818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ feat_selinux = [
"id/selinux",
"ls/selinux",
"mkdir/selinux",
"mknod/selinux",
"stat/selinux",
"selinux",
"feat_require_selinux",
Expand Down
3 changes: 3 additions & 0 deletions src/uu/mknod/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
112 changes: 89 additions & 23 deletions src/uu/mknod/src/mknod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <sys/sysmacros.h>
Expand All @@ -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 {
Expand All @@ -56,51 +78,80 @@ 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))?;
let mode = get_mode(matches.get_one::<String>("mode")).map_err(|e| USimpleError::new(1, e))?;

let file_name = matches
.get_one::<String>("name")
.expect("Missing argument 'NAME'");

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 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::<u64>("major"),
matches.get_one::<u64>("minor"),
matches.get_one::<u64>(options::MAJOR),
matches.get_one::<u64>(options::MINOR),
) {
(_, None) | (None, _) => Err(UUsageError::new(
1,
"Special files require major and minor device numbers.",
)),
(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")
}
Expand All @@ -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")
Expand All @@ -134,28 +185,43 @@ 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also have to specify require_equals, otherwise something like cargo run --features=unix,feat_selinux mknod --context hello_world p will fail.

Suggested change
.num_args(0..=1)
.num_args(0..=1)
.require_equals(true)

.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> {
match matches.get_one::<String>("mode") {
fn get_mode(str_mode: Option<&String>) -> Result<mode_t, String> {
match str_mode {
None => Ok(MODE_RW_UGO),
Some(str_mode) => uucore::mode::parse_mode(str_mode)
.map_err(|e| format!("invalid mode ({e})"))
Expand Down
77 changes: 77 additions & 0 deletions tests/by-util/test_mknod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Loading