mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 21:32:05 +00:00
fix(prompt): prevent freeze when pasting >1023 bytes to stdin
When stdin is an interactive TTY in canonical mode (ICANON), input is line-buffered until newline with a kernel buffer limit of ~1024 bytes. Pasting more than this limit without a newline causes a deadlock: the terminal blocks on write (paste) while Bun blocks on read (waiting for newline). This fix adds a new TTY mode (mode 3) that disables canonical mode but keeps echo and signals enabled, providing user-friendly behavior while avoiding the buffer limit deadlock. The mode is applied to prompt(), confirm(), and alert() when stdin is a TTY. Fixes #26458 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -150,6 +150,21 @@ extern "C" int Bun__ttySetMode(int fd, int mode)
|
||||
case 2: // io
|
||||
uv__tty_make_raw(&tmp);
|
||||
|
||||
std::call_once(reset_once_flag, [] {
|
||||
Bun__atexit([] {
|
||||
uv_tty_reset_mode();
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 3: // prompt - disable canonical mode but keep echo and signals
|
||||
// This mode is used by prompt() to avoid deadlock when pasting large input.
|
||||
// In canonical mode, the terminal buffers input until newline (~1024 bytes limit).
|
||||
// When pasting more than the buffer size without newline, both read and write block.
|
||||
// Disabling ICANON allows immediate byte-by-byte reading while keeping user-friendly echo.
|
||||
tmp.c_lflag &= ~(ICANON);
|
||||
tmp.c_cc[VMIN] = 1;
|
||||
tmp.c_cc[VTIME] = 0;
|
||||
|
||||
std::call_once(reset_once_flag, [] {
|
||||
Bun__atexit([] {
|
||||
uv_tty_reset_mode();
|
||||
|
||||
@@ -45,6 +45,19 @@ fn alert(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSErr
|
||||
// * Not pertinent to use their complex system in a server context.
|
||||
bun.Output.flush();
|
||||
|
||||
// On POSIX, switch to non-canonical TTY mode to avoid deadlock when pasting large input.
|
||||
const stdin_is_tty = if (comptime Environment.isPosix) std.posix.isatty(std.posix.STDIN_FILENO) else false;
|
||||
if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 3);
|
||||
}
|
||||
}
|
||||
defer if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Optionally, pause while waiting for the user to acknowledge the message.
|
||||
var stdin = std.fs.File.stdin();
|
||||
var stdin_buf: [1]u8 = undefined;
|
||||
@@ -95,6 +108,19 @@ fn confirm(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSE
|
||||
// * Not relevant in a server context.
|
||||
bun.Output.flush();
|
||||
|
||||
// On POSIX, switch to non-canonical TTY mode to avoid deadlock when pasting large input.
|
||||
const stdin_is_tty = if (comptime Environment.isPosix) std.posix.isatty(std.posix.STDIN_FILENO) else false;
|
||||
if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 3);
|
||||
}
|
||||
}
|
||||
defer if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Pause until the user responds either positively or negatively.
|
||||
var stdin = std.fs.File.stdin();
|
||||
var stdin_buf: [1024]u8 = undefined;
|
||||
@@ -261,6 +287,22 @@ pub const prompt = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// On POSIX, switch to non-canonical TTY mode to avoid deadlock when pasting large input.
|
||||
// In canonical mode (ICANON), the terminal buffers input until newline with ~1024 byte limit.
|
||||
// When pasting more than the buffer size without newline, both read and write block.
|
||||
// Mode 3 disables ICANON but keeps echo and signals enabled for user-friendly behavior.
|
||||
const stdin_is_tty = if (comptime Environment.isPosix) std.posix.isatty(std.posix.STDIN_FILENO) else false;
|
||||
if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 3);
|
||||
}
|
||||
}
|
||||
defer if (comptime Environment.isPosix) {
|
||||
if (stdin_is_tty) {
|
||||
_ = Bun__ttySetMode(std.posix.STDIN_FILENO, 0);
|
||||
}
|
||||
};
|
||||
|
||||
// 7. Pause while waiting for the user's response.
|
||||
const reader = bun.Output.buffered_stdin.reader();
|
||||
var second_byte: ?u8 = null;
|
||||
@@ -351,3 +393,5 @@ const bun = @import("bun");
|
||||
const Environment = bun.Environment;
|
||||
const c = bun.c;
|
||||
const jsc = bun.jsc;
|
||||
|
||||
extern fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int;
|
||||
|
||||
97
test/regression/issue/26458.test.ts
Normal file
97
test/regression/issue/26458.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26458
|
||||
// Bun freezes when pasting >1023 bytes to stdin without trailing newline
|
||||
// This was caused by terminal canonical mode buffering (~1024 byte limit)
|
||||
// The fix disables ICANON during prompt/alert/confirm reads on POSIX systems
|
||||
//
|
||||
// Note: The actual freeze only occurs when stdin is an interactive TTY (not a pipe).
|
||||
// This test uses stdin: "pipe" so it can't reproduce the exact freeze scenario,
|
||||
// but it verifies that prompt/confirm/alert can handle large input correctly.
|
||||
|
||||
describe("stdin large input handling", () => {
|
||||
// Generate test data larger than the canonical mode buffer limit (~1024 bytes)
|
||||
const largeInput = "x".repeat(2048);
|
||||
|
||||
test("prompt() handles large input without hanging", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log(prompt('Enter:'))"],
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
// Write large input followed by newline
|
||||
proc.stdin!.write(largeInput + "\n");
|
||||
await proc.stdin!.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// The output should contain our large input (after the "Enter: " prompt)
|
||||
expect(stdout).toContain(largeInput);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("confirm() handles large input without hanging", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log(confirm('Confirm:'))"],
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
// Write large input (not starting with y/Y) followed by newline
|
||||
proc.stdin!.write(largeInput + "\n");
|
||||
await proc.stdin!.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// confirm() should return false for non-y input
|
||||
expect(stdout).toContain("false");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("alert() handles large input without hanging", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "alert('Hello'); console.log('done')"],
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
// Write large input followed by newline (alert just waits for Enter)
|
||||
proc.stdin!.write(largeInput + "\n");
|
||||
await proc.stdin!.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// alert() should complete and print "done"
|
||||
expect(stdout).toContain("done");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("prompt() handles very large input (10KB)", async () => {
|
||||
const veryLargeInput = "y".repeat(10240);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "-e", "console.log(prompt('Enter:').length)"],
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: bunEnv,
|
||||
});
|
||||
|
||||
proc.stdin!.write(veryLargeInput + "\n");
|
||||
await proc.stdin!.end();
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should output the length of our input
|
||||
expect(stdout).toContain("10240");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user