commit 7e2e116343695b61ef8211a0c3eaea3456badef3
parent 40114d16318cb80282123b27df8eceb7d1069492
Author: Sean Dewar <6256228+seandewar@users.noreply.github.com>
Date: Fri, 9 May 2025 16:33:48 +0100
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.
Diffstat:
2 files changed, 64 insertions(+), 5 deletions(-)
diff --git a/src/nvim/api/options.c b/src/nvim/api/options.c
@@ -19,6 +19,7 @@
#include "nvim/option.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
+#include "nvim/window.h"
#include "api/options.c.generated.h"
@@ -149,6 +150,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
@@ -191,10 +213,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;
}
@@ -210,8 +230,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)) {
diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua
@@ -1995,6 +1995,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 = []