From 8b3b43fa5d097c1f995e20629d407791d4bf1aa3 Mon Sep 17 00:00:00 2001 From: Marc Schoolderman Date: Wed, 3 Dec 2025 14:38:44 +0100 Subject: [PATCH 1/3] change cvt_err in make_pattern --- src/sudoers/tokens.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs index 42ef6fdc6..6ec3a963b 100644 --- a/src/sudoers/tokens.rs +++ b/src/sudoers/tokens.rs @@ -224,13 +224,13 @@ impl Token for SimpleCommand { const MAX_LEN: usize = 1024; fn construct(mut cmd: String) -> Result { - let cvt_err = |pat: Result<_, glob::PatternError>| { - pat.map_err(|err| format!("wildcard pattern error {err}")) + let make_pattern = |pat: String| { + glob::Pattern::new(&pat).map_err(|err| format!("wildcard pattern error {err}")) }; // detect the two edges cases if cmd == "list" || cmd == "sudoedit" { - return cvt_err(glob::Pattern::new(&cmd)); + return make_pattern(cmd); } else if cmd.starts_with("sha") { return Err("digest specifications are not supported".to_string()); } else if cmd.starts_with('^') { @@ -258,7 +258,7 @@ impl Token for SimpleCommand { cmd.push_str("/*"); } - cvt_err(glob::Pattern::new(&cmd)) + make_pattern(cmd) } // all commands start with "/" except "sudoedit" or "list" From 8386600945b723e18bbbbe61a2372648feba129f Mon Sep 17 00:00:00 2001 From: Marc Schoolderman Date: Wed, 3 Dec 2025 15:01:51 +0100 Subject: [PATCH 2/3] add fnmatch as an option --- Cargo.toml | 7 +++++-- src/cutils/mod.rs | 23 +++++++++++++++++++++++ src/sudoers/entry.rs | 2 +- src/sudoers/mod.rs | 9 +++++++++ src/sudoers/tokens.rs | 21 +++++++++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6db8a9961..7fe8b2faf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,10 @@ default-run = "sudo" [dependencies] libc = "0.2.152" -glob = "0.3.0" +glob = { version = "0.3.0", optional = true } [features] -default = [] +default = ["rust-glob"] # when enabled, use "sudo-i" PAM service name for sudo -i instead of "sudo" # ONLY ENABLE THIS FEATURE if you know that original sudo uses "sudo-i" @@ -32,6 +32,9 @@ apparmor = [] # whether to enable 'gettext' support for giving localized user-facing messages gettext = [] +# this enables the use or the Rust glob crate as instead of fnmatch(3) +rust-glob = ["dep:glob"] + # enable detailed logging (use for development only) to /tmp # this will compromise the security of sudo-rs somewhat dev = [] diff --git a/src/cutils/mod.rs b/src/cutils/mod.rs index 5fdc94a83..e8fc1934c 100644 --- a/src/cutils/mod.rs +++ b/src/cutils/mod.rs @@ -104,6 +104,29 @@ pub fn is_fifo(fildes: BorrowedFd) -> bool { fstat_mode_set(&fildes, libc::S_IFIFO) } +/// Wrapper around fnmatch for globbing +#[cfg(not(feature = "rust-glob"))] +pub fn fnmatch( + pattern: &crate::common::SudoString, + name: &std::path::Path, +) -> std::io::Result { + let pattern = pattern.as_cstr(); + let name = std::ffi::CString::new(name.as_os_str().as_bytes()).expect("path is not a C string"); + + // equivalent to "require_literal_separator" + let flags = libc::FNM_PATHNAME; + + // SAFETY: fnmatch is passed two valid pointers to a CString + match unsafe { libc::fnmatch(pattern.as_ptr(), name.as_ptr(), flags) } { + 0 => Ok(true), + libc::FNM_NOMATCH => Ok(false), + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "pattern error", + )), + } +} + #[allow(clippy::undocumented_unsafe_blocks)] #[cfg(test)] mod test { diff --git a/src/sudoers/entry.rs b/src/sudoers/entry.rs index dfadc2674..4c08f3d77 100644 --- a/src/sudoers/entry.rs +++ b/src/sudoers/entry.rs @@ -253,7 +253,7 @@ fn write_spec<'a>( Meta::All => f.write_str("ALL")?, Meta::Only((cmd, args)) => { - write!(f, "{cmd}")?; + write!(f, "{}", cmd.as_str())?; if let Some(args) = args { for arg in args.iter() { write!(f, " {arg}")?; diff --git a/src/sudoers/mod.rs b/src/sudoers/mod.rs index f69e8d2da..5e4f3d23b 100644 --- a/src/sudoers/mod.rs +++ b/src/sudoers/mod.rs @@ -626,6 +626,7 @@ fn match_token>( move |token| token.as_str() == text } +#[cfg(feature = "rust-glob")] fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) -> bool + 'a { let opts = glob::MatchOptions { require_literal_separator: true, @@ -637,6 +638,14 @@ fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) } } +#[cfg(not(feature = "rust-glob"))] +fn match_command<'a>((cmd, args): (&'a Path, &'a [String])) -> impl Fn(&Command) -> bool + 'a { + move |(cmdpat, argpat)| { + crate::cutils::fnmatch(cmdpat, cmd).unwrap_or(false) + && argpat.as_ref().map_or(true, |vec| args == vec.as_ref()) + } +} + /// Find all the aliases that a object is a member of; this requires [sanitize_alias_table] to have run first; /// I.e. this function should not be "pub". fn get_aliases(table: &VecOrd>, pred: &Predicate) -> FoundAliases diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs index 6ec3a963b..13300a0e2 100644 --- a/src/sudoers/tokens.rs +++ b/src/sudoers/tokens.rs @@ -168,7 +168,19 @@ pub type Command = (SimpleCommand, Option>); /// A type that is specific to 'only commands', that can only happen in "Defaults!command" contexts; /// which is essentially a subset of "Command" +#[cfg(feature = "rust-glob")] pub type SimpleCommand = glob::Pattern; +#[cfg(not(feature = "rust-glob"))] +pub struct SimpleCommand(SudoString); + +#[cfg(not(feature = "rust-glob"))] +impl std::ops::Deref for SimpleCommand { + type Target = SudoString; + + fn deref(&self) -> &SudoString { + &self.0 + } +} impl Token for Command { const MAX_LEN: usize = 1024; @@ -224,10 +236,19 @@ impl Token for SimpleCommand { const MAX_LEN: usize = 1024; fn construct(mut cmd: String) -> Result { + #[cfg(feature = "rust-glob")] let make_pattern = |pat: String| { glob::Pattern::new(&pat).map_err(|err| format!("wildcard pattern error {err}")) }; + #[cfg(not(feature = "rust-glob"))] + let make_pattern = |pat| match SudoString::new(pat) { + Ok(pattern) if crate::cutils::fnmatch(&pattern, &std::path::PathBuf::new()).is_ok() => { + Ok(SimpleCommand(pattern)) + } + _ => Err("wildcard pattern error".to_string()), + }; + // detect the two edges cases if cmd == "list" || cmd == "sudoedit" { return make_pattern(cmd); From e8a737a02e04c596d67e4388106f457530c67652 Mon Sep 17 00:00:00 2001 From: Marc Schoolderman Date: Wed, 3 Dec 2025 15:50:00 +0100 Subject: [PATCH 3/3] use newtype for glob::Pattern as well --- src/sudoers/tokens.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs index 13300a0e2..0ec4aa980 100644 --- a/src/sudoers/tokens.rs +++ b/src/sudoers/tokens.rs @@ -168,16 +168,15 @@ pub type Command = (SimpleCommand, Option>); /// A type that is specific to 'only commands', that can only happen in "Defaults!command" contexts; /// which is essentially a subset of "Command" -#[cfg(feature = "rust-glob")] -pub type SimpleCommand = glob::Pattern; -#[cfg(not(feature = "rust-glob"))] -pub struct SimpleCommand(SudoString); +pub struct SimpleCommand(::Target); -#[cfg(not(feature = "rust-glob"))] impl std::ops::Deref for SimpleCommand { + #[cfg(feature = "rust-glob")] + type Target = glob::Pattern; + #[cfg(not(feature = "rust-glob"))] type Target = SudoString; - fn deref(&self) -> &SudoString { + fn deref(&self) -> &Self::Target { &self.0 } } @@ -238,7 +237,9 @@ impl Token for SimpleCommand { fn construct(mut cmd: String) -> Result { #[cfg(feature = "rust-glob")] let make_pattern = |pat: String| { - glob::Pattern::new(&pat).map_err(|err| format!("wildcard pattern error {err}")) + glob::Pattern::new(&pat) + .map(SimpleCommand) + .map_err(|_| "wildcard pattern error".to_string()) }; #[cfg(not(feature = "rust-glob"))]