From 79bbf52a21a554faece46f3209406f23258ecc47 Mon Sep 17 00:00:00 2001 From: Jonas Jelten Date: Tue, 29 Jul 2025 11:53:21 +0200 Subject: [PATCH] Merge pull request #8343 from TheJJ/fix-mknod-mode mknod: set cli specified file mode even when it's 0o666 --- src/uu/mknod/src/mknod.rs | 62 ++++++++++++++++++++++------------- tests/by-util/test_mknod.rs | 32 ++++++++++++++++++ tests/uutests/src/lib/util.rs | 13 ++++++++ 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/src/uu/mknod/src/mknod.rs b/src/uu/mknod/src/mknod.rs index ac2b998f9..5bd79ade9 100644 --- a/src/uu/mknod/src/mknod.rs +++ b/src/uu/mknod/src/mknod.rs @@ -49,10 +49,14 @@ impl FileType { } } -/// Configuration for directory creation. +/// Configuration for special inode creation. pub struct Config<'a> { + /// bitmask of inode mode (permissions and file type) pub mode: mode_t, + /// when false, the exact mode bits will be set + pub use_umask: bool, + pub dev: dev_t, /// Set `SELinux` security context. @@ -65,18 +69,19 @@ pub struct Config<'a> { 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 = config.mode & MODE_RW_UGO != MODE_RW_UGO; - unsafe { - // store prev umask - let last_umask = if set_umask { libc::umask(0) } else { 0 }; + // set umask to 0 and store previous umask + let have_prev_umask = if config.use_umask { + None + } else { + Some(libc::umask(0)) + }; let errno = libc::mknod(c_str.as_ptr(), config.mode, config.dev); // set umask back to original value - if set_umask { - libc::umask(last_umask); + if let Some(prev_umask) = have_prev_umask { + libc::umask(prev_umask); } if errno == -1 { @@ -109,8 +114,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; let file_type = matches.get_one::("type").unwrap(); - let mode = get_mode(matches.get_one::("mode")).map_err(|e| USimpleError::new(1, e))? - | file_type.as_mode(); + + let mut use_umask = true; + let mode_permissions = match matches.get_one::("mode") { + None => MODE_RW_UGO, + Some(str_mode) => { + use_umask = false; + parse_mode(str_mode).map_err(|e| USimpleError::new(1, e))? + } + }; + let mode = mode_permissions | file_type.as_mode(); let file_name = matches .get_one::("name") @@ -143,6 +156,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = Config { mode, + use_umask, dev, set_selinux_context: set_selinux_context || context.is_some(), context, @@ -210,19 +224,21 @@ pub fn uu_app() -> Command { ) } -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| translate!("mknod-error-invalid-mode", "error" => e)) - .and_then(|mode| { - if mode > 0o777 { - Err(translate!("mknod-error-mode-permission-bits-only")) - } else { - Ok(mode) - } - }), - } +fn parse_mode(str_mode: &str) -> Result { + uucore::mode::parse_mode(str_mode) + .map_err(|e| { + translate!( + "mknod-error-invalid-mode", + "error" => e + ) + }) + .and_then(|mode| { + if mode > 0o777 { + Err(translate!("mknod-error-mode-permission-bits-only")) + } else { + Ok(mode) + } + }) } fn parse_type(tpe: &str) -> Result { diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index e9a0ba375..1c558dfd3 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -5,8 +5,11 @@ // spell-checker:ignore nconfined +use std::os::unix::fs::PermissionsExt; + use uutests::new_ucmd; use uutests::util::TestScenario; +use uutests::util::run_ucmd_as_root; use uutests::util_name; #[test] @@ -120,6 +123,35 @@ fn test_mknod_invalid_mode() { .stderr_contains("invalid mode"); } +#[test] +fn test_mknod_mode_permissions() { + for test_mode in [0o0666, 0o0000, 0o0444, 0o0004, 0o0040, 0o0400, 0o0644] { + let ts = TestScenario::new(util_name!()); + let filename = format!("null_file-{test_mode:04o}"); + + if let Ok(result) = run_ucmd_as_root( + &ts, + &[ + "--mode", + &format!("{test_mode:04o}"), + &filename, + "c", + "1", + "3", + ], + ) { + result.success().no_stdout(); + } else { + print!("Test skipped; `mknod c 1 3` for null char dev requires root user"); + break; + } + + assert!(ts.fixtures.is_char_device(&filename)); + let permissions = ts.fixtures.metadata(&filename).permissions(); + assert_eq!(test_mode, PermissionsExt::mode(&permissions) & 0o777); + } +} + #[test] #[cfg(feature = "feat_selinux")] fn test_mknod_selinux() { diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 1e71a3cf1..18a7c048a 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -1145,6 +1145,19 @@ impl AtPath { } } + #[cfg(not(windows))] + pub fn is_char_device(&self, char_dev: &str) -> bool { + unsafe { + let name = CString::new(self.plus_as_string(char_dev)).unwrap(); + let mut stat: libc::stat = std::mem::zeroed(); + if libc::stat(name.as_ptr(), &mut stat) >= 0 { + libc::S_IFCHR & stat.st_mode as libc::mode_t != 0 + } else { + false + } + } + } + pub fn hard_link(&self, original: &str, link: &str) { log_info( "hard_link",