1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! A line read from the controlling terminal with echo off. Only
//! `/dev/tty` is read: stdin, a variable or an argument is never the
//! answer. The terminal's settings come back on every way out, a
//! signal included.
use std::cell::UnsafeCell;
use std::io::Write as _;
use std::mem::MaybeUninit;
use std::os::fd::AsRawFd as _;
use std::sync::atomic::{AtomicI32, Ordering};
use anyhow::{bail, Context, Result};
/// The longest line read, in bytes. A longer one is refused.
///
/// Kept below POSIX's guaranteed canonical-line floor (255), so
/// `MAX_LINE + 1` plus the trailing newline still reaches user space on
/// every tty and can be refused as too long instead of stalling in the
/// line discipline.
pub(crate) const MAX_LINE: usize = 240;
/// Signals that end the process while the prompt is up. Each restores the
/// terminal first, then takes its default action.
const FATAL: &[libc::c_int] = &[libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGQUIT];
/// Signals that would stop the process with echo off. They wait until
/// the prompt is down.
const STOPS: &[libc::c_int] = &[libc::SIGTSTP, libc::SIGTTIN, libc::SIGTTOU];
/// Bytes locked in memory, and zeroed when dropped. The buffer is
/// [`MAX_LINE`] long from the start, so a long line never moves it and
/// leaves a copy behind.
pub struct Secret(crate::memlock::Locked);
impl Secret {
fn new() -> Self {
Self(crate::memlock::Locked::with_capacity(MAX_LINE))
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.0
}
#[cfg(test)]
pub(crate) fn capacity(&self) -> usize {
self.0.capacity()
}
}
/// The controlling terminal, opened for reading and writing.
///
/// # Errors
///
/// This process has no controlling terminal.
pub fn open() -> Result<std::fs::File> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
.context("no controlling terminal (/dev/tty does not open)")
}
/// Turn echo off on `tty`, which drops anything typed ahead, write
/// `prompt`, read one line, and end it. `None` when the terminal closed
/// before a line ended.
///
/// # Errors
///
/// The terminal's settings cannot be read or set, a write or a read
/// failed, or the line is longer than [`MAX_LINE`].
pub fn ask(tty: &mut std::fs::File, prompt: &str) -> Result<Option<Secret>> {
let line = {
let _quiet = Quiet::enter(tty.as_raw_fd())?;
tty.write_all(prompt.as_bytes())?;
tty.flush()?;
read_line(tty.as_raw_fd())
};
let _ = tty.write_all(b"\n");
line
}
fn read_line(fd: libc::c_int) -> Result<Option<Secret>> {
let mut line = Secret::new();
loop {
let mut byte = 0u8;
// SAFETY: one byte into a local the call may write.
let n = unsafe { libc::read(fd, std::ptr::addr_of_mut!(byte).cast(), 1) };
if n < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
// A pty whose other side closed reads EIO.
if err.raw_os_error() == Some(libc::EIO) {
return Ok(None);
}
return Err(err).context("reading the terminal");
}
if n == 0 {
return Ok(None);
}
if byte == b'\n' || byte == b'\r' {
return Ok(Some(line));
}
if !line.0.push(byte) {
bail!("the line is longer than {MAX_LINE} bytes");
}
}
}
/// The settings to put back, readable from a signal handler.
struct Saved(UnsafeCell<MaybeUninit<libc::termios>>);
// SAFETY: written only while `RESTORE_FD` is -1, before it is published,
// and read only after a handler or `Quiet::drop` takes the fd.
unsafe impl Sync for Saved {}
static SAVED: Saved = Saved(UnsafeCell::new(MaybeUninit::uninit()));
/// The terminal a handler restores, or -1.
static RESTORE_FD: AtomicI32 = AtomicI32::new(-1);
extern "C" fn restore_and_die(sig: libc::c_int) {
let fd = RESTORE_FD.swap(-1, Ordering::SeqCst);
// SAFETY: tcsetattr, signal and raise are async-signal-safe, and SAVED
// was written before fd was published.
unsafe {
if fd >= 0 {
libc::tcsetattr(fd, libc::TCSANOW, (*SAVED.0.get()).as_ptr());
}
libc::signal(sig, libc::SIG_DFL);
libc::raise(sig);
}
}
/// Echo off on one terminal until dropped, with the fatal signals set to
/// restore it and the stop signals held.
struct Quiet {
fd: libc::c_int,
saved: libc::termios,
actions: Vec<(libc::c_int, libc::sigaction)>,
mask: libc::sigset_t,
}
impl Quiet {
fn enter(fd: libc::c_int) -> Result<Self> {
// SAFETY: termios and sigset_t are plain data; every call below gets
// valid pointers to locals or to SAVED, which no one else writes
// while RESTORE_FD is -1.
unsafe {
let mut saved: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut saved) != 0 {
return Err(std::io::Error::last_os_error())
.context("reading the terminal's settings");
}
let mut held: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut held);
for &sig in STOPS {
libc::sigaddset(&mut held, sig);
}
let mut mask: libc::sigset_t = std::mem::zeroed();
libc::pthread_sigmask(libc::SIG_BLOCK, &held, &mut mask);
(*SAVED.0.get()).write(saved);
RESTORE_FD.store(fd, Ordering::SeqCst);
let mut actions = Vec::with_capacity(FATAL.len());
for &sig in FATAL {
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = restore_and_die as extern "C" fn(libc::c_int) as usize;
libc::sigemptyset(&mut action.sa_mask);
let mut old: libc::sigaction = std::mem::zeroed();
if libc::sigaction(sig, &action, &mut old) == 0 {
actions.push((sig, old));
}
}
let quiet = Self {
fd,
saved,
actions,
mask,
};
let mut off = saved;
off.c_lflag &= !(libc::ECHO | libc::ECHOE | libc::ECHOK | libc::ECHONL);
// TCSANOW, not TCSAFLUSH. On macOS a flush waits until
// output drains, and a pty with no session never drains,
// so the prompt hangs. Type-ahead is dropped by reading
// what is already queued, which ends when the queue is empty.
if libc::tcsetattr(fd, libc::TCSANOW, &off) != 0 {
return Err(std::io::Error::last_os_error()).context("turning echo off");
}
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 {
return Err(std::io::Error::last_os_error()).context("reading the terminal flags");
}
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) != 0 {
return Err(std::io::Error::last_os_error()).context("dropping type-ahead");
}
let mut byte = 0u8;
loop {
let n = libc::read(fd, std::ptr::addr_of_mut!(byte).cast(), 1);
if n > 0 {
continue;
}
if n == 0 {
break;
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if err.raw_os_error() == Some(libc::EAGAIN)
|| err.raw_os_error() == Some(libc::EWOULDBLOCK)
{
break;
}
let _ = libc::fcntl(fd, libc::F_SETFL, flags);
return Err(err).context("dropping type-ahead");
}
if libc::fcntl(fd, libc::F_SETFL, flags) != 0 {
return Err(std::io::Error::last_os_error())
.context("restoring the terminal flags");
}
Ok(quiet)
}
}
}
impl Drop for Quiet {
fn drop(&mut self) {
// SAFETY: the same terminal and the settings read from it; the
// actions and the mask are the ones `enter` replaced.
unsafe {
RESTORE_FD.store(-1, Ordering::SeqCst);
libc::tcsetattr(self.fd, libc::TCSANOW, &self.saved);
for (sig, old) in &self.actions {
libc::sigaction(*sig, old, std::ptr::null_mut());
}
libc::pthread_sigmask(libc::SIG_SETMASK, &self.mask, std::ptr::null_mut());
}
}
}
#[cfg(test)]
mod tests {
use super::Secret;
/// Keep room for one byte past MAX_LINE and the trailing newline on
/// canonical ttys that only guarantee 255 bytes.
#[test]
fn max_line_fits_the_posix_canonical_floor() {
let max_line = Secret::new().capacity();
assert!(max_line + 2 <= 255, "MAX_LINE={max_line}");
}
}