From 24c33dbd17d8d764e836b83b0825a9c0e3f59c99 Mon Sep 17 00:00:00 2001 From: H-TTTTT <36735327+H-TTTTT@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:41:19 +0800 Subject: [PATCH 1/2] fix: accept any execute bit for USE_SENDMAIL command check Operator precedence made `!mode & 0o111 != 0` require all three execute bits (u/g/o), so owner-only (0700) and group-executable (0750) sendmail commands were rejected as non-executable. Check `(mode & 0o111) != 0` via mode_is_executable and add a unit test for the common permission modes. Fixes #7445 --- src/config.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 49281b6c..0046e56d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -920,6 +920,28 @@ make_config! { }, } +#[cfg(unix)] +fn mode_is_executable(mode: u32) -> bool { + mode & 0o111 != 0 +} + +#[cfg(all(test, unix))] +mod tests { + use super::mode_is_executable; + + #[test] + fn executable_mode_accepts_any_execute_bit() { + assert!(mode_is_executable(0o100)); + assert!(mode_is_executable(0o010)); + assert!(mode_is_executable(0o001)); + assert!(mode_is_executable(0o700)); + assert!(mode_is_executable(0o750)); + assert!(mode_is_executable(0o755)); + assert!(!mode_is_executable(0o600)); + assert!(!mode_is_executable(0o000)); + } +} + fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { // Validate connection URL is valid and DB feature is enabled #[cfg(sqlite)] @@ -1136,7 +1158,7 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - if !metadata.permissions().mode() & 0o111 != 0 { + if !mode_is_executable(metadata.permissions().mode()) { err!(format!("sendmail command at `{path:?}` isn't executable")); } } From 1cb10979bc338a63a7d7bc41ca0b758d19fa3236 Mon Sep 17 00:00:00 2001 From: x06579 Date: Tue, 21 Jul 2026 09:03:57 +0800 Subject: [PATCH 2/2] fix: use nix access for USE_SENDMAIL executable check Per maintainer review, replace the mode-bit helper/tests with nix::unistd::access(..., X_OK) so validation matches the current process's real execute permission. --- Cargo.lock | 19 +++++++++++++++++++ Cargo.toml | 2 ++ src/config.rs | 26 ++------------------------ 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0715098c..f84594b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -948,6 +948,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.1" @@ -3250,6 +3256,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5930,6 +5948,7 @@ dependencies = [ "macros", "mimalloc", "moka", + "nix", "num-derive", "num-traits", "opendal", diff --git a/Cargo.toml b/Cargo.toml index 909570e7..ac9dc77e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,8 @@ unstable = [] [target."cfg(unix)".dependencies] # Logging syslog = "7.0.0" +# Filesystem access checks (e.g. sendmail executability for the current user) +nix = { version = "0.30.1", default-features = false, features = ["fs"] } [dependencies] macros = { path = "./macros" } diff --git a/src/config.rs b/src/config.rs index 0046e56d..7bda3199 100644 --- a/src/config.rs +++ b/src/config.rs @@ -920,28 +920,6 @@ make_config! { }, } -#[cfg(unix)] -fn mode_is_executable(mode: u32) -> bool { - mode & 0o111 != 0 -} - -#[cfg(all(test, unix))] -mod tests { - use super::mode_is_executable; - - #[test] - fn executable_mode_accepts_any_execute_bit() { - assert!(mode_is_executable(0o100)); - assert!(mode_is_executable(0o010)); - assert!(mode_is_executable(0o001)); - assert!(mode_is_executable(0o700)); - assert!(mode_is_executable(0o750)); - assert!(mode_is_executable(0o755)); - assert!(!mode_is_executable(0o600)); - assert!(!mode_is_executable(0o000)); - } -} - fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { // Validate connection URL is valid and DB feature is enabled #[cfg(sqlite)] @@ -1157,8 +1135,8 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { #[cfg(unix)] { - use std::os::unix::fs::PermissionsExt; - if !mode_is_executable(metadata.permissions().mode()) { + // Check actual execute permission for the current process, not just mode bits. + if nix::unistd::access(&path, nix::unistd::AccessFlags::X_OK).is_err() { err!(format!("sendmail command at `{path:?}` isn't executable")); } }