Skip to content

Commit 19778b1

Browse files
authored
Implement passwd_timeout (#1165)
2 parents 55846ef + e5c7516 commit 19778b1

File tree

14 files changed

+300
-22
lines changed

14 files changed

+300
-22
lines changed

src/defaults/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ defaults! {
5252

5353
verifypw = all (!= never) [all, always, any, never] #ignored
5454

55+
passwd_timeout = (5*60) (!= 0) {fractional_minutes}
5556
timestamp_timeout = (15*60) (!= 0) {fractional_minutes}
5657

5758
env_keep = ["COLORS", "DISPLAY", "HOSTNAME", "KRB5CCNAME", "LS_COLORS", "PATH",

src/pam/converse.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use std::io;
2+
13
use crate::cutils::string_from_ptr;
4+
use crate::system::time::Duration;
25

36
use super::sys::*;
47

@@ -98,6 +101,7 @@ pub struct CLIConverser {
98101
pub(super) use_stdin: bool,
99102
pub(super) bell: bool,
100103
pub(super) password_feedback: bool,
104+
pub(super) password_timeout: Option<Duration>,
101105
}
102106

103107
use rpassword::Terminal;
@@ -126,10 +130,17 @@ impl Converser for CLIConverser {
126130
}
127131
tty.prompt(msg)?;
128132
if self.password_feedback {
129-
Ok(tty.read_password_with_feedback()?)
133+
tty.read_password_with_feedback(self.password_timeout)
130134
} else {
131-
Ok(tty.read_password()?)
135+
tty.read_password(self.password_timeout)
132136
}
137+
.map_err(|err| {
138+
if let io::ErrorKind::TimedOut = err.kind() {
139+
PamError::TimedOut
140+
} else {
141+
PamError::IoError(err)
142+
}
143+
})
133144
}
134145

135146
fn handle_error(&self, msg: &str) -> PamResult<()> {
@@ -149,6 +160,10 @@ pub(super) struct ConverserData<C> {
149160
pub(super) converser_name: String,
150161
pub(super) no_interact: bool,
151162
pub(super) auth_prompt: Option<String>,
163+
// pam_authenticate does not return error codes returned by the conversation
164+
// function; these are set by the conversation function instead of returning
165+
// multiple error codes.
166+
pub(super) timed_out: bool,
152167
pub(super) panicked: bool,
153168
}
154169

@@ -194,11 +209,16 @@ pub(super) unsafe extern "C" fn converse<C: Converser>(
194209
// send the conversation off to the Rust part
195210
// SAFETY: appdata_ptr contains the `*mut ConverserData` that is untouched by PAM
196211
let app_data = unsafe { &mut *(appdata_ptr as *mut ConverserData<C>) };
197-
let Ok(resp_buf) = handle_message(app_data, style, &msg) else {
198-
return PamErrorType::ConversationError;
199-
};
200-
201-
resp_bufs.push(resp_buf);
212+
match handle_message(app_data, style, &msg) {
213+
Ok(resp_buf) => {
214+
resp_bufs.push(resp_buf);
215+
}
216+
Err(PamError::TimedOut) => {
217+
app_data.timed_out = true;
218+
return PamErrorType::ConversationError;
219+
}
220+
Err(_) => return PamErrorType::ConversationError,
221+
}
202222
}
203223

204224
// Allocate enough memory for the responses, which are initialized with zero.
@@ -374,6 +394,7 @@ mod test {
374394
converser_name: "tux".to_string(),
375395
no_interact: false,
376396
auth_prompt: Some("authenticate".to_owned()),
397+
timed_out: false,
377398
panicked: false,
378399
});
379400
let cookie = PamConvBorrow::new(hello.as_mut());

src/pam/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ pub enum PamError {
175175
IoError(std::io::Error),
176176
EnvListFailure,
177177
InteractionRequired,
178+
TimedOut,
178179
InvalidUser(String, String),
179180
}
180181

@@ -222,6 +223,7 @@ impl fmt::Display for PamError {
222223
)
223224
}
224225
PamError::InteractionRequired => write!(f, "Interaction is required"),
226+
PamError::TimedOut => write!(f, "timed out"),
225227
PamError::InvalidUser(username, other_user) => {
226228
write!(
227229
f,

src/pam/mod.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::{
66
ptr::NonNull,
77
};
88

9+
use crate::system::time::Duration;
10+
911
use converse::ConverserData;
1012
use error::pam_err;
1113
pub use error::{PamError, PamErrorType, PamResult};
@@ -43,20 +45,23 @@ impl PamContext {
4345
/// username.
4446
///
4547
/// This function will error when initialization of the PAM session somehow failed.
48+
#[allow(clippy::too_many_arguments)]
4649
pub fn new_cli(
4750
converser_name: &str,
4851
service_name: &str,
4952
use_stdin: bool,
5053
bell: bool,
5154
no_interact: bool,
5255
password_feedback: bool,
56+
password_timeout: Option<Duration>,
5357
target_user: Option<&str>,
5458
) -> PamResult<PamContext> {
5559
let converser = CLIConverser {
5660
bell,
5761
name: converser_name.to_owned(),
5862
use_stdin,
5963
password_feedback,
64+
password_timeout,
6065
};
6166

6267
let c_service_name = CString::new(service_name)?;
@@ -75,6 +80,7 @@ impl PamContext {
7580
.ok()
7681
.filter(|s| !s.is_empty())
7782
.or(Some("authenticate".to_owned())),
83+
timed_out: false,
7884
panicked: false,
7985
}));
8086

@@ -151,12 +157,22 @@ impl PamContext {
151157
flags |= self.disallow_null_auth_token_flag();
152158

153159
// SAFETY: `self.pamh` contains a correct handle (obtained from `pam_start`)
154-
pam_err(unsafe { pam_authenticate(self.pamh, flags) })?;
160+
let auth_res = pam_err(unsafe { pam_authenticate(self.pamh, flags) });
155161

156162
if self.has_panicked() {
157163
panic!("Panic during pam authentication");
158164
}
159165

166+
// SAFETY: self.data_ptr was created by Box::into_raw
167+
if unsafe { (*self.data_ptr).timed_out } {
168+
return Err(PamError::TimedOut);
169+
}
170+
171+
#[allow(clippy::question_mark)]
172+
if let Err(err) = auth_res {
173+
return Err(err);
174+
}
175+
160176
// Check that no PAM module changed the user.
161177
match self.get_user() {
162178
Ok(pam_user) => {

src/pam/rpassword.rs

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
/// (although much more robust than in the original code)
1515
///
1616
use std::io::{self, Error, ErrorKind, Read};
17-
use std::os::fd::{AsFd, AsRawFd};
17+
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
18+
use std::time::Instant;
1819
use std::{fs, mem};
1920

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

2223
use crate::cutils::cerr;
24+
use crate::system::time::Duration;
2325

2426
use super::securemem::PamBuffer;
2527

@@ -177,6 +179,66 @@ fn write_unbuffered(sink: &mut dyn io::Write, text: &[u8]) -> io::Result<()> {
177179
sink.flush()
178180
}
179181

182+
struct TimeoutRead<'a> {
183+
timeout_at: Option<Instant>,
184+
fd: BorrowedFd<'a>,
185+
}
186+
187+
impl<'a> TimeoutRead<'a> {
188+
fn new(fd: BorrowedFd<'a>, timeout: Option<Duration>) -> TimeoutRead<'a> {
189+
TimeoutRead {
190+
timeout_at: timeout.map(|timeout| Instant::now() + timeout.into()),
191+
fd,
192+
}
193+
}
194+
}
195+
196+
impl io::Read for TimeoutRead<'_> {
197+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
198+
let pollmask = libc::POLLIN | libc::POLLRDHUP;
199+
200+
let mut pollfd = [libc::pollfd {
201+
fd: self.fd.as_raw_fd(),
202+
events: pollmask,
203+
revents: 0,
204+
}; 1];
205+
206+
let timeout = match self.timeout_at {
207+
Some(timeout_at) => {
208+
let now = Instant::now();
209+
if now > timeout_at {
210+
return Err(io::Error::from(ErrorKind::TimedOut));
211+
}
212+
213+
(timeout_at - now)
214+
.as_millis()
215+
.try_into()
216+
.unwrap_or(i32::MAX)
217+
}
218+
None => -1,
219+
};
220+
221+
// SAFETY: pollfd is initialized and its length matches
222+
cerr(unsafe { libc::poll(pollfd.as_mut_ptr(), pollfd.len() as u64, timeout) })?;
223+
224+
// There may yet be data waiting to be read even if POLLHUP is set.
225+
if pollfd[0].revents & (pollmask | libc::POLLHUP) > 0 {
226+
// SAFETY: buf is initialized and its length matches
227+
let ret = cerr(unsafe {
228+
libc::read(
229+
self.fd.as_raw_fd(),
230+
buf.as_mut_ptr() as *mut libc::c_void,
231+
buf.len(),
232+
)
233+
})?;
234+
235+
Ok(ret as usize)
236+
} else {
237+
Err(io::Error::from(io::ErrorKind::TimedOut))
238+
}
239+
}
240+
}
241+
180242
/// A data structure representing either /dev/tty or /dev/stdin+stderr
181243
pub enum Terminal<'a> {
182244
Tty(fs::File),
@@ -200,21 +262,27 @@ impl Terminal<'_> {
200262
}
201263

202264
/// Reads input with TTY echo disabled
203-
pub fn read_password(&mut self) -> io::Result<PamBuffer> {
204-
let input = self.source();
265+
pub fn read_password(&mut self, timeout: Option<Duration>) -> io::Result<PamBuffer> {
266+
let mut input = self.source_timeout(timeout);
205267
let _hide_input = HiddenInput::new(false)?;
206-
read_unbuffered(input)
268+
read_unbuffered(&mut input)
207269
}
208270

209271
/// Reads input with TTY echo disabled, but do provide visual feedback while typing.
210-
pub fn read_password_with_feedback(&mut self) -> io::Result<PamBuffer> {
211-
if let Some(hide_input) = HiddenInput::new(true)? {
212-
match self {
213-
Terminal::StdIE(x, y) => read_unbuffered_with_feedback(x, y, &hide_input),
214-
Terminal::Tty(x) => read_unbuffered_with_feedback(&mut &*x, &mut &*x, &hide_input),
272+
pub fn read_password_with_feedback(
273+
&mut self,
274+
timeout: Option<Duration>,
275+
) -> io::Result<PamBuffer> {
276+
match (HiddenInput::new(true)?, self) {
277+
(Some(hide_input), Terminal::StdIE(stdin, stdout)) => {
278+
let mut reader = TimeoutRead::new(stdin.as_fd(), timeout);
279+
read_unbuffered_with_feedback(&mut reader, stdout, &hide_input)
215280
}
216-
} else {
217-
read_unbuffered(self.source())
281+
(Some(hide_input), Terminal::Tty(file)) => {
282+
let mut reader = TimeoutRead::new(file.as_fd(), timeout);
283+
read_unbuffered_with_feedback(&mut reader, &mut &*file, &hide_input)
284+
}
285+
(None, term) => read_unbuffered(&mut term.source_timeout(timeout)),
218286
}
219287
}
220288

@@ -242,6 +310,13 @@ impl Terminal<'_> {
242310
}
243311
}
244312

313+
fn source_timeout(&self, timeout: Option<Duration>) -> TimeoutRead {
314+
match self {
315+
Terminal::StdIE(stdin, _) => TimeoutRead::new(stdin.as_fd(), timeout),
316+
Terminal::Tty(file) => TimeoutRead::new(file.as_fd(), timeout),
317+
}
318+
}
319+
245320
fn sink(&mut self) -> &mut dyn io::Write {
246321
match self {
247322
Terminal::StdIE(_, x) => x,

src/su/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,16 @@ fn authenticate(requesting_user: &str, user: &str, login: bool) -> Result<PamCon
2929
"su"
3030
};
3131
let use_stdin = true;
32-
let mut pam = PamContext::new_cli("su", context, use_stdin, false, false, false, Some(user))?;
32+
let mut pam = PamContext::new_cli(
33+
"su",
34+
context,
35+
use_stdin,
36+
false,
37+
false,
38+
false,
39+
None,
40+
Some(user),
41+
)?;
3342
pam.set_requesting_user(requesting_user)?;
3443

3544
// attempt to set the TTY this session is communicating on

src/sudo/pam.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ use crate::common::context::LaunchType;
44
use crate::common::error::Error;
55
use crate::log::{dev_info, user_warn};
66
use crate::pam::{PamContext, PamError, PamErrorType, PamResult};
7-
use crate::system::term::current_tty_name;
7+
use crate::system::{term::current_tty_name, time::Duration};
88

99
pub(super) struct InitPamArgs<'a> {
1010
pub(super) launch: LaunchType,
1111
pub(super) use_stdin: bool,
1212
pub(super) bell: bool,
1313
pub(super) non_interactive: bool,
1414
pub(super) password_feedback: bool,
15+
pub(super) password_timeout: Option<Duration>,
1516
pub(super) auth_prompt: Option<String>,
1617
pub(super) auth_user: &'a str,
1718
pub(super) requesting_user: &'a str,
@@ -26,6 +27,7 @@ pub(super) fn init_pam(
2627
bell,
2728
non_interactive,
2829
password_feedback,
30+
password_timeout,
2931
auth_prompt,
3032
auth_user,
3133
requesting_user,
@@ -44,6 +46,7 @@ pub(super) fn init_pam(
4446
bell,
4547
non_interactive,
4648
password_feedback,
49+
password_timeout,
4750
Some(auth_user),
4851
)?;
4952
pam.mark_silent(matches!(launch, LaunchType::Direct));

src/sudo/pipeline.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ fn auth_and_update_record_file(
149149
must_authenticate,
150150
prior_validity,
151151
allowed_attempts,
152+
password_timeout,
152153
ref credential,
153154
pwfeedback,
154155
}: Authentication,
@@ -178,6 +179,7 @@ fn auth_and_update_record_file(
178179
bell: context.bell,
179180
non_interactive: context.non_interactive,
180181
password_feedback: pwfeedback,
182+
password_timeout,
181183
auth_prompt: context.prompt.clone(),
182184
auth_user: &auth_user.name,
183185
requesting_user: &context.current_user.name,

0 commit comments

Comments
 (0)