fix(api): nvim_get_option_value dummy buffer crashes

Problem: nvim_get_option_value with "filetype" set can crash if autocommands
open the dummy buffer in more windows, or if &bufhidden == "wipe".

Solution: Attempt to close all dummy buffer windows before wiping. Promote the
dummy buffer to a normal buffer if that fails.

(cherry picked from commit 7e2e116343)
This commit is contained in:
Sean Dewar
2025-05-09 16:33:48 +01:00
committed by github-actions[bot]
parent b0f341feea
commit 10a1df2789
2 changed files with 64 additions and 5 deletions

View File

@@ -19,6 +19,7 @@
#include "nvim/option.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "api/options.c.generated.h"
@@ -151,6 +152,27 @@ static buf_T *do_ft_buf(const char *filetype, aco_save_T *aco, bool *aco_used, E
return ftbuf;
}
static void wipe_ft_buf(buf_T *buf)
FUNC_ATTR_NONNULL_ALL
{
block_autocmds();
bufref_T bufref;
set_bufref(&bufref, buf);
close_windows(buf, false);
// Autocommands are blocked, but 'bufhidden' may have wiped it already.
// Also can't wipe if the buffer is somehow still in a window or current.
if (bufref_valid(&bufref) && buf != curbuf && buf->b_nwindows == 0) {
wipe_buffer(buf, false);
}
if (bufref_valid(&bufref)) {
buf->b_flags &= ~BF_DUMMY; // Couldn't wipe; keep it instead.
}
unblock_autocmds();
}
/// Gets the value of an option. The behavior of this function matches that of
/// |:set|: the local value of an option is returned if it exists; otherwise,
/// the global value is returned. Local values always correspond to the current
@@ -193,10 +215,8 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err)
aucmd_restbuf(&aco);
}
if (ftbuf != NULL) {
assert(curbuf != ftbuf); // safety check
wipe_buffer(ftbuf, false);
wipe_ft_buf(ftbuf);
}
return (Object)OBJECT_INIT;
}
@@ -212,8 +232,7 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err)
// restore curwin/curbuf and a few other things
aucmd_restbuf(&aco);
}
assert(curbuf != ftbuf); // safety check
wipe_buffer(ftbuf, false);
wipe_ft_buf(ftbuf);
}
if (ERROR_SET(err)) {

View File

@@ -2008,6 +2008,46 @@ describe('API', function()
)
end)
it('does not crash if autocmds open dummy buffer in other windows', function()
exec [[
autocmd FileType * ++once let g:dummy_buf = bufnr() | split
" Autocommands should be blocked while Nvim attempts to wipe the buffer.
let g:wipe_events = []
autocmd WinClosed * if winbufnr(expand('<amatch>')) == g:dummy_buf
\| let g:wipe_events += ['WinClosed']
\| endif
autocmd BufWipeout * if expand('<abuf>') == g:dummy_buf
\| let g:wipe_events += ['BufWipeout']
\| endif
]]
api.nvim_get_option_value('formatexpr', { filetype = 'lua' })
eq(0, eval('bufexists(g:dummy_buf)'))
eq({}, eval('win_findbuf(g:dummy_buf)'))
eq({}, eval('g:wipe_events'))
-- Be an ABSOLUTE nuisance and make it the only window to prevent it from wiping.
-- Do it this way to avoid E813 from :only trying to close the autocmd window.
command('autocmd FileType * ++once let g:dummy_buf = bufnr() | split | wincmd w | quit')
api.nvim_get_option_value('formatexpr', { filetype = 'lua' })
eq(1, eval('bufexists(g:dummy_buf)'))
-- Ensure the buffer does not remain as a dummy by checking that we can switch to it.
local old_win = api.nvim_get_current_win()
command('execute g:dummy_buf "sbuffer"')
eq(eval('g:dummy_buf'), api.nvim_get_current_buf())
neq(old_win, api.nvim_get_current_win())
eq({}, eval('g:wipe_events'))
end)
it('does not crash if dummy buffer wiped after autocommands', function()
-- Autocommands are blocked while Nvim attempts to wipe the buffer, but check something like
-- &bufhidden = "wipe" causing a premature wipe doesn't crash.
command('autocmd FileType * ++once setlocal bufhidden=wipe | split')
api.nvim_get_option_value('formatexpr', { filetype = 'lua' })
assert_alive()
end)
it('sets dummy buffer options without side-effects', function()
exec [[
let g:events = []