fix(terminal): patch various autocommand-related holes

Problem: autocommands can cause various problems in terminal mode, which can
lead to crashes, for example.

Solution: fix found issues. Move some checks to terminal_check and guard against
autocommands messing with things. Trigger TermEnter/Leave after terminal mode
has changed/restored most state. Wipeout the correct buffer if TermLeave
switches buffers and fix a UAF if it or WinScrolled/Resized frees the terminal
prematurely.

These changes also allow us to remove the buffer restrictions on TextChangedT;
they were inadequate in stopping some issues, and WinScrolled/Resized was
lacking them anyway.
This commit is contained in:
Sean Dewar
2025-03-03 20:43:07 +00:00
committed by zeertzjq
parent 2eea65fe68
commit 46d68e5290
4 changed files with 193 additions and 67 deletions

View File

@@ -606,14 +606,14 @@ void terminal_close(Terminal **termpp, int status)
// If this was called by close_buffer() (status is -1), or if exiting, we
// must inform the buffer the terminal no longer exists so that
// close_buffer() won't call this again.
// If inside Terminal mode K_EVENT handling, setting buf_handle to 0 also
// If inside Terminal mode event handling, setting buf_handle to 0 also
// informs terminal_enter() to call the close callback before returning.
term->buf_handle = 0;
if (buf) {
buf->terminal = NULL;
}
if (!term->refcount) {
// Not inside Terminal mode K_EVENT handling.
// Not inside Terminal mode event handling.
// We should not wait for the user to press a key.
term->destroy = true;
term->opts.close_cb(term->opts.data);
@@ -777,12 +777,13 @@ bool terminal_enter(void)
adjust_topline(s->term, buf, 0); // scroll to end
showmode();
ui_cursor_shape();
apply_autocmds(EVENT_TERMENTER, NULL, NULL, false, curbuf);
may_trigger_modechanged();
// Tell the terminal it has focus
terminal_focus(s->term, true);
apply_autocmds(EVENT_TERMENTER, NULL, NULL, false, curbuf);
may_trigger_modechanged();
s->state.execute = terminal_execute;
s->state.check = terminal_check;
state_enter(&s->state);
@@ -797,8 +798,6 @@ bool terminal_enter(void)
ui_busy_stop();
}
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
// Restore the terminal cursor to what is set in 'guicursor'
(void)parse_shape_opt(SHAPE_CURSOR);
@@ -816,12 +815,19 @@ bool terminal_enter(void)
unshowmode(true);
}
ui_cursor_shape();
// If we're to close the terminal, don't let TermLeave autocommands free it first!
if (s->close) {
bool wipe = s->term->buf_handle != 0;
s->term->refcount++;
}
apply_autocmds(EVENT_TERMLEAVE, NULL, NULL, false, curbuf);
if (s->close) {
s->term->refcount--;
const handle_T buf_handle = s->term->buf_handle; // Callback may free s->term.
s->term->destroy = true;
s->term->opts.close_cb(s->term->opts.data);
if (wipe) {
do_cmdline_cmd("bwipeout!");
if (buf_handle != 0) {
do_buffer(DOBUF_WIPE, DOBUF_FIRST, FORWARD, buf_handle, true);
}
}
@@ -845,24 +851,68 @@ static void terminal_check_cursor(void)
coladvance(curwin, MAX(0, term->cursor.col + off));
}
// Function executed before each iteration of terminal mode.
// Return:
// 1 if the iteration should continue normally
// 0 if the main loop must exit
static bool terminal_check_focus(TerminalState *const s)
FUNC_ATTR_NONNULL_ALL
{
if (curbuf->terminal == NULL) {
return false;
}
if (s->save_curwin_handle != curwin->handle) {
// Terminal window changed, update window options.
unset_terminal_winopts(s);
set_terminal_winopts(s);
}
if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI.
terminal_focus(s->term, false);
s->term = curbuf->terminal;
s->term->pending.cursor = true;
invalidate_terminal(s->term, -1, -1);
terminal_focus(s->term, true);
}
return true;
}
/// Function executed before each iteration of terminal mode.
///
/// @return:
/// 1 if the iteration should continue normally
/// 0 if the main loop must exit
static int terminal_check(VimState *state)
{
TerminalState *const s = (TerminalState *)state;
if (stop_insert_mode) {
if (stop_insert_mode || !terminal_check_focus(s)) {
return 0;
}
assert(s->term == curbuf->terminal);
// Validate topline and cursor position for autocommands. Especially important for WinScrolled.
terminal_check_cursor();
validate_cursor(curwin);
const bool text_changed = must_redraw != 0;
show_cursor_info_later(false);
// Don't let autocommands free the terminal from under our fingers.
s->term->refcount++;
if (must_redraw) {
// TODO(seandewar): above changes will maybe change the behaviour of this more; untrollify this
apply_autocmds(EVENT_TEXTCHANGEDT, NULL, NULL, false, curbuf);
}
may_trigger_win_scrolled_resized();
s->term->refcount--;
if (s->term->buf_handle == 0) {
s->close = true;
return 0;
}
// Autocommands above may have changed focus, scrolled, or moved the cursor.
if (!terminal_check_focus(s)) {
return 0;
}
terminal_check_cursor();
validate_cursor(curwin);
show_cursor_info_later(false);
if (must_redraw) {
update_screen();
} else {
@@ -871,21 +921,6 @@ static int terminal_check(VimState *state)
showmode(); // clear cmdline and show mode
}
}
if (text_changed) {
// Make sure an invoked autocmd doesn't delete the buffer (and the
// terminal) under our fingers.
curbuf->b_locked++;
// save and restore curwin and curbuf, in case the autocmd changes them
aco_save_T aco;
aucmd_prepbuf(&aco, curbuf);
apply_autocmds(EVENT_TEXTCHANGEDT, NULL, NULL, false, curbuf);
aucmd_restbuf(&aco);
curbuf->b_locked--;
}
may_trigger_win_scrolled_resized();
setcursor();
refresh_cursor(s->term, &s->cursor_visible);
@@ -986,23 +1021,6 @@ static int terminal_execute(VimState *state, int key)
terminal_send_key(s->term, key);
}
if (curbuf->terminal == NULL) {
return 0;
}
if (s->save_curwin_handle != curwin->handle) {
// Terminal window changed, update window options.
unset_terminal_winopts(s);
set_terminal_winopts(s);
}
if (s->term != curbuf->terminal) {
// Active terminal buffer changed, flush terminal's cursor state to the UI
terminal_focus(s->term, false);
s->term = curbuf->terminal;
s->term->pending.cursor = true;
invalidate_terminal(s->term, -1, -1);
terminal_focus(s->term, true);
}
return 1;
}