Merge pull request #27874 from luukvbaal/funcerr

fix(messages)!: vim.ui_attach message callbacks are unsafe
This commit is contained in:
bfredl 2024-11-14 18:56:56 +01:00 committed by GitHub
commit 05d354e216
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 215 additions and 141 deletions

View File

@ -1083,6 +1083,10 @@ vim.ui_attach({ns}, {options}, {callback}) *vim.ui_attach()*
|ui-popupmenu| and the sections below for event format for respective
events.
Callbacks for `msg_show` events are executed in |api-fast| context.
Excessive errors inside the callback will result in forced detachment.
WARNING: This api is considered experimental. Usability will vary for
different screen elements. In particular `ext_messages` behavior is
subject to further changes and usability improvements. This is expected to

View File

@ -72,7 +72,8 @@ EDITOR
EVENTS
• TODO
• |vim.ui_attach()| callbacks for |ui-messages| `msg_show` events are executed in
|api-fast| context.
LSP

View File

@ -233,6 +233,10 @@ function vim.wait(time, callback, interval, fast_only) end
--- {callback} receives event name plus additional parameters. See |ui-popupmenu|
--- and the sections below for event format for respective events.
---
--- Callbacks for `msg_show` events are executed in |api-fast| context.
---
--- Excessive errors inside the callback will result in forced detachment.
---
--- WARNING: This api is considered experimental. Usability will vary for
--- different screen elements. In particular `ext_messages` behavior is subject
--- to further changes and usability improvements. This is expected to be

View File

@ -159,7 +159,7 @@ void wildmenu_hide(void)
FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY;
void msg_show(String kind, Array content, Boolean replace_last)
FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY;
FUNC_API_SINCE(6) FUNC_API_FAST FUNC_API_REMOTE_ONLY;
void msg_clear(void)
FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY;
void msg_showcmd(Array content)

View File

@ -594,10 +594,12 @@ ArrayOf(String) nvim_get_runtime_file(String name, Boolean all, Arena *arena, Er
kvi_init(cookie.rv);
int flags = DIP_DIRFILE | (all ? DIP_ALL : 0);
TryState tstate;
try_enter(&tstate);
do_in_runtimepath((name.size ? name.data : ""), flags, find_runtime_cb, &cookie);
vim_ignored = try_leave(&tstate, err);
TRY_WRAP(err, {
do_in_runtimepath((name.size ? name.data : ""), flags, find_runtime_cb, &cookie);
});
return arena_take_arraybuilder(arena, &cookie.rv);
}

View File

@ -151,7 +151,7 @@ EXTERN const char e_auabort[] INIT(= N_("E855: Autocommands caused command to ab
EXTERN const char e_api_error[] INIT(= N_("E5555: API call: %s"));
EXTERN const char e_luv_api_disabled[] INIT(= N_("E5560: %s must not be called in a lua loop callback"));
EXTERN const char e_fast_api_disabled[] INIT(= N_("E5560: %s must not be called in a fast event context"));
EXTERN const char e_floatonly[] INIT(= N_("E5601: Cannot close window, only floating window would remain"));
EXTERN const char e_floatexchange[] INIT(= N_("E5602: Cannot exchange or rotate float"));

View File

@ -8616,7 +8616,7 @@ bool eval_has_provider(const char *feat, bool throw_if_fast)
}
if (throw_if_fast && !nlua_is_deferred_safe()) {
semsg(e_luv_api_disabled, "Vimscript function");
semsg(e_fast_api_disabled, "Vimscript function");
return false;
}

View File

@ -750,7 +750,7 @@ local function process_function(fn)
write_shifted_output(
[[
if (!nlua_is_deferred_safe()) {
return luaL_error(lstate, e_luv_api_disabled, "%s");
return luaL_error(lstate, e_fast_api_disabled, "%s");
}
]],
fn.name

View File

@ -136,8 +136,8 @@ for i = 1, #events do
call_output:write(' }\n')
call_output:write(' entered = true;\n')
write_arglist(call_output, ev)
call_output:write(' ui_call_event("' .. ev.name .. '", ' .. args .. ');\n')
call_output:write(' entered = false;\n')
call_output:write((' ui_call_event("%s", %s, %s)'):format(ev.name, tostring(ev.fast), args))
call_output:write(';\n entered = false;\n')
elseif ev.compositor_impl then
call_output:write(' ui_comp_' .. ev.name)
write_signature(call_output, ev, '', true)

View File

@ -187,7 +187,7 @@ static void nlua_luv_error_event(void **argv)
msg_ext_set_kind("lua_error");
switch (type) {
case kCallback:
semsg_multiline("Error executing luv callback:\n%s", error);
semsg_multiline("Error executing callback:\n%s", error);
break;
case kThread:
semsg_multiline("Error in luv thread:\n%s", error);
@ -201,13 +201,13 @@ static void nlua_luv_error_event(void **argv)
xfree(error);
}
static int nlua_luv_cfpcall(lua_State *lstate, int nargs, int nresult, int flags)
/// Execute callback in "fast" context. Used for luv and some vim.ui_event
/// callbacks where using the API directly is not safe.
static int nlua_fast_cfpcall(lua_State *lstate, int nargs, int nresult, int flags)
FUNC_ATTR_NONNULL_ALL
{
int retval;
// luv callbacks might be executed at any os_breakcheck/line_breakcheck
// call, so using the API directly here is not safe.
in_fast_callback++;
int top = lua_gettop(lstate);
@ -366,11 +366,13 @@ static int nlua_init_argv(lua_State *const L, char **argv, int argc, int lua_arg
static void nlua_schedule_event(void **argv)
{
LuaRef cb = (LuaRef)(ptrdiff_t)argv[0];
uint32_t ns_id = (uint32_t)(ptrdiff_t)argv[1];
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, cb);
nlua_unref_global(lstate, cb);
if (nlua_pcall(lstate, 0, 0)) {
nlua_error(lstate, _("Error executing vim.schedule lua callback: %.*s"));
ui_remove_cb(ns_id, true);
}
}
@ -392,8 +394,9 @@ static int nlua_schedule(lua_State *const lstate)
}
LuaRef cb = nlua_ref_global(lstate, 1);
multiqueue_put(main_loop.events, nlua_schedule_event, (void *)(ptrdiff_t)cb);
// Pass along UI event handler to disable on error.
multiqueue_put(main_loop.events, nlua_schedule_event, (void *)(ptrdiff_t)cb,
(void *)(ptrdiff_t)ui_event_ns_id);
return 0;
}
@ -425,7 +428,7 @@ static int nlua_wait(lua_State *lstate)
FUNC_ATTR_NONNULL_ALL
{
if (in_fast_callback) {
return luaL_error(lstate, e_luv_api_disabled, "vim.wait");
return luaL_error(lstate, e_fast_api_disabled, "vim.wait");
}
intptr_t timeout = luaL_checkinteger(lstate, 1);
@ -598,7 +601,7 @@ static void nlua_common_vim_init(lua_State *lstate, bool is_thread, bool is_stan
luv_set_cthread(lstate, nlua_luv_thread_cfcpcall);
} else {
luv_set_loop(lstate, &main_loop.uv);
luv_set_callback(lstate, nlua_luv_cfpcall);
luv_set_callback(lstate, nlua_fast_cfpcall);
}
luaopen_luv(lstate);
lua_pushvalue(lstate, -1);
@ -724,7 +727,7 @@ static int nlua_ui_detach(lua_State *lstate)
return luaL_error(lstate, "invalid ns_id");
}
ui_remove_cb(ns_id);
ui_remove_cb(ns_id, false);
return 0;
}
@ -1174,7 +1177,7 @@ int nlua_call(lua_State *lstate)
size_t name_len;
const char *name = luaL_checklstring(lstate, 1, &name_len);
if (!nlua_is_deferred_safe() && !viml_func_is_fast(name)) {
return luaL_error(lstate, e_luv_api_disabled, "Vimscript function");
return luaL_error(lstate, e_fast_api_disabled, "Vimscript function");
}
int nargs = lua_gettop(lstate) - 1;
@ -1231,7 +1234,7 @@ free_vim_args:
static int nlua_rpcrequest(lua_State *lstate)
{
if (!nlua_is_deferred_safe()) {
return luaL_error(lstate, e_luv_api_disabled, "rpcrequest");
return luaL_error(lstate, e_fast_api_disabled, "rpcrequest");
}
return nlua_rpc(lstate, true);
}
@ -1593,6 +1596,12 @@ bool nlua_ref_is_function(LuaRef ref)
/// @return Return value of function, as per mode
Object nlua_call_ref(LuaRef ref, const char *name, Array args, LuaRetMode mode, Arena *arena,
Error *err)
{
return nlua_call_ref_ctx(false, ref, name, args, mode, arena, err);
}
Object nlua_call_ref_ctx(bool fast, LuaRef ref, const char *name, Array args, LuaRetMode mode,
Arena *arena, Error *err)
{
lua_State *const lstate = global_lstate;
nlua_pushref(lstate, ref);
@ -1605,7 +1614,13 @@ Object nlua_call_ref(LuaRef ref, const char *name, Array args, LuaRetMode mode,
nlua_push_Object(lstate, &args.items[i], 0);
}
if (nlua_pcall(lstate, nargs, 1)) {
if (fast) {
if (nlua_fast_cfpcall(lstate, nargs, 1, -1) < 0) {
// error is already scheduled, set anyways to convey failure.
api_set_error(err, kErrorTypeException, "fast context failure");
}
return NIL;
} else if (nlua_pcall(lstate, nargs, 1)) {
// if err is passed, the caller will deal with the error.
if (err) {
size_t len;

View File

@ -44,6 +44,7 @@
typedef struct {
LuaRef cb;
uint8_t errors;
bool ext_widgets[kUIGlobalCount];
} UIEventCallback;
@ -212,21 +213,20 @@ void ui_refresh(void)
cursor_row = cursor_col = 0;
pending_cursor_update = true;
bool had_message = ui_ext[kUIMessages];
for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
ui_ext[i] = ext_widgets[i] | ui_cb_ext[i];
if (i < kUIGlobalCount) {
ext_widgets[i] |= ui_cb_ext[i];
ui_call_option_set(cstr_as_string(ui_ext_names[i]), BOOLEAN_OBJ(ui_ext[i]));
}
// Set 'cmdheight' to zero for all tabpages when ext_messages becomes active.
if (i == kUIMessages && !ui_ext[i] && ext_widgets[i]) {
set_option_value(kOptCmdheight, NUMBER_OPTVAL(0), 0);
command_height();
FOR_ALL_TABS(tp) {
tp->tp_ch_used = 0;
}
}
ui_ext[i] = ext_widgets[i];
if (i < kUIGlobalCount) {
ui_call_option_set(cstr_as_string(ui_ext_names[i]), BOOLEAN_OBJ(ext_widgets[i]));
}
// Reset 'cmdheight' for all tabpages when ext_messages toggles.
if (had_message != ui_ext[kUIMessages]) {
set_option_value(kOptCmdheight, NUMBER_OPTVAL(had_message), 0);
command_height();
FOR_ALL_TABS(tp) {
tp->tp_ch_used = had_message;
}
}
@ -713,13 +713,15 @@ void ui_grid_resize(handle_T grid_handle, int width, int height, Error *err)
}
}
void ui_call_event(char *name, Array args)
void ui_call_event(char *name, bool fast, Array args)
{
UIEventCallback *event_cb;
bool handled = false;
map_foreach_value(&ui_event_cbs, event_cb, {
UIEventCallback *event_cb;
map_foreach(&ui_event_cbs, ui_event_ns_id, event_cb, {
Error err = ERROR_INIT;
Object res = nlua_call_ref(event_cb->cb, name, args, kRetNilBool, NULL, &err);
uint32_t ns_id = ui_event_ns_id;
Object res = nlua_call_ref_ctx(fast, event_cb->cb, name, args, kRetNilBool, NULL, &err);
ui_event_ns_id = 0;
// TODO(bfredl/luukvbaal): should this be documented or reconsidered?
// Why does truthy return from Lua callback mean remote UI should not receive
// the event.
@ -728,6 +730,7 @@ void ui_call_event(char *name, Array args)
}
if (ERROR_SET(&err)) {
ELOG("Error executing UI event callback: %s", err.msg);
ui_remove_cb(ns_id, true);
}
api_clear_error(&err);
})
@ -780,12 +783,16 @@ void ui_add_cb(uint32_t ns_id, LuaRef cb, bool *ext_widgets)
ui_refresh();
}
void ui_remove_cb(uint32_t ns_id)
void ui_remove_cb(uint32_t ns_id, bool checkerr)
{
if (map_has(uint32_t, &ui_event_cbs, ns_id)) {
UIEventCallback *item = pmap_del(uint32_t)(&ui_event_cbs, ns_id, NULL);
UIEventCallback *item = pmap_get(uint32_t)(&ui_event_cbs, ns_id);
if (item && (!checkerr || ++item->errors > 10)) {
pmap_del(uint32_t)(&ui_event_cbs, ns_id, NULL);
free_ui_event_callback(item);
ui_cb_update_ext();
ui_refresh();
if (checkerr) {
msg_schedule_semsg("Excessive errors in vim.ui_attach() callback from ns: %d.", ns_id);
}
}
ui_cb_update_ext();
ui_refresh();
}

View File

@ -18,4 +18,6 @@ EXTERN Array noargs INIT(= ARRAY_DICT_INIT);
#endif
// uncrustify:on
EXTERN MultiQueue *resize_events;
// vim.ui_attach() namespace of currently executed callback.
EXTERN uint32_t ui_event_ns_id INIT( = 0);
EXTERN MultiQueue *resize_events INIT( = NULL);

View File

@ -757,7 +757,7 @@ describe('API/extmarks', function()
{
Ïf (!nlua_is_deferred_safe(lstate)) {
// strictly not allowed
Яetörn luaL_error(lstate, e_luv_api_disabled, "rpcrequest");
Яetörn luaL_error(lstate, e_fast_api_disabled, "rpcrequest");
}
return nlua_rpc(lstate, true);
}]])

View File

@ -87,9 +87,9 @@ describe('vim.uv', function()
screen:expect([[
|
{2: }|
{3:Error executing luv callback:} |
{3:Error executing callback:} |
{3:[string "<nvim>"]:5: E5560: nvim_set_var must not }|
{3:be called in a lua loop callback} |
{3:be called in a fast event context} |
{3:stack traceback:} |
{3: [C]: in function 'nvim_set_var'} |
{3: [string "<nvim>"]:5: in function <[string }|

View File

@ -32,15 +32,6 @@ describe('vim.ui_attach', function()
]]
screen = Screen.new(40, 5)
screen:set_default_attr_ids({
[1] = { bold = true, foreground = Screen.colors.Blue1 },
[2] = { bold = true },
[3] = { background = Screen.colors.Grey },
[4] = { background = Screen.colors.LightMagenta },
[5] = { reverse = true },
[6] = { reverse = true, bold = true },
[7] = { background = Screen.colors.Yellow1 },
})
end)
local function expect_events(expected)
@ -55,7 +46,7 @@ describe('vim.ui_attach', function()
grid = [[
fo^ |
{1:~ }|*3
{2:-- INSERT --} |
{5:-- INSERT --} |
]],
}
@ -64,7 +55,7 @@ describe('vim.ui_attach', function()
grid = [[
food^ |
{1:~ }|*3
{2:-- INSERT --} |
{5:-- INSERT --} |
]],
}
expect_events {
@ -83,7 +74,7 @@ describe('vim.ui_attach', function()
grid = [[
foobar^ |
{1:~ }|*3
{2:-- INSERT --} |
{5:-- INSERT --} |
]],
}
expect_events {
@ -105,10 +96,10 @@ describe('vim.ui_attach', function()
screen:expect {
grid = [[
food^ |
{3:food }{1: }|
{12:food }{1: }|
{4:foobar }{1: }|
{4:foo }{1: }|
{2:-- INSERT --} |
{5:-- INSERT --} |
]],
}
expect_events {}
@ -180,12 +171,17 @@ describe('vim.ui_attach', function()
exec_lua([[
_G.cmdline = 0
vim.ui_attach(ns, { ext_messages = true }, function(ev)
vim.cmd.redraw()
if ev == 'msg_show' then
vim.schedule(function() vim.cmd.redraw() end)
else
vim.cmd.redraw()
end
_G.cmdline = _G.cmdline + (ev == 'cmdline_show' and 1 or 0)
end
)]])
feed(':')
eq(1, exec_lua('return _G.cmdline'))
n.assert_alive()
eq(2, exec_lua('return _G.cmdline'))
n.assert_alive()
feed('version<CR><CR>v<Esc>')
n.assert_alive()
@ -211,9 +207,9 @@ describe('vim.ui_attach', function()
screen:expect({
grid = [[
cmdline |
{5:cmdline [+] }|
{2:cmdline [+] }|
fooba^r |
{6:[No Name] [+] }|
{3:[No Name] [+] }|
|
]],
})
@ -222,9 +218,9 @@ describe('vim.ui_attach', function()
screen:expect({
grid = [[
foo |
{5:cmdline [+] }|
{5:foo}ba^r |
{6:[No Name] [+] }|
{2:cmdline [+] }|
{2:foo}ba^r |
{3:[No Name] [+] }|
|
]],
})
@ -233,13 +229,124 @@ describe('vim.ui_attach', function()
screen:expect({
grid = [[
%s/bar/baz |
{5:cmdline [+] }|
foo{7:ba^z} |
{6:[No Name] [+] }|
{2:cmdline [+] }|
foo{10:ba^z} |
{3:[No Name] [+] }|
|
]],
})
end)
it('aborts :function on error with ext_messages', function()
exec_lua([[
vim.ui_attach(ns, { ext_messages = true }, function(event, _, content)
if event == "msg_show" then
-- "fast-api" does not prevent aborting :function
vim.api.nvim_get_runtime_file("foo", false)
-- non-"fast-api" is not allowed in msg_show callback and should be scheduled
local _, err = pcall(vim.api.nvim_buf_set_lines, 0, -2, -1, false, { content[1][2] })
vim.schedule(function()
vim.api.nvim_buf_set_lines(0, -2, -1, false, { content[1][2], err })
end)
end
end)
]])
feed(':func Foo()<cr>bar<cr>endf<cr>:func Foo()<cr>')
screen:expect({
grid = [[
^E122: Function Foo already exists, add !|
to replace it |
E5560: nvim_buf_set_lines must not be ca|
lled in a fast event context |
{1:~ }|
]],
messages = {
{
content = { { 'E122: Function Foo already exists, add ! to replace it', 9, 7 } },
kind = 'emsg',
},
},
})
end)
it('detaches after excessive errors', function()
screen:add_extra_attr_ids({ [100] = { bold = true, foreground = Screen.colors.SeaGreen } })
exec_lua([[
vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function()
vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] })
end)
]])
screen:expect({
grid = [[
^ |
{1:~ }|*4
]],
})
feed('ifoo')
screen:expect({
grid = [[
foo^ |
{1:~ }|*4
]],
showmode = { { '-- INSERT --', 5, 12 } },
})
feed('<esc>:1mes clear<cr>:mes<cr>')
screen:expect({
grid = [[
foo |
{3: }|
{9:Excessive errors in vim.ui_attach() call}|
{9:back from ns: 2.} |
{100:Press ENTER or type command to continue}^ |
]],
})
feed('<cr>')
-- Also when scheduled
exec_lua([[
vim.ui_attach(vim.api.nvim_create_namespace(''), { ext_messages = true }, function()
vim.schedule(function() vim.api.nvim_buf_set_lines(0, -2, -1, false, { err[1] }) end)
end)
]])
screen:expect({
any = 'fo^o',
messages = {
{
content = {
{
'Error executing vim.schedule lua callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
9,
7,
},
},
kind = 'lua_error',
},
{
content = {
{
'Error executing vim.schedule lua callback: [string "<nvim>"]:2: attempt to index global \'err\' (a nil value)\nstack traceback:\n\t[string "<nvim>"]:2: in function <[string "<nvim>"]:2>',
9,
7,
},
},
kind = 'lua_error',
},
{
content = { { 'Press ENTER or type command to continue', 100, 19 } },
kind = 'return_prompt',
},
},
})
feed('<esc>:1mes clear<cr>:mes<cr>')
screen:expect({
grid = [[
foo |
{3: }|
{9:Excessive errors in vim.ui_attach() call}|
{9:back from ns: 3.} |
{100:Press ENTER or type command to continue}^ |
]],
})
end)
end)
describe('vim.ui_attach', function()

View File

@ -1414,7 +1414,7 @@ describe('lua stdlib', function()
screen:expect {
grid = [[
{9:[string "<nvim>"]:6: E5560: rpcrequest must not be}|
{9: called in a lua loop callback} |
{9: called in a fast event context} |
{9:stack traceback:} |
{9: [C]: in function 'rpcrequest'} |
{9: [string "<nvim>"]:6: in function <[string }|
@ -3783,7 +3783,7 @@ stack traceback:
end)
]])
screen:expect({
any = pesc('E5560: vim.wait must not be called in a lua loop callback'),
any = pesc('E5560: vim.wait must not be called in a fast event context'),
})
feed('<CR>')
assert_alive()

View File

@ -25,8 +25,6 @@ local command = n.command
local write_file = t.write_file
local api = n.api
local sleep = vim.uv.sleep
local matches = t.matches
local pcall_err = t.pcall_err
local assert_alive = n.assert_alive
local poke_eventloop = n.poke_eventloop
local feed = n.feed
@ -227,72 +225,6 @@ describe('listing functions using :function', function()
exec_capture(('function <lambda>%s'):format(num))
)
end)
it('does not crash if another function is deleted while listing', function()
local _ = Screen.new(80, 24)
matches(
'Vim%(function%):E454: Function list was modified$',
pcall_err(
exec_lua,
[=[
vim.cmd([[
func Func1()
endfunc
func Func2()
endfunc
func Func3()
endfunc
]])
local ns = vim.api.nvim_create_namespace('test')
vim.ui_attach(ns, { ext_messages = true }, function(event, _, content)
if event == 'msg_show' and content[1][2] == 'function Func1()' then
vim.cmd('delfunc Func3')
end
end)
vim.cmd('function')
vim.ui_detach(ns)
]=]
)
)
assert_alive()
end)
it('does not crash if the same function is deleted while listing', function()
local _ = Screen.new(80, 24)
matches(
'Vim%(function%):E454: Function list was modified$',
pcall_err(
exec_lua,
[=[
vim.cmd([[
func Func1()
endfunc
func Func2()
endfunc
func Func3()
endfunc
]])
local ns = vim.api.nvim_create_namespace('test')
vim.ui_attach(ns, { ext_messages = true }, function(event, _, content)
if event == 'msg_show' and content[1][2] == 'function Func1()' then
vim.cmd('delfunc Func2')
end
end)
vim.cmd('function')
vim.ui_detach(ns)
]=]
)
)
assert_alive()
end)
end)
it('no double-free in garbage collection #16287', function()