Skip to content

Commit 6d3b14a

Browse files
authored
Don't open /dev/tty twice (#1344)
2 parents 440fbc7 + ff0dd78 commit 6d3b14a

File tree

2 files changed

+60
-63
lines changed

2 files changed

+60
-63
lines changed

src/pam/converse.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::io;
22

33
use crate::cutils::string_from_ptr;
4+
use crate::pam::rpassword::Hidden;
45
use crate::system::{
56
signal::{self, SignalSet},
67
time::Duration,
@@ -142,21 +143,27 @@ impl CLIConverser {
142143
impl Converser for CLIConverser {
143144
fn handle_normal_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
144145
let (mut tty, _guard) = self.open()?;
145-
tty.prompt(&format!("[{}: input needed] {msg} ", self.name))?;
146-
Ok(tty.read_cleartext()?)
146+
Ok(tty.read_input(
147+
&format!("[{}: input needed] {msg} ", self.name),
148+
None,
149+
Hidden::No,
150+
)?)
147151
}
148152

149153
fn handle_hidden_prompt(&self, msg: &str) -> PamResult<PamBuffer> {
150154
let (mut tty, _guard) = self.open()?;
151155
if self.bell && !self.use_stdin {
152156
tty.bell()?;
153157
}
154-
tty.prompt(msg)?;
155-
if self.password_feedback {
156-
tty.read_password_with_feedback(self.password_timeout)
157-
} else {
158-
tty.read_password(self.password_timeout)
159-
}
158+
tty.read_input(
159+
msg,
160+
self.password_timeout,
161+
if self.password_feedback {
162+
Hidden::WithFeedback(())
163+
} else {
164+
Hidden::Yes(())
165+
},
166+
)
160167
.map_err(|err| {
161168
if let io::ErrorKind::TimedOut = err.kind() {
162169
PamError::TimedOut

src/pam/rpassword.rs

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,23 @@ use std::{fs, mem};
2020

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

23-
use crate::cutils::cerr;
23+
use crate::cutils::{cerr, safe_isatty};
2424
use crate::system::time::Duration;
2525

2626
use super::securemem::PamBuffer;
2727

28-
struct HiddenInput {
29-
tty: fs::File,
28+
struct HiddenInput<'a> {
29+
tty: BorrowedFd<'a>,
3030
term_orig: termios,
3131
}
3232

33-
impl HiddenInput {
34-
fn new() -> io::Result<Option<HiddenInput>> {
35-
// control ourselves that we are really talking to a TTY
36-
// mitigates: https://marc.info/?l=oss-security&m=168164424404224
37-
let Ok(tty) = fs::File::open("/dev/tty") else {
38-
// if we have nothing to show, we have nothing to hide
39-
return Ok(None);
40-
};
41-
33+
impl HiddenInput<'_> {
34+
fn new(tty: BorrowedFd) -> io::Result<HiddenInput> {
4235
// Make two copies of the terminal settings. The first one will be modified
4336
// and the second one will act as a backup for when we want to set the
4437
// terminal back to its original state.
45-
let mut term = safe_tcgetattr(&tty)?;
46-
let term_orig = safe_tcgetattr(&tty)?;
38+
let mut term = safe_tcgetattr(tty)?;
39+
let term_orig = safe_tcgetattr(tty)?;
4740

4841
// Hide the password. This is what makes this function useful.
4942
term.c_lflag &= !ECHO;
@@ -58,11 +51,11 @@ impl HiddenInput {
5851
// SAFETY: we are passing tcsetattr a valid file descriptor and pointer-to-struct
5952
cerr(unsafe { tcsetattr(tty.as_raw_fd(), TCSANOW, &term) })?;
6053

61-
Ok(Some(HiddenInput { tty, term_orig }))
54+
Ok(HiddenInput { tty, term_orig })
6255
}
6356
}
6457

65-
impl Drop for HiddenInput {
58+
impl Drop for HiddenInput<'_> {
6659
fn drop(&mut self) {
6760
// Set the the mode back to normal
6861
// SAFETY: we are passing tcsetattr a valid file descriptor and pointer-to-struct
@@ -89,17 +82,17 @@ fn erase_feedback(sink: &mut dyn io::Write, i: usize) {
8982
}
9083
}
9184

92-
enum Hidden<'a> {
85+
pub(super) enum Hidden<T> {
9386
No,
94-
Yes(&'a HiddenInput),
95-
WithFeedback(&'a HiddenInput),
87+
Yes(T),
88+
WithFeedback(T),
9689
}
9790

9891
/// Reads a password from the given file descriptor while optionally showing feedback to the user.
9992
fn read_unbuffered(
10093
source: &mut dyn io::Read,
10194
sink: &mut dyn io::Write,
102-
hide_input: Hidden<'_>,
95+
hide_input: &Hidden<HiddenInput>,
10396
) -> io::Result<PamBuffer> {
10497
struct ExitGuard<'a> {
10598
pw_len: usize,
@@ -259,6 +252,8 @@ pub enum Terminal<'a> {
259252
impl Terminal<'_> {
260253
/// Open the current TTY for user communication
261254
pub fn open_tty() -> io::Result<Self> {
255+
// control ourselves that we are really talking to a TTY
256+
// mitigates: https://marc.info/?l=oss-security&m=168164424404224
262257
Ok(Terminal::Tty(
263258
fs::OpenOptions::new()
264259
.read(true)
@@ -272,48 +267,41 @@ impl Terminal<'_> {
272267
Ok(Terminal::StdIE(io::stdin().lock(), io::stderr().lock()))
273268
}
274269

275-
/// Reads input with TTY echo disabled
276-
pub fn read_password(&mut self, timeout: Option<Duration>) -> io::Result<PamBuffer> {
277-
let hide_input = HiddenInput::new()?;
278-
self.read_inner(
279-
timeout,
280-
hide_input.as_ref().map(Hidden::Yes).unwrap_or(Hidden::No),
281-
)
282-
}
283-
284-
/// Reads input with TTY echo disabled, but do provide visual feedback while typing.
285-
pub fn read_password_with_feedback(
270+
/// Reads input with TTY echo and visual feedback set according to the `hidden` parameter.
271+
pub(super) fn read_input(
286272
&mut self,
273+
prompt: &str,
287274
timeout: Option<Duration>,
275+
hidden: Hidden<()>,
288276
) -> io::Result<PamBuffer> {
289-
let hide_input = HiddenInput::new()?;
290-
self.read_inner(
291-
timeout,
292-
hide_input
293-
.as_ref()
294-
.map(Hidden::WithFeedback)
295-
.unwrap_or(Hidden::No),
296-
)
297-
}
298-
299-
/// Reads input with TTY echo enabled
300-
pub fn read_cleartext(&mut self) -> io::Result<PamBuffer> {
301-
self.read_inner(None, Hidden::No)
302-
}
277+
fn do_hide_input(
278+
hidden: Hidden<()>,
279+
input: BorrowedFd,
280+
) -> Result<Hidden<HiddenInput>, io::Error> {
281+
Ok(match hidden {
282+
// If input is not a tty, we can't hide feedback.
283+
_ if !safe_isatty(input) => Hidden::No,
284+
285+
Hidden::No => Hidden::No,
286+
Hidden::Yes(()) => Hidden::Yes(HiddenInput::new(input)?),
287+
Hidden::WithFeedback(()) => Hidden::WithFeedback(HiddenInput::new(input)?),
288+
})
289+
}
303290

304-
fn read_inner(
305-
&mut self,
306-
timeout: Option<Duration>,
307-
hide_input: Hidden<'_>,
308-
) -> io::Result<PamBuffer> {
309291
match self {
310292
Terminal::StdIE(stdin, stdout) => {
293+
write_unbuffered(stdout, prompt.as_bytes())?;
294+
295+
let hide_input = do_hide_input(hidden, stdin.as_fd())?;
311296
let mut reader = TimeoutRead::new(stdin.as_fd(), timeout);
312-
read_unbuffered(&mut reader, stdout, hide_input)
297+
read_unbuffered(&mut reader, stdout, &hide_input)
313298
}
314299
Terminal::Tty(file) => {
300+
write_unbuffered(file, prompt.as_bytes())?;
301+
302+
let hide_input = do_hide_input(hidden, file.as_fd())?;
315303
let mut reader = TimeoutRead::new(file.as_fd(), timeout);
316-
read_unbuffered(&mut reader, &mut &*file, hide_input)
304+
read_unbuffered(&mut reader, &mut &*file, &hide_input)
317305
}
318306
}
319307
}
@@ -346,7 +334,7 @@ mod test {
346334
fn miri_test_read() {
347335
let mut data = "password123\nhello world".as_bytes();
348336
let mut stdout = Vec::new();
349-
let buf = read_unbuffered(&mut data, &mut stdout, Hidden::No).unwrap();
337+
let buf = read_unbuffered(&mut data, &mut stdout, &Hidden::No).unwrap();
350338
// check that the \n is not part of input
351339
assert_eq!(
352340
buf.iter()
@@ -362,8 +350,10 @@ mod test {
362350
#[test]
363351
fn miri_test_longpwd() {
364352
let mut stdout = Vec::new();
365-
assert!(read_unbuffered(&mut "a".repeat(511).as_bytes(), &mut stdout, Hidden::No).is_ok());
366-
assert!(read_unbuffered(&mut "a".repeat(512).as_bytes(), &mut stdout, Hidden::No).is_err());
353+
assert!(read_unbuffered(&mut "a".repeat(511).as_bytes(), &mut stdout, &Hidden::No).is_ok());
354+
assert!(
355+
read_unbuffered(&mut "a".repeat(512).as_bytes(), &mut stdout, &Hidden::No).is_err()
356+
);
367357
}
368358

369359
#[test]

0 commit comments

Comments
 (0)