From 732bf4db97d83d2874888cd6c1dacc7e9b422b81 Mon Sep 17 00:00:00 2001 From: prwang Date: Mon, 6 Jul 2026 12:37:02 +0000 Subject: [PATCH] fix(input): stops accepting input after a large paste/input burst #40585 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Neovim can permanently stop accepting keyboard input after a large paste, or after any sufficiently large input burst. The screen still redraws and honours window resize, but typed keys have no effect and the session must be killed. A paste that triggers the freeze is applied only partially. Root cause: `src/nvim/os/input.c` holds `input_buffer[INPUT_BUFFER_SIZE]` (16386 bytes), compacted left rather than used as a ring: - `input_get()` drains the buffer by advancing `input_read_pos`, but never rewinds the cursors when it empties (`input_read_pos == input_write_pos`). - The only rewind lives in `input_enqueue_raw()`, which `input_enqueue()` reaches only inside `while (input_space() >= 19 && ptr < end)` (19 is the maximum expansion of one `` key form). So once input fills the buffer to within 19 bytes of the top and is then fully drained, the cursors are pinned near the top with `input_space() < 19`. The gate never reopens, `input_enqueue()` never rewinds, and all further input is silently dropped: `input_available()` stays 0 and the editor blocks forever in `state_enter()` → `input_get()`. Redraw and resize run on independent paths, which is why the UI looks alive while the keyboard is ignored. Solution: Rewind the read/write cursors when the buffer is empty, at the start of `input_enqueue()`. The reset moves no data in the empty case and guarantees the space gate can reopen; the non-empty case self-heals as the editor drains. Test case: A reproducer (no terminal required; drives `nvim --embed` over msgpack-RPC via `nvim_input`) floods the input buffer, lets the editor drain it, then probes with `:qa!`. It shows a sharp threshold at `INPUT_BUFFER_SIZE − 18`: | `--fill` | master | with this fix | |----------|-----------|---------------| | ≤ 16367 | quits | quits | | ≥ 16368 | **hangs** | quits | - On current `master`, the freeze reproduces at `fill=16368`; the patched build quits for every fill up to 100000. - Interactive: a ~40 kB bracketed paste into Insert mode or a `:terminal` no longer freezes. --- src/nvim/os/input.c | 10 ++++++++++ test/functional/api/vim_spec.lua | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 29aa6d0cad..ef7d7428a1 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -315,6 +315,16 @@ size_t input_enqueue(uint64_t chan_id, String keys) { current_ui = chan_id; + // If the buffer has been fully drained, rewind the read/write cursors to the + // start to reclaim tail space. Otherwise a drained-but-not-rewound buffer left + // pinned within <19 bytes of the top (input_get() advances input_read_pos but + // never rewinds it) would fail the `input_space() >= 19` gate below forever, + // silently dropping all further input and freezing the editor. + if (input_read_pos == input_write_pos) { + input_read_pos = input_buffer; + input_write_pos = input_buffer; + } + if (keys.size > 0) { set_vim_var_nr(VV_USERACTIVE, (varnumber_T)os_realtime()); } diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 817684846f..3846cfd3ae 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -820,6 +820,23 @@ describe('API', function() command("call nvim_input('')") eq(1, eval('1')) end) + + it('keeps accepting input after the input buffer fills and drains', function() + -- Regression test: the fixed input buffer (INPUT_BUFFER_SIZE == 16386) is + -- compacted left, not a ring. Filling it to within <19 bytes of the top + -- and then draining it used to leave the read/write cursors pinned, so + -- input_enqueue() could never reclaim space and silently dropped all + -- further input, freezing the editor. One oversized burst of a Normal-mode + -- no-op (CTRL-L) saturates the buffer in a single nvim_input() call; once it + -- has drained, input must still register. poke_eventloop() waits for the + -- burst to be fully consumed before the probe: the fix only reclaims space + -- once the buffer is empty, so the probe must arrive after the drain. + api.nvim_input(('\12'):rep(20000)) + n.poke_eventloop() + api.nvim_input('ix') + n.poke_eventloop() + eq('x', api.nvim_get_current_line()) + end) end) describe('nvim_paste', function()