Skip to content

Commit 86eee28

Browse files
authored
fix(permissions): don't hang prompt when stdin is in raw mode (#34457)
When a library has put stdin into raw mode — e.g. ts-node calling `process.stdin.setRawMode(true)` from a REPL — the line-oriented permission prompt would hang forever. With `ICANON` cleared, Enter delivers `\r` rather than `\n` so `read_line` never returns, and with `ECHO` off the user can't even see they're typing into the void. From the program's perspective Deno is just frozen. Detect the raw-mode state via `tcgetattr` on Unix (and `GetConsoleMode` on Windows) before issuing the prompt; if `ICANON` / `ENABLE_LINE_INPUT` is cleared we print a clear message naming the requested permission and deny the request, so the program fails fast with a normal `NotCapable` error instead of appearing to hang. Closes #34399
1 parent 4cb40bf commit 86eee28

3 files changed

Lines changed: 95 additions & 0 deletions

File tree

runtime/permissions/prompter.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,54 @@ fn clear_n_lines(stderr_lock: &mut std::io::StderrLock, n: usize) {
308308
write!(stderr_lock, "\x1B[{n}A\x1B[0J").unwrap();
309309
}
310310

311+
/// Returns true if stdin's terminal line discipline has been put into raw
312+
/// mode by something else in this process — for example a Node.js library
313+
/// calling `process.stdin.setRawMode(true)`.
314+
///
315+
/// When that has happened our line-oriented `read_line()` prompt loop would
316+
/// hang forever (Enter delivers `\r` rather than `\n`, and ECHO is off so the
317+
/// user can't see they're typing), so we bail out instead.
318+
///
319+
/// We require *both* canonical input and echo to be disabled, matching what
320+
/// `setRaw`/`setRawMode` actually does (see `runtime/ops/tty.rs`). Clearing
321+
/// canonical mode alone does not trigger the hang as long as newlines are
322+
/// still delivered, and treating that as raw would misfire on setups that
323+
/// disable only canonical mode (such as the test PTY harness).
324+
#[cfg(unix)]
325+
fn stdin_is_raw_mode() -> bool {
326+
// SAFETY: tcgetattr on a possibly-invalid fd 0 returns -1; on any failure we
327+
// conservatively report not-raw.
328+
unsafe {
329+
let mut termios = std::mem::MaybeUninit::<libc::termios>::uninit();
330+
if libc::tcgetattr(libc::STDIN_FILENO, termios.as_mut_ptr()) != 0 {
331+
return false;
332+
}
333+
let termios = termios.assume_init();
334+
termios.c_lflag & (libc::ICANON | libc::ECHO) == 0
335+
}
336+
}
337+
338+
#[cfg(all(not(unix), not(target_arch = "wasm32")))]
339+
fn stdin_is_raw_mode() -> bool {
340+
use windows_sys::Win32::System::Console::ENABLE_ECHO_INPUT;
341+
use windows_sys::Win32::System::Console::ENABLE_LINE_INPUT;
342+
use windows_sys::Win32::System::Console::GetConsoleMode;
343+
use windows_sys::Win32::System::Console::GetStdHandle;
344+
use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
345+
346+
// SAFETY: winapi calls. GetConsoleMode returns 0 (FALSE) for non-console
347+
// handles (e.g. when stdin is a pipe), in which case we conservatively
348+
// return false.
349+
unsafe {
350+
let handle = GetStdHandle(STD_INPUT_HANDLE);
351+
let mut mode = 0u32;
352+
if GetConsoleMode(handle, &mut mode) == 0 {
353+
return false;
354+
}
355+
mode & (ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT) == 0
356+
}
357+
}
358+
311359
#[cfg(unix)]
312360
fn get_stdin_metadata() -> std::io::Result<std::fs::Metadata> {
313361
use std::os::fd::FromRawFd;
@@ -351,6 +399,26 @@ impl PermissionPrompter for TtyPrompter {
351399
return PromptResponse::Deny;
352400
};
353401

402+
// If stdin has been put into raw mode (e.g. a Node.js library has called
403+
// `process.stdin.setRawMode(true)`) our line-oriented prompt loop would
404+
// hang forever waiting for a `\n` that the terminal will never deliver,
405+
// and the user wouldn't see what they're typing either. Bail out with a
406+
// clear message so the program doesn't appear to freeze.
407+
#[allow(clippy::print_stderr, reason = "actually want to print")]
408+
if stdin_is_raw_mode() {
409+
// Escape the message/name since they can contain user-controlled strings
410+
// (env var names, file paths) that could otherwise spoof the terminal.
411+
eprintln!(
412+
"❌ Cannot prompt for {}: stdin is in raw mode (a library has likely called setRawMode).",
413+
escape_control_characters(message)
414+
);
415+
eprintln!(
416+
"❌ Run again with --allow-{} to grant the permission up front, or with -A to skip all prompts.",
417+
escape_control_characters(name)
418+
);
419+
return PromptResponse::Deny;
420+
}
421+
354422
#[allow(clippy::print_stderr, reason = "actually want to print")]
355423
if message.len() > MAX_PERMISSION_PROMPT_LENGTH {
356424
eprintln!(

tests/integration/run_tests.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,23 @@ fn permissions_prompt_allow_all_lowercase_a() {
389389
});
390390
}
391391

392+
// Regression test for https://github.com/denoland/deno/issues/34399
393+
// When stdin has been put into raw mode by user code (e.g. ts-node calling
394+
// `process.stdin.setRawMode(true)`), the prompt's line-buffered `read_line`
395+
// would hang because Enter delivers `\r` and ECHO is off. Verify we bail
396+
// out with a clear message and deny the permission instead.
397+
#[test(flaky)]
398+
fn permissions_prompt_raw_stdin() {
399+
TestContext::default()
400+
.new_command()
401+
.args_vec(["run", "--quiet", "run/permissions_prompt_raw_stdin.ts"])
402+
.with_pty(|mut console| {
403+
console.expect("stdin is in raw mode");
404+
console.expect("Run again with --allow-env");
405+
console.expect("STATUS: denied");
406+
});
407+
}
408+
392409
#[test(flaky)]
393410
fn permission_request_long() {
394411
TestContext::default()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Regression test for https://github.com/denoland/deno/issues/34399
2+
// When a library (e.g. ts-node) has put stdin into raw mode, the
3+
// permission prompt's line-buffered `read_line` would hang forever
4+
// because Enter delivers `\r` (no `\n`) and ECHO is off.
5+
//
6+
// Verify that the prompt instead bails out with a clear message and
7+
// denies the permission.
8+
Deno.stdin.setRaw(true);
9+
const status = Deno.permissions.requestSync({ name: "env", variable: "FOO" });
10+
console.log("STATUS:", status.state);

0 commit comments

Comments
 (0)