From 16e1cb3b098cc53f1044f4ad95d9b656860f5795 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:31:11 +0100 Subject: [PATCH 1/6] Propagate all error kinds from pam::converse not just timeouts This way they rather than silently discarding the error message and doing another authentication attempt, they properly report the error message and cause sudo to exit. This way for example pam_faillock won't cause a persistent error like incorrect SUDO_ASKPASS value (once implemented) to be treated as multiple successive failed password attempts. --- src/pam/converse.rs | 9 ++++----- src/pam/mod.rs | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/pam/converse.rs b/src/pam/converse.rs index c64c6ea22..1670534a5 100644 --- a/src/pam/converse.rs +++ b/src/pam/converse.rs @@ -192,7 +192,7 @@ pub(super) struct ConverserData { // pam_authenticate does not return error codes returned by the conversation // function; these are set by the conversation function instead of returning // multiple error codes. - pub(super) timed_out: bool, + pub(super) error: Option, pub(super) panicked: bool, } @@ -242,11 +242,10 @@ pub(super) unsafe extern "C" fn converse( Ok(resp_buf) => { resp_bufs.push(resp_buf); } - Err(PamError::TimedOut) => { - app_data.timed_out = true; + Err(err) => { + app_data.error = Some(err); return PamErrorType::ConversationError; } - Err(_) => return PamErrorType::ConversationError, } } @@ -423,7 +422,7 @@ mod test { converser_name: "tux".to_string(), no_interact: false, auth_prompt: Some("authenticate".to_owned()), - timed_out: false, + error: None, panicked: false, }); let cookie = PamConvBorrow::new(hello.as_mut()); diff --git a/src/pam/mod.rs b/src/pam/mod.rs index 341a5be0b..2a8fa0cbd 100644 --- a/src/pam/mod.rs +++ b/src/pam/mod.rs @@ -78,7 +78,7 @@ impl PamContext { converser_name: converser_name.to_owned(), no_interact, auth_prompt: Some("authenticate".to_owned()), - timed_out: false, + error: None, panicked: false, })); @@ -174,8 +174,8 @@ impl PamContext { } // SAFETY: self.data_ptr was created by Box::into_raw - if unsafe { (*self.data_ptr).timed_out } { - return Err(PamError::TimedOut); + if let Some(error) = unsafe { (*self.data_ptr).error.take() } { + return Err(error); } #[allow(clippy::question_mark)] From 4d5908c01fa5f944169ec203919c0b07534d6a53 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:35:20 +0100 Subject: [PATCH 2/6] Error fixes --- src/pam/converse.rs | 15 ++++++--------- src/pam/error.rs | 4 ++++ src/pam/rpassword.rs | 17 ++++++++--------- .../src/sudo/flag_list/credential_caching.rs | 2 +- .../src/sudo/pass_auth/stdin.rs | 4 ++-- .../src/sudo/pass_auth/tty.rs | 4 ++-- .../src/sudo/sudoers/timestamp_timeout.rs | 4 ++-- .../sudo-compliance-tests/src/sudo/timestamp.rs | 4 ++-- .../src/sudo/timestamp/remove.rs | 2 +- .../src/sudo/timestamp/reset.rs | 6 +++--- .../src/sudo/timestamp/validate.rs | 2 +- .../src/sudoedit/sudoers.rs | 5 ++++- 12 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/pam/converse.rs b/src/pam/converse.rs index 1670534a5..90e26e8ef 100644 --- a/src/pam/converse.rs +++ b/src/pam/converse.rs @@ -128,7 +128,7 @@ impl Drop for SignalGuard { } impl CLIConverser { - fn open(&self) -> std::io::Result<(Terminal<'_>, SignalGuard)> { + fn open(&self) -> PamResult<(Terminal<'_>, SignalGuard)> { let term = if self.use_stdin { Terminal::open_stdie()? } else { @@ -142,11 +142,11 @@ impl CLIConverser { impl Converser for CLIConverser { fn handle_normal_prompt(&self, msg: &str) -> PamResult { let (mut tty, _guard) = self.open()?; - Ok(tty.read_input( + tty.read_input( &format!("[{}: input needed] {msg} ", self.name), None, Hidden::No, - )?) + ) } fn handle_hidden_prompt(&self, msg: &str) -> PamResult { @@ -163,12 +163,9 @@ impl Converser for CLIConverser { Hidden::Yes(()) }, ) - .map_err(|err| { - if let io::ErrorKind::TimedOut = err.kind() { - PamError::TimedOut - } else { - PamError::IoError(err) - } + .map_err(|err| match err { + PamError::IoError(err) if err.kind() == io::ErrorKind::TimedOut => PamError::TimedOut, + err => err, }) } diff --git a/src/pam/error.rs b/src/pam/error.rs index 1039aa16d..513b847b3 100644 --- a/src/pam/error.rs +++ b/src/pam/error.rs @@ -175,8 +175,10 @@ pub enum PamError { Utf8Error(Utf8Error), Pam(PamErrorType), IoError(std::io::Error), + TtyRequired, EnvListFailure, InteractionRequired, + IncorrectPasswordAttempt, TimedOut, InvalidUser(String, String), } @@ -218,6 +220,7 @@ impl fmt::Display for PamError { } PamError::Pam(tp) => write!(f, "PAM error: {}", tp.get_err_msg()), PamError::IoError(e) => write!(f, "IO error: {e}"), + PamError::TtyRequired => write!(f, "A terminal is required to read the password"), PamError::EnvListFailure => { write!( f, @@ -225,6 +228,7 @@ impl fmt::Display for PamError { ) } PamError::InteractionRequired => write!(f, "Interaction is required"), + PamError::IncorrectPasswordAttempt => write!(f, "Incorrect password attempt"), PamError::TimedOut => write!(f, "timed out"), PamError::InvalidUser(username, other_user) => { write!( diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index 110b2481c..bd643a888 100644 --- a/src/pam/rpassword.rs +++ b/src/pam/rpassword.rs @@ -14,7 +14,7 @@ //! (although much more robust than in the original code) use std::ffi::c_void; -use std::io::{self, Error, ErrorKind, Read}; +use std::io::{self, ErrorKind, Read}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; use std::time::{Duration, Instant}; use std::{fs, mem}; @@ -22,6 +22,7 @@ use std::{fs, mem}; use libc::{tcsetattr, termios, ECHO, ECHONL, ICANON, TCSANOW, VEOF, VERASE, VKILL}; use crate::cutils::{cerr, safe_isatty}; +use crate::pam::{PamError, PamResult}; use super::securemem::PamBuffer; @@ -93,7 +94,7 @@ fn read_unbuffered( source: &mut dyn io::Read, sink: &mut dyn io::Write, hide_input: &Hidden, -) -> io::Result { +) -> PamResult { struct ExitGuard<'a> { pw_len: usize, feedback: bool, @@ -161,10 +162,7 @@ fn read_unbuffered( let _ = state.sink.write(b"*"); } } else { - return Err(Error::new( - ErrorKind::OutOfMemory, - "incorrect password attempt", - )); + return Err(PamError::IncorrectPasswordAttempt); } } @@ -251,14 +249,15 @@ pub enum Terminal<'a> { impl Terminal<'_> { /// Open the current TTY for user communication - pub fn open_tty() -> io::Result { + pub fn open_tty() -> PamResult { // control ourselves that we are really talking to a TTY // mitigates: https://marc.info/?l=oss-security&m=168164424404224 Ok(Terminal::Tty( fs::OpenOptions::new() .read(true) .write(true) - .open("/dev/tty")?, + .open("/dev/tty") + .map_err(|_| PamError::TtyRequired)?, )) } @@ -273,7 +272,7 @@ impl Terminal<'_> { prompt: &str, timeout: Option, hidden: Hidden<()>, - ) -> io::Result { + ) -> PamResult { fn do_hide_input( hidden: Hidden<()>, input: BorrowedFd, diff --git a/test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs b/test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs index c8d1e1d8c..61788d037 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs @@ -87,7 +87,7 @@ fn flag_reset_timestamp() { let diagnostic = if sudo_test::is_original_sudo() { "sudo: a password is required" } else { - "sudo: Authentication failed" + "sudo: A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs index 14e922afa..6de521c2c 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs @@ -96,7 +96,7 @@ fn input_longer_than_max_pam_response_size_is_handled_gracefully() { assert_contains!(stderr, "sudo: 2 incorrect password attempts"); } } else { - assert_contains!(stderr, "incorrect authentication attempt"); + assert_contains!(stderr, "Incorrect password attempt"); assert_not_contains!(stderr, "panic"); } } @@ -124,7 +124,7 @@ fn input_longer_than_password_should_not_be_accepted_as_correct_password() { if sudo_test::is_original_sudo() { assert_contains!(stderr, "sudo: 1 incorrect password attempt"); } else { - assert_contains!(stderr, "incorrect authentication attempt"); + assert_contains!(stderr, "Incorrect password attempt"); } } } diff --git a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/tty.rs b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/tty.rs index 19002a11f..4fe22352b 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/tty.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/tty.rs @@ -52,7 +52,7 @@ fn no_tty() { let diagnostic = if sudo_test::is_original_sudo() { "a terminal is required to read the password" } else { - "Maximum 3 incorrect authentication attempts" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } @@ -96,7 +96,7 @@ fn input_longer_than_password_should_not_be_accepted_as_correct_password() { let diagnostic = if sudo_test::is_original_sudo() { "sudo: 1 incorrect password attempt" } else { - "Authentication failed, try again" + "Incorrect password attempt" }; assert_contains!(stderr, diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/sudoers/timestamp_timeout.rs b/test-framework/sudo-compliance-tests/src/sudo/sudoers/timestamp_timeout.rs index 62bc70125..99ca6bc18 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/sudoers/timestamp_timeout.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/sudoers/timestamp_timeout.rs @@ -46,7 +46,7 @@ Defaults timestamp_timeout=0.1" let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } @@ -73,7 +73,7 @@ Defaults timestamp_timeout=0" let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs index 43a5fd591..9d0bbf13b 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs @@ -45,7 +45,7 @@ fn by_default_credential_caching_is_local() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } @@ -220,7 +220,7 @@ fn cached_credential_not_shared_between_auth_users() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs index bcfa4b295..467b100b6 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs @@ -83,7 +83,7 @@ fn also_works_locally() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "Authentication failed" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp/reset.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp/reset.rs index f62672c75..827bf6be2 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp/reset.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp/reset.rs @@ -24,7 +24,7 @@ fn it_works() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } @@ -72,7 +72,7 @@ fn with_command_prompts_for_password() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } @@ -144,7 +144,7 @@ fn with_command_does_not_cache_credentials() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "Authentication failed" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp/validate.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp/validate.rs index 8e7a1c224..e49135f2f 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp/validate.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp/validate.rs @@ -40,7 +40,7 @@ fn prompts_for_password() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "incorrect authentication attempt" + "A terminal is required to read the password" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudoedit/sudoers.rs b/test-framework/sudo-compliance-tests/src/sudoedit/sudoers.rs index 261ea04fd..1dc131ea0 100644 --- a/test-framework/sudo-compliance-tests/src/sudoedit/sudoers.rs +++ b/test-framework/sudo-compliance-tests/src/sudoedit/sudoers.rs @@ -216,6 +216,9 @@ fn passwd() { if sudo_test::is_original_sudo() { assert_contains!(output.stderr(), "a password is required"); } else { - assert_contains!(output.stderr(), "Authentication failed"); + assert_contains!( + output.stderr(), + "A terminal is required to read the password" + ); } } From 2550cc0cb2ab1d20d9d7adaa1b08a292896c2cc7 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:25:52 +0100 Subject: [PATCH 3/6] Don't retry authentication on password input error For example a timeout or ctrl+d. In addition don't allow PAM to ask for another password when an input error happened. We will still retry if the password that was entered was incorrect of course. This matches the behavior of og sudo. --- src/pam/converse.rs | 10 +++++++++- src/pam/error.rs | 2 ++ src/pam/rpassword.rs | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pam/converse.rs b/src/pam/converse.rs index 90e26e8ef..9c50bc783 100644 --- a/src/pam/converse.rs +++ b/src/pam/converse.rs @@ -12,7 +12,7 @@ use super::{error::PamResult, rpassword, securemem::PamBuffer, PamError, PamErro /// Each message in a PAM conversation will have a message style. Each of these /// styles must be handled separately. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Eq, PartialEq)] pub enum PamMessageStyle { /// Prompt for input using a message. The input should considered secret /// and should be hidden from view. @@ -235,6 +235,14 @@ pub(super) unsafe extern "C" fn converse( // send the conversation off to the Rust part // SAFETY: appdata_ptr contains the `*mut ConverserData` that is untouched by PAM let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData) }; + + if app_data.error.is_some() + && (style == PamMessageStyle::PromptEchoOff + || style == PamMessageStyle::PromptEchoOn) + { + return PamErrorType::ConversationError; + } + match handle_message(app_data, style, &msg) { Ok(resp_buf) => { resp_bufs.push(resp_buf); diff --git a/src/pam/error.rs b/src/pam/error.rs index 513b847b3..44331d9f7 100644 --- a/src/pam/error.rs +++ b/src/pam/error.rs @@ -178,6 +178,7 @@ pub enum PamError { TtyRequired, EnvListFailure, InteractionRequired, + NeedsPassword, IncorrectPasswordAttempt, TimedOut, InvalidUser(String, String), @@ -228,6 +229,7 @@ impl fmt::Display for PamError { ) } PamError::InteractionRequired => write!(f, "Interaction is required"), + PamError::NeedsPassword => write!(f, "Password is required"), PamError::IncorrectPasswordAttempt => write!(f, "Incorrect password attempt"), PamError::TimedOut => write!(f, "timed out"), PamError::InvalidUser(username, other_user) => { diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index bd643a888..68d5be281 100644 --- a/src/pam/rpassword.rs +++ b/src/pam/rpassword.rs @@ -130,6 +130,10 @@ fn read_unbuffered( if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input { if read_byte == input.term_orig.c_cc[VEOF] { + if state.pw_len == 0 { + return Err(PamError::NeedsPassword); + } + password.fill(0); break; } From 85b7a05129378573bb563e981a5a006813b3be61 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:49:24 +0100 Subject: [PATCH 4/6] Move timeout error conversion into rpassword --- src/pam/converse.rs | 5 ----- src/pam/rpassword.rs | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pam/converse.rs b/src/pam/converse.rs index 9c50bc783..25038e075 100644 --- a/src/pam/converse.rs +++ b/src/pam/converse.rs @@ -1,5 +1,4 @@ use std::ffi::{c_int, c_void}; -use std::io; use std::time::Duration; use crate::cutils::string_from_ptr; @@ -163,10 +162,6 @@ impl Converser for CLIConverser { Hidden::Yes(()) }, ) - .map_err(|err| match err { - PamError::IoError(err) if err.kind() == io::ErrorKind::TimedOut => PamError::TimedOut, - err => err, - }) } fn handle_error(&self, msg: &str) -> PamResult<()> { diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index 68d5be281..9f2fb5c6d 100644 --- a/src/pam/rpassword.rs +++ b/src/pam/rpassword.rs @@ -122,7 +122,10 @@ fn read_unbuffered( // with the amount of asterisks on the terminal (both tracked in `pw_len`) #[allow(clippy::unbuffered_bytes)] for read_byte in source.bytes() { - let read_byte = read_byte?; + let read_byte = read_byte.map_err(|err| match err { + err if err.kind() == io::ErrorKind::TimedOut => PamError::TimedOut, + err => PamError::IoError(err), + })?; if read_byte == b'\n' || read_byte == b'\r' { break; From bfd948863e920205379cfa966b4a44f163def979 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:37:24 +0100 Subject: [PATCH 5/6] Review comment --- src/pam/rpassword.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index 9f2fb5c6d..c1141843f 100644 --- a/src/pam/rpassword.rs +++ b/src/pam/rpassword.rs @@ -134,6 +134,8 @@ fn read_unbuffered( if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input { if read_byte == input.term_orig.c_cc[VEOF] { if state.pw_len == 0 { + // In case of Ctrl-D we don't want to ask for a password a second time, so + // return an error. return Err(PamError::NeedsPassword); } From 0956be773776e1a0d91dc17fa4936e443e034af3 Mon Sep 17 00:00:00 2001 From: bjorn3 <17426603+bjorn3@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:50:51 +0100 Subject: [PATCH 6/6] Fix handling of EOF other than as caused by Ctrl-D --- src/pam/rpassword.rs | 17 +++++------ .../sudo-compliance-tests/src/su.rs | 4 +-- .../sudo-compliance-tests/src/su/syslog.rs | 13 ++++----- .../src/sudo/nopasswd.rs | 5 +--- .../src/sudo/pass_auth/stdin.rs | 4 +-- .../sudo-compliance-tests/src/sudo/passwd.rs | 2 +- .../src/sudo/password_retry.rs | 29 +++++++++++++++++++ .../sudo-compliance-tests/src/sudo/sudoers.rs | 2 +- .../sudo-compliance-tests/src/sudo/syslog.rs | 7 +---- .../src/sudo/timestamp.rs | 2 +- .../src/sudo/timestamp/remove.rs | 2 +- 11 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index c1141843f..fd16584cf 100644 --- a/src/pam/rpassword.rs +++ b/src/pam/rpassword.rs @@ -128,18 +128,11 @@ fn read_unbuffered( })?; if read_byte == b'\n' || read_byte == b'\r' { - break; + return Ok(password); } if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input { if read_byte == input.term_orig.c_cc[VEOF] { - if state.pw_len == 0 { - // In case of Ctrl-D we don't want to ask for a password a second time, so - // return an error. - return Err(PamError::NeedsPassword); - } - - password.fill(0); break; } @@ -175,7 +168,13 @@ fn read_unbuffered( } } - Ok(password) + if state.pw_len == 0 { + // In case of EOF or Ctrl-D we don't want to ask for a password a second + // time, so return an error. + Err(PamError::NeedsPassword) + } else { + Ok(password) + } } /// Write something and immediately flush diff --git a/test-framework/sudo-compliance-tests/src/su.rs b/test-framework/sudo-compliance-tests/src/su.rs index 9f2f9d523..8a7049cef 100644 --- a/test-framework/sudo-compliance-tests/src/su.rs +++ b/test-framework/sudo-compliance-tests/src/su.rs @@ -98,7 +98,7 @@ fn required_password_is_target_users_fail() { let diagnostic = if sudo_test::is_original_sudo() { "Authentication failure" } else { - "Maximum 3 incorrect authentication attempts" + "Authentication failed, try again." }; assert_contains!(output.stderr(), diagnostic); } @@ -131,7 +131,7 @@ fn password_is_required_when_target_user_is_self() { let diagnostic = if sudo_test::is_original_sudo() { "Authentication failure" } else { - "Maximum 3 incorrect authentication attempts" + "Password is required" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/su/syslog.rs b/test-framework/sudo-compliance-tests/src/su/syslog.rs index 790cdb1f3..db32efa57 100644 --- a/test-framework/sudo-compliance-tests/src/su/syslog.rs +++ b/test-framework/sudo-compliance-tests/src/su/syslog.rs @@ -85,19 +85,16 @@ fn logs_every_failed_authentication_attempt() { eprintln!("\n--- /var/log/auth.log ---\n{auth_log}\n--- /var/log/auth.log ---\n"); - if sudo_test::is_original_sudo() { - assert_contains!( - auth_log, - format!("su: pam_unix(su:auth): auth could not identify password for [{target_user}]") - ); + assert_contains!( + auth_log, + format!("su: pam_unix(su:auth): auth could not identify password for [{target_user}]") + ); + if sudo_test::is_original_sudo() { let tty = "none"; assert_contains!( auth_log, format!("FAILED SU (to {target_user}) {invoking_user} on {tty}") ); - } else { - let tty = ""; - assert_contains!(auth_log, format!("su: pam_unix(su:auth): authentication failure; logname= uid={invoking_userid} euid=0 tty={tty} ruser={invoking_user} rhost= user={target_user}")); } } diff --git a/test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs b/test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs index 33537a586..b4932c566 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs @@ -141,9 +141,6 @@ fn v_flag_without_pwd_fails_if_nopasswd_is_not_set_for_all_users_entries() { ); } } else { - assert_contains!( - stderr, - "[sudo: authenticate] Password: \nsudo: Authentication failed, try again.\n[sudo: authenticate] Password: \nsudo: Authentication failed, try again.\n[sudo: authenticate] Password: \nsudo: Maximum 3 incorrect authentication attempts" - ); + assert_contains!(stderr, "Password is required"); } } diff --git a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs index 6de521c2c..9d5f63134 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs @@ -34,7 +34,7 @@ fn incorrect_password() { let diagnostic = if sudo_test::is_original_sudo() { "incorrect password attempt" } else { - "incorrect authentication attempt" + "Authentication failed, try again." }; assert_contains!(output.stderr(), diagnostic); } @@ -54,7 +54,7 @@ fn no_password() { let diagnostic = if sudo_test::is_original_sudo() { "no password was provided" } else { - "incorrect authentication attempt" + "Password is required" }; assert_contains!(output.stderr(), diagnostic); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/passwd.rs b/test-framework/sudo-compliance-tests/src/sudo/passwd.rs index 9a49d8540..6bf4c660d 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/passwd.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/passwd.rs @@ -48,6 +48,6 @@ fn explicit_passwd_overrides_nopasswd() { if sudo_test::is_original_sudo() { assert_snapshot!(stderr); } else { - assert_contains!(stderr, "Maximum 3 incorrect authentication attempts"); + assert_contains!(stderr, "Password is required"); } } diff --git a/test-framework/sudo-compliance-tests/src/sudo/password_retry.rs b/test-framework/sudo-compliance-tests/src/sudo/password_retry.rs index ee0a9f741..986a60413 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/password_retry.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/password_retry.rs @@ -202,3 +202,32 @@ it could be that the image defaults to a high retry delay value; \ you may want to increase NEW_DELAY_MICROS" ); } + +#[test] +fn no_password_retry_on_empty_stdin() { + let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL")) + .user(User(USERNAME).password(PASSWORD)) + .build(); + + let output = Command::new("sh") + .arg("-c") + .arg("echo -n | sudo -S true") + .as_user(USERNAME) + .output(&env); + + output.assert_exit_code(1); + + if sudo_test::is_original_sudo() { + assert_eq!( + output.stderr(), + "[sudo] password for ferris: \n\ +sudo: no password was provided\n\ +sudo: a password is required" + ); + } else { + assert_eq!( + output.stderr(), + "[sudo: authenticate] Password: \nsudo: Password is required" + ); + } +} diff --git a/test-framework/sudo-compliance-tests/src/sudo/sudoers.rs b/test-framework/sudo-compliance-tests/src/sudo/sudoers.rs index 14a240070..d673783ce 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/sudoers.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/sudoers.rs @@ -167,7 +167,7 @@ fn user_specifications_evaluated_bottom_to_top() { let diagnostic = if sudo_test::is_original_sudo() { "no password was provided" } else { - "incorrect authentication attempt" + "Password is required" }; assert_contains!(output.stderr(), diagnostic); diff --git a/test-framework/sudo-compliance-tests/src/sudo/syslog.rs b/test-framework/sudo-compliance-tests/src/sudo/syslog.rs index ff0494509..67fd2bd19 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/syslog.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/syslog.rs @@ -39,10 +39,5 @@ fn sudo_logs_every_failed_authentication_attempt() { assert!(!output.status().success()); let auth_log = rsyslog.auth_log(); - let diagnostic = if sudo_test::is_original_sudo() { - "auth could not identify password" - } else { - "authentication failure" - }; - assert_contains!(auth_log, diagnostic); + assert_contains!(auth_log, "auth could not identify password"); } diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs index 9d0bbf13b..548b1e218 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp.rs @@ -141,7 +141,7 @@ fn cached_credential_not_shared_with_target_user_that_are_not_self() { let diagnostic = if sudo_test::is_original_sudo() { "a password is required" } else { - "Maximum 3 incorrect authentication attempts" + "Password is required" }; assert_contains!(output.stderr(), diagnostic); diff --git a/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs b/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs index 467b100b6..da7b367a8 100644 --- a/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs +++ b/test-framework/sudo-compliance-tests/src/sudo/timestamp/remove.rs @@ -56,7 +56,7 @@ fn has_a_user_global_effect() { let diagnostic = if sudo_test::is_original_sudo() { "1 incorrect password attempt" } else { - "incorrect authentication attempt" + "Authentication failed, try again." }; assert_contains!(output.stderr(), diagnostic); }