diff --git a/src/pam/converse.rs b/src/pam/converse.rs index c64c6ea22..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; @@ -12,7 +11,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. @@ -128,7 +127,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 +141,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,13 +162,6 @@ impl Converser for CLIConverser { Hidden::Yes(()) }, ) - .map_err(|err| { - if let io::ErrorKind::TimedOut = err.kind() { - PamError::TimedOut - } else { - PamError::IoError(err) - } - }) } fn handle_error(&self, msg: &str) -> PamResult<()> { @@ -192,7 +184,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, } @@ -238,15 +230,22 @@ 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); } - 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/error.rs b/src/pam/error.rs index 1039aa16d..44331d9f7 100644 --- a/src/pam/error.rs +++ b/src/pam/error.rs @@ -175,8 +175,11 @@ pub enum PamError { Utf8Error(Utf8Error), Pam(PamErrorType), IoError(std::io::Error), + TtyRequired, EnvListFailure, InteractionRequired, + NeedsPassword, + IncorrectPasswordAttempt, TimedOut, InvalidUser(String, String), } @@ -218,6 +221,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 +229,8 @@ 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) => { write!( 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)] diff --git a/src/pam/rpassword.rs b/src/pam/rpassword.rs index 110b2481c..fd16584cf 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, @@ -121,15 +122,17 @@ 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; + return Ok(password); } if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input { if read_byte == input.term_orig.c_cc[VEOF] { - password.fill(0); break; } @@ -161,14 +164,17 @@ fn read_unbuffered( let _ = state.sink.write(b"*"); } } else { - return Err(Error::new( - ErrorKind::OutOfMemory, - "incorrect password attempt", - )); + return Err(PamError::IncorrectPasswordAttempt); } } - 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 @@ -251,14 +257,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 +280,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/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/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/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 14e922afa..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); } @@ -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/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/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/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 43a5fd591..548b1e218 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); } @@ -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); @@ -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..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); } @@ -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" + ); } }