Skip to content

Commit 732347b

Browse files
authored
Propagate all error kinds from pam::converse not just timeouts (#1346)
2 parents a40b611 + 37139a3 commit 732347b

File tree

20 files changed

+104
-74
lines changed

20 files changed

+104
-74
lines changed

src/pam/converse.rs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::ffi::{c_int, c_void};
2-
use std::io;
32
use std::time::Duration;
43

54
use crate::cutils::string_from_ptr;
@@ -12,7 +11,7 @@ use super::{error::PamResult, rpassword, securemem::PamBuffer, PamError, PamErro
1211

1312
/// Each message in a PAM conversation will have a message style. Each of these
1413
/// styles must be handled separately.
15-
#[derive(Clone, Copy)]
14+
#[derive(Clone, Copy, Eq, PartialEq)]
1615
pub enum PamMessageStyle {
1716
/// Prompt for input using a message. The input should considered secret
1817
/// and should be hidden from view.
@@ -128,7 +127,7 @@ impl Drop for SignalGuard {
128127
}
129128

130129
impl CLIConverser {
131-
fn open(&self) -> std::io::Result<(Terminal<'_>, SignalGuard)> {
130+
fn open(&self) -> PamResult<(Terminal<'_>, SignalGuard)> {
132131
let term = if self.use_stdin {
133132
Terminal::open_stdie()?
134133
} else {
@@ -142,11 +141,11 @@ impl CLIConverser {
142141
impl Converser for CLIConverser {
143142
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
144143
let (mut tty, _guard) = self.open()?;
145-
Ok(tty.read_input(
144+
tty.read_input(
146145
&format!("[{}: input needed] {msg} ", self.name),
147146
None,
148147
Hidden::No,
149-
)?)
148+
)
150149
}
151150

152151
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
@@ -163,13 +162,6 @@ impl Converser for CLIConverser {
163162
Hidden::Yes(())
164163
},
165164
)
166-
.map_err(|err| {
167-
if let io::ErrorKind::TimedOut = err.kind() {
168-
PamError::TimedOut
169-
} else {
170-
PamError::IoError(err)
171-
}
172-
})
173165
}
174166

175167
fn handle_error(&self, msg: &str) -> PamResult<()> {
@@ -192,7 +184,7 @@ pub(super) struct ConverserData<C> {
192184
// pam_authenticate does not return error codes returned by the conversation
193185
// function; these are set by the conversation function instead of returning
194186
// multiple error codes.
195-
pub(super) timed_out: bool,
187+
pub(super) error: Option<PamError>,
196188
pub(super) panicked: bool,
197189
}
198190

@@ -238,15 +230,22 @@ pub(super) unsafe extern "C" fn converse<C: Converser>(
238230
// send the conversation off to the Rust part
239231
// SAFETY: appdata_ptr contains the `*mut ConverserData` that is untouched by PAM
240232
let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
233+
234+
if app_data.error.is_some()
235+
&& (style == PamMessageStyle::PromptEchoOff
236+
|| style == PamMessageStyle::PromptEchoOn)
237+
{
238+
return PamErrorType::ConversationError;
239+
}
240+
241241
match handle_message(app_data, style, &msg) {
242242
Ok(resp_buf) => {
243243
resp_bufs.push(resp_buf);
244244
}
245-
Err(PamError::TimedOut) => {
246-
app_data.timed_out = true;
245+
Err(err) => {
246+
app_data.error = Some(err);
247247
return PamErrorType::ConversationError;
248248
}
249-
Err(_) => return PamErrorType::ConversationError,
250249
}
251250
}
252251

@@ -423,7 +422,7 @@ mod test {
423422
converser_name: "tux".to_string(),
424423
no_interact: false,
425424
auth_prompt: Some("authenticate".to_owned()),
426-
timed_out: false,
425+
error: None,
427426
panicked: false,
428427
});
429428
let cookie = PamConvBorrow::new(hello.as_mut());

src/pam/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,11 @@ pub enum PamError {
175175
Utf8Error(Utf8Error),
176176
Pam(PamErrorType),
177177
IoError(std::io::Error),
178+
TtyRequired,
178179
EnvListFailure,
179180
InteractionRequired,
181+
NoPasswordProvided,
182+
IncorrectPasswordAttempt,
180183
TimedOut,
181184
InvalidUser(String, String),
182185
}
@@ -218,13 +221,16 @@ impl fmt::Display for PamError {
218221
}
219222
PamError::Pam(tp) => write!(f, "PAM error: {}", tp.get_err_msg()),
220223
PamError::IoError(e) => write!(f, "IO error: {e}"),
224+
PamError::TtyRequired => write!(f, "A terminal is required to authenticate"),
221225
PamError::EnvListFailure => {
222226
write!(
223227
f,
224228
"It was not possible to get a list of environment variables"
225229
)
226230
}
227231
PamError::InteractionRequired => write!(f, "Interaction is required"),
232+
PamError::NoPasswordProvided => write!(f, "Authentication required but not attempted"),
233+
PamError::IncorrectPasswordAttempt => write!(f, "Incorrect authentication attempt"),
228234
PamError::TimedOut => write!(f, "timed out"),
229235
PamError::InvalidUser(username, other_user) => {
230236
write!(

src/pam/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl PamContext {
7878
converser_name: converser_name.to_owned(),
7979
no_interact,
8080
auth_prompt: Some("authenticate".to_owned()),
81-
timed_out: false,
81+
error: None,
8282
panicked: false,
8383
}));
8484

@@ -174,8 +174,8 @@ impl PamContext {
174174
}
175175

176176
// SAFETY: self.data_ptr was created by Box::into_raw
177-
if unsafe { (*self.data_ptr).timed_out } {
178-
return Err(PamError::TimedOut);
177+
if let Some(error) = unsafe { (*self.data_ptr).error.take() } {
178+
return Err(error);
179179
}
180180

181181
#[allow(clippy::question_mark)]

src/pam/rpassword.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
//! (although much more robust than in the original code)
1515
1616
use std::ffi::c_void;
17-
use std::io::{self, Error, ErrorKind, Read};
17+
use std::io::{self, ErrorKind, Read};
1818
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
1919
use std::time::{Duration, Instant};
2020
use std::{fs, mem};
2121

2222
use libc::{tcsetattr, termios, ECHO, ECHONL, ICANON, TCSANOW, VEOF, VERASE, VKILL};
2323

2424
use crate::cutils::{cerr, safe_isatty};
25+
use crate::pam::{PamError, PamResult};
2526

2627
use super::securemem::PamBuffer;
2728

@@ -93,7 +94,7 @@ fn read_unbuffered(
9394
source: &mut dyn io::Read,
9495
sink: &mut dyn io::Write,
9596
hide_input: &Hidden<HiddenInput>,
96-
) -> io::Result<PamBuffer> {
97+
) -> PamResult<PamBuffer> {
9798
struct ExitGuard<'a> {
9899
pw_len: usize,
99100
feedback: bool,
@@ -121,15 +122,17 @@ fn read_unbuffered(
121122
// with the amount of asterisks on the terminal (both tracked in `pw_len`)
122123
#[allow(clippy::unbuffered_bytes)]
123124
for read_byte in source.bytes() {
124-
let read_byte = read_byte?;
125+
let read_byte = read_byte.map_err(|err| match err {
126+
err if err.kind() == io::ErrorKind::TimedOut => PamError::TimedOut,
127+
err => PamError::IoError(err),
128+
})?;
125129

126130
if read_byte == b'\n' || read_byte == b'\r' {
127-
break;
131+
return Ok(password);
128132
}
129133

130134
if let Hidden::Yes(input) | Hidden::WithFeedback(input) = hide_input {
131135
if read_byte == input.term_orig.c_cc[VEOF] {
132-
password.fill(0);
133136
break;
134137
}
135138

@@ -161,14 +164,17 @@ fn read_unbuffered(
161164
let _ = state.sink.write(b"*");
162165
}
163166
} else {
164-
return Err(Error::new(
165-
ErrorKind::OutOfMemory,
166-
"incorrect password attempt",
167-
));
167+
return Err(PamError::IncorrectPasswordAttempt);
168168
}
169169
}
170170

171-
Ok(password)
171+
if state.pw_len == 0 {
172+
// In case of EOF or Ctrl-D we don't want to ask for a password a second
173+
// time, so return an error.
174+
Err(PamError::NoPasswordProvided)
175+
} else {
176+
Ok(password)
177+
}
172178
}
173179

174180
/// Write something and immediately flush
@@ -251,14 +257,15 @@ pub enum Terminal<'a> {
251257

252258
impl Terminal<'_> {
253259
/// Open the current TTY for user communication
254-
pub fn open_tty() -> io::Result<Self> {
260+
pub fn open_tty() -> PamResult<Self> {
255261
// control ourselves that we are really talking to a TTY
256262
// mitigates: https://marc.info/?l=oss-security&m=168164424404224
257263
Ok(Terminal::Tty(
258264
fs::OpenOptions::new()
259265
.read(true)
260266
.write(true)
261-
.open("/dev/tty")?,
267+
.open("/dev/tty")
268+
.map_err(|_| PamError::TtyRequired)?,
262269
))
263270
}
264271

@@ -273,7 +280,7 @@ impl Terminal<'_> {
273280
prompt: &str,
274281
timeout: Option<Duration>,
275282
hidden: Hidden<()>,
276-
) -> io::Result<PamBuffer> {
283+
) -> PamResult<PamBuffer> {
277284
fn do_hide_input(
278285
hidden: Hidden<()>,
279286
input: BorrowedFd,

test-framework/sudo-compliance-tests/src/su.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ fn required_password_is_target_users_fail() {
9898
let diagnostic = if sudo_test::is_original_sudo() {
9999
"Authentication failure"
100100
} else {
101-
"Maximum 3 incorrect authentication attempts"
101+
"Authentication failed, try again."
102102
};
103103
assert_contains!(output.stderr(), diagnostic);
104104
}
@@ -131,7 +131,7 @@ fn password_is_required_when_target_user_is_self() {
131131
let diagnostic = if sudo_test::is_original_sudo() {
132132
"Authentication failure"
133133
} else {
134-
"Maximum 3 incorrect authentication attempts"
134+
"Authentication required but not attempted"
135135
};
136136
assert_contains!(output.stderr(), diagnostic);
137137
}

test-framework/sudo-compliance-tests/src/su/syslog.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,16 @@ fn logs_every_failed_authentication_attempt() {
8585

8686
eprintln!("\n--- /var/log/auth.log ---\n{auth_log}\n--- /var/log/auth.log ---\n");
8787

88-
if sudo_test::is_original_sudo() {
89-
assert_contains!(
90-
auth_log,
91-
format!("su: pam_unix(su:auth): auth could not identify password for [{target_user}]")
92-
);
88+
assert_contains!(
89+
auth_log,
90+
format!("su: pam_unix(su:auth): auth could not identify password for [{target_user}]")
91+
);
9392

93+
if sudo_test::is_original_sudo() {
9494
let tty = "none";
9595
assert_contains!(
9696
auth_log,
9797
format!("FAILED SU (to {target_user}) {invoking_user} on {tty}")
9898
);
99-
} else {
100-
let tty = "";
101-
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}"));
10299
}
103100
}

test-framework/sudo-compliance-tests/src/sudo/flag_list/credential_caching.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ fn flag_reset_timestamp() {
8787
let diagnostic = if sudo_test::is_original_sudo() {
8888
"sudo: a password is required"
8989
} else {
90-
"sudo: Authentication failed"
90+
"sudo: A terminal is required to authenticate"
9191
};
9292
assert_contains!(output.stderr(), diagnostic);
9393
}

test-framework/sudo-compliance-tests/src/sudo/nopasswd.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,6 @@ fn v_flag_without_pwd_fails_if_nopasswd_is_not_set_for_all_users_entries() {
141141
);
142142
}
143143
} else {
144-
assert_contains!(
145-
stderr,
146-
"[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"
147-
);
144+
assert_contains!(stderr, "Authentication required but not attempted");
148145
}
149146
}

test-framework/sudo-compliance-tests/src/sudo/pass_auth/stdin.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn incorrect_password() {
3434
let diagnostic = if sudo_test::is_original_sudo() {
3535
"incorrect password attempt"
3636
} else {
37-
"incorrect authentication attempt"
37+
"Authentication failed, try again."
3838
};
3939
assert_contains!(output.stderr(), diagnostic);
4040
}
@@ -54,7 +54,7 @@ fn no_password() {
5454
let diagnostic = if sudo_test::is_original_sudo() {
5555
"no password was provided"
5656
} else {
57-
"incorrect authentication attempt"
57+
"Authentication required but not attempted"
5858
};
5959
assert_contains!(output.stderr(), diagnostic);
6060
}
@@ -96,7 +96,7 @@ fn input_longer_than_max_pam_response_size_is_handled_gracefully() {
9696
assert_contains!(stderr, "sudo: 2 incorrect password attempts");
9797
}
9898
} else {
99-
assert_contains!(stderr, "incorrect authentication attempt");
99+
assert_contains!(stderr, "Incorrect authentication attempt");
100100
assert_not_contains!(stderr, "panic");
101101
}
102102
}
@@ -124,7 +124,7 @@ fn input_longer_than_password_should_not_be_accepted_as_correct_password() {
124124
if sudo_test::is_original_sudo() {
125125
assert_contains!(stderr, "sudo: 1 incorrect password attempt");
126126
} else {
127-
assert_contains!(stderr, "incorrect authentication attempt");
127+
assert_contains!(stderr, "Incorrect authentication attempt");
128128
}
129129
}
130130
}

test-framework/sudo-compliance-tests/src/sudo/pass_auth/tty.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ fn no_tty() {
5252
let diagnostic = if sudo_test::is_original_sudo() {
5353
"a terminal is required to read the password"
5454
} else {
55-
"Maximum 3 incorrect authentication attempts"
55+
"A terminal is required to authenticate"
5656
};
5757
assert_contains!(output.stderr(), diagnostic);
5858
}
@@ -96,7 +96,7 @@ fn input_longer_than_password_should_not_be_accepted_as_correct_password() {
9696
let diagnostic = if sudo_test::is_original_sudo() {
9797
"sudo: 1 incorrect password attempt"
9898
} else {
99-
"Authentication failed, try again"
99+
"Incorrect authentication attempt"
100100
};
101101
assert_contains!(stderr, diagnostic);
102102
}

0 commit comments

Comments
 (0)