From 3387729a169d15cc4298dcd6dd0bd698dc6afe27 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 26 Jan 2026 11:56:36 +0000 Subject: [PATCH] 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 --- src/bun.js/bindings/wtf-bindings.cpp | 15 +++++ src/bun.js/webcore/prompt.zig | 44 +++++++++++++ test/regression/issue/26458.test.ts | 97 ++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 test/regression/issue/26458.test.ts diff --git a/src/bun.js/bindings/wtf-bindings.cpp b/src/bun.js/bindings/wtf-bindings.cpp index 144a6f5ea7..351e70710a 100644 --- a/src/bun.js/bindings/wtf-bindings.cpp +++ b/src/bun.js/bindings/wtf-bindings.cpp @@ -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(); diff --git a/src/bun.js/webcore/prompt.zig b/src/bun.js/webcore/prompt.zig index 747cd935fd..5aa2a517d0 100644 --- a/src/bun.js/webcore/prompt.zig +++ b/src/bun.js/webcore/prompt.zig @@ -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; diff --git a/test/regression/issue/26458.test.ts b/test/regression/issue/26458.test.ts new file mode 100644 index 0000000000..9c28ac6ce5 --- /dev/null +++ b/test/regression/issue/26458.test.ts @@ -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); + }); +});