Skip to content

Commit aebada4

Browse files
committed
mknod: implement selinux support
+ improve the option management a bit
1 parent a423041 commit aebada4

File tree

4 files changed

+167
-20
lines changed

4 files changed

+167
-20
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ feat_selinux = [
5151
"id/selinux",
5252
"ls/selinux",
5353
"mkdir/selinux",
54+
"mknod/selinux",
5455
"stat/selinux",
5556
"selinux",
5657
"feat_require_selinux",

src/uu/mknod/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ clap = { workspace = true }
2323
libc = { workspace = true }
2424
uucore = { workspace = true, features = ["mode"] }
2525

26+
[features]
27+
selinux = ["uucore/selinux"]
28+
2629
[[bin]]
2730
name = "mknod"
2831
path = "src/main.rs"

src/uu/mknod/src/mknod.rs

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
// spell-checker:ignore (ToDO) parsemode makedev sysmacros perror IFBLK IFCHR IFIFO
77

8-
use clap::{Arg, ArgMatches, Command, value_parser};
8+
use clap::{Arg, ArgAction, Command, value_parser};
99
use libc::{S_IFBLK, S_IFCHR, S_IFIFO, S_IRGRP, S_IROTH, S_IRUSR, S_IWGRP, S_IWOTH, S_IWUSR};
1010
use libc::{dev_t, mode_t};
1111
use std::ffi::CString;
@@ -20,6 +20,15 @@ const AFTER_HELP: &str = help_section!("after help", "mknod.md");
2020

2121
const MODE_RW_UGO: mode_t = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
2222

23+
mod options {
24+
pub const MODE: &str = "mode";
25+
pub const TYPE: &str = "type";
26+
pub const MAJOR: &str = "major";
27+
pub const MINOR: &str = "minor";
28+
pub const SELINUX: &str = "z";
29+
pub const CONTEXT: &str = "context";
30+
}
31+
2332
#[inline(always)]
2433
fn makedev(maj: u64, min: u64) -> dev_t {
2534
// pick up from <sys/sysmacros.h>
@@ -33,17 +42,30 @@ enum FileType {
3342
Fifo,
3443
}
3544

36-
fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
45+
/// Configuration for directory creation.
46+
pub struct Config<'a> {
47+
pub mode: mode_t,
48+
49+
pub dev: dev_t,
50+
51+
/// Set SELinux security context.
52+
pub set_selinux_context: bool,
53+
54+
/// Specific SELinux context.
55+
pub context: Option<&'a String>,
56+
}
57+
58+
fn _mknod(file_name: &str, config: Config) -> i32 {
3759
let c_str = CString::new(file_name).expect("Failed to convert to CString");
3860

3961
// the user supplied a mode
40-
let set_umask = mode & MODE_RW_UGO != MODE_RW_UGO;
62+
let set_umask = config.mode & MODE_RW_UGO != MODE_RW_UGO;
4163

4264
unsafe {
4365
// store prev umask
4466
let last_umask = if set_umask { libc::umask(0) } else { 0 };
4567

46-
let errno = libc::mknod(c_str.as_ptr(), mode, dev);
68+
let errno = libc::mknod(c_str.as_ptr(), config.mode, config.dev);
4769

4870
// set umask back to original value
4971
if set_umask {
@@ -56,16 +78,27 @@ fn _mknod(file_name: &str, mode: mode_t, dev: dev_t) -> i32 {
5678
// shows the error from the mknod syscall
5779
libc::perror(c_str.as_ptr());
5880
}
81+
82+
// Apply SELinux context if requested
83+
#[cfg(feature = "selinux")]
84+
if config.set_selinux_context {
85+
if let Err(e) = uucore::selinux::set_selinux_security_context(
86+
std::path::Path::new(file_name),
87+
config.context,
88+
) {
89+
// if it fails, delete the file
90+
let _ = std::fs::remove_dir(file_name);
91+
eprintln!("failed to set SELinux security context: {}", e);
92+
return 1;
93+
}
94+
}
95+
5996
errno
6097
}
6198
}
6299

63100
#[uucore::main]
64101
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
65-
// Linux-specific options, not implemented
66-
// opts.optflag("Z", "", "set the SELinux security context to default type");
67-
// opts.optopt("", "context", "like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX");
68-
69102
let matches = uu_app().try_get_matches_from(args)?;
70103

71104
let mode = get_mode(&matches).map_err(|e| USimpleError::new(1, e))?;
@@ -75,32 +108,50 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
75108
.expect("Missing argument 'NAME'");
76109

77110
let file_type = matches.get_one::<FileType>("type").unwrap();
111+
// Extract the SELinux related flags and options
112+
let set_selinux_context = matches.get_flag(options::SELINUX);
113+
let context = matches.get_one::<String>(options::CONTEXT);
114+
115+
let mut config = Config {
116+
mode,
117+
dev: 0,
118+
set_selinux_context: set_selinux_context || context.is_some(),
119+
context,
120+
};
78121

79122
if *file_type == FileType::Fifo {
80-
if matches.contains_id("major") || matches.contains_id("minor") {
123+
if matches.contains_id(options::MAJOR) || matches.contains_id(options::MINOR) {
81124
Err(UUsageError::new(
82125
1,
83126
"Fifos do not have major and minor device numbers.",
84127
))
85128
} else {
86-
let exit_code = _mknod(file_name, S_IFIFO | mode, 0);
129+
config.mode = S_IFIFO | mode;
130+
let exit_code = _mknod(file_name, config);
87131
set_exit_code(exit_code);
88132
Ok(())
89133
}
90134
} else {
91135
match (
92-
matches.get_one::<u64>("major"),
93-
matches.get_one::<u64>("minor"),
136+
matches.get_one::<u64>(options::MAJOR),
137+
matches.get_one::<u64>(options::MINOR),
94138
) {
95139
(_, None) | (None, _) => Err(UUsageError::new(
96140
1,
97141
"Special files require major and minor device numbers.",
98142
)),
99143
(Some(&major), Some(&minor)) => {
100144
let dev = makedev(major, minor);
145+
config.dev = dev;
101146
let exit_code = match file_type {
102-
FileType::Block => _mknod(file_name, S_IFBLK | mode, dev),
103-
FileType::Character => _mknod(file_name, S_IFCHR | mode, dev),
147+
FileType::Block => {
148+
config.mode |= S_IFBLK;
149+
_mknod(file_name, config)
150+
}
151+
FileType::Character => {
152+
config.mode |= S_IFCHR;
153+
_mknod(file_name, config)
154+
}
104155
FileType::Fifo => {
105156
unreachable!("file_type was validated to be only block or character")
106157
}
@@ -120,7 +171,7 @@ pub fn uu_app() -> Command {
120171
.about(ABOUT)
121172
.infer_long_args(true)
122173
.arg(
123-
Arg::new("mode")
174+
Arg::new(options::MODE)
124175
.short('m')
125176
.long("mode")
126177
.value_name("MODE")
@@ -134,24 +185,39 @@ pub fn uu_app() -> Command {
134185
.value_hint(clap::ValueHint::AnyPath),
135186
)
136187
.arg(
137-
Arg::new("type")
188+
Arg::new(options::TYPE)
138189
.value_name("TYPE")
139190
.help("type of the new file (b, c, u or p)")
140191
.required(true)
141192
.value_parser(parse_type),
142193
)
143194
.arg(
144-
Arg::new("major")
145-
.value_name("MAJOR")
195+
Arg::new(options::MAJOR)
196+
.value_name(options::MAJOR)
146197
.help("major file type")
147198
.value_parser(value_parser!(u64)),
148199
)
149200
.arg(
150-
Arg::new("minor")
151-
.value_name("MINOR")
201+
Arg::new(options::MINOR)
202+
.value_name(options::MINOR)
152203
.help("minor file type")
153204
.value_parser(value_parser!(u64)),
154205
)
206+
.arg(
207+
Arg::new(options::SELINUX)
208+
.short('Z')
209+
.help("set SELinux security context of each created directory to the default type")
210+
.action(ArgAction::SetTrue),
211+
)
212+
.arg(
213+
Arg::new(options::CONTEXT)
214+
.long(options::CONTEXT)
215+
.value_name("CTX")
216+
.value_parser(value_parser!(String))
217+
.num_args(0..=1)
218+
.require_equals(true)
219+
.help("like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX")
220+
)
155221
}
156222

157223
fn get_mode(matches: &ArgMatches) -> Result<mode_t, String> {

tests/by-util/test_mknod.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
//
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
5+
6+
// spell-checker:ignore getfattr nconfined
7+
58
use uutests::new_ucmd;
69
use uutests::util::TestScenario;
710
use uutests::util_name;
@@ -121,3 +124,77 @@ fn test_mknod_invalid_mode() {
121124
.code_is(1)
122125
.stderr_contains("invalid mode");
123126
}
127+
128+
#[test]
129+
#[cfg(feature = "feat_selinux")]
130+
fn test_mknod_selinux() {
131+
use std::process::Command;
132+
let ts = TestScenario::new(util_name!());
133+
let at = &ts.fixtures;
134+
let dest = "test_file";
135+
let args = [
136+
"-Z",
137+
"--context",
138+
"--context=unconfined_u:object_r:user_tmp_t:s0",
139+
];
140+
for arg in args {
141+
ts.ucmd()
142+
.arg(arg)
143+
.arg("-m")
144+
.arg("a=r")
145+
.arg(dest)
146+
.arg("p")
147+
.succeeds();
148+
assert!(ts.fixtures.is_fifo("test_file"));
149+
assert!(ts.fixtures.metadata("test_file").permissions().readonly());
150+
151+
let getfattr_output = Command::new("getfattr")
152+
.arg(at.plus_as_string(dest))
153+
.arg("-n")
154+
.arg("security.selinux")
155+
.output()
156+
.expect("Failed to run `getfattr` on the destination file");
157+
println!("{:?}", getfattr_output);
158+
assert!(
159+
getfattr_output.status.success(),
160+
"getfattr did not run successfully: {}",
161+
String::from_utf8_lossy(&getfattr_output.stderr)
162+
);
163+
164+
let stdout = String::from_utf8_lossy(&getfattr_output.stdout);
165+
assert!(
166+
stdout.contains("unconfined_u"),
167+
"Expected '{}' not found in getfattr output:\n{}",
168+
"foo",
169+
stdout
170+
);
171+
at.remove(&at.plus_as_string(dest));
172+
}
173+
}
174+
175+
#[test]
176+
#[cfg(feature = "feat_selinux")]
177+
fn test_mknod_selinux_invalid() {
178+
let scene = TestScenario::new(util_name!());
179+
let at = &scene.fixtures;
180+
let dest = "orig";
181+
182+
let args = [
183+
"--context=a",
184+
"--context=unconfined_u:object_r:user_tmp_t:s0:a",
185+
"--context=nconfined_u:object_r:user_tmp_t:s0",
186+
];
187+
for arg in args {
188+
new_ucmd!()
189+
.arg(arg)
190+
.arg("-m")
191+
.arg("a=r")
192+
.arg(dest)
193+
.arg("p")
194+
.fails()
195+
.stderr_contains("Failed to");
196+
if at.file_exists(dest) {
197+
at.remove(dest);
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)