Merge pull request #39920 from bfredl/luastring

fix(lua): use even safer and even better error() message conversion
This commit is contained in:
bfredl
2026-05-25 16:35:40 +02:00
committed by GitHub
7 changed files with 41 additions and 23 deletions

View File

@@ -277,16 +277,18 @@ lua_State *get_global_lstate(void)
/// The returned string points to memory on the Lua stack. Use or duplicate it before using
/// `lstate` again.
///
/// @param[out] len length of error (can be NULL)
/// @param[out] len length of error
static const char *nlua_get_error(lua_State *lstate, size_t *len)
FUNC_ATTR_NONNULL_RET
{
if (luaL_getmetafield(lstate, -1, "__tostring")) {
if (lua_isfunction(lstate, -1) && luaL_callmeta(lstate, -2, "__tostring")) {
// call __tostring, convert the result and replace error with it
lua_replace(lstate, -3);
if (lua_type(lstate, -1) != LUA_TSTRING) {
lua_getglobal(lstate, "tostring");
lua_pushvalue(lstate, -2);
if (lua_pcall(lstate, 1, 1, 0) || lua_type(lstate, -1) != LUA_TSTRING) {
lua_pop(lstate, 1);
lua_pushstring(lstate, "[UNPRINTABLE ERROR]");
}
// pop __tostring.
lua_pop(lstate, 1);
lua_replace(lstate, -2);
}
return lua_tolstring(lstate, -1, len);
@@ -433,10 +435,11 @@ static int nlua_luv_thread_common_cfpcall(lua_State *lstate, int nargs, int nres
pthread_exit(0);
#endif
}
const char *error = lua_tostring(lstate, -1);
size_t len;
const char *error = nlua_get_error(lstate, &len);
loop_schedule_deferred(&main_loop,
event_create(nlua_luv_error_event,
error != NULL ? xstrdup(error) : NULL,
xmemdupz(error, len),
(void *)(intptr_t)(is_callback
? kThreadCallback
: kThread)));

View File

@@ -828,7 +828,7 @@ describe('nvim_create_user_command', function()
})
]])
feed(':Test <Tab>')
eq('E5108: Lua function: [NULL]', api.nvim_get_vvar('errmsg'))
eq('E5108: Lua function: nil', api.nvim_get_vvar('errmsg'))
eq('Test ', fn.getcmdline())
assert_alive()
end)

View File

@@ -72,8 +72,22 @@ describe(':lua', function()
eq({ '' }, api.nvim_buf_get_lines(0, 0, 100, false))
end)
it('works with NULL errors', function()
eq([=[Vim(lua):E5108: Lua: [NULL]]=], pcall_err(command, 'lua error(nil)'))
it('works with nil errors', function()
eq([=[Vim(lua):E5108: Lua: nil]=], pcall_err(command, 'lua error(nil)'))
end)
it('works with errors with __tostring failure', function()
eq(
[=[Vim(lua):E5108: Lua: [UNPRINTABLE ERROR]]=],
pcall_err(command, 'lua error(setmetatable({}, {__tostring=error}))')
)
end)
it('works with printable errors', function()
eq([=[Vim(lua):E5108: Lua: false]=], pcall_err(command, 'lua error(false)'))
-- numbers get the location prefixed because numbers are almost strings (know the workplace rules)
eq([=[Vim(lua):E5108: Lua: [string ":lua"]:0: 39]=], pcall_err(command, 'lua error(39)'))
matches([=[Vim%(lua%):E5108: Lua: table: 0x%x+]=], pcall_err(command, 'lua error({})'))
end)
it('accepts embedded NLs without heredoc', function()
@@ -280,8 +294,8 @@ describe(':luado command', function()
)
end)
it('works with NULL errors', function()
eq([=[Vim(luado):E5111: Lua: [NULL]]=], pcall_err(command, 'luado error(nil)'))
it('works with nil errors', function()
eq([=[Vim(luado):E5111: Lua: nil]=], pcall_err(command, 'luado error(nil)'))
end)
it('fails in sandbox when needed', function()
@@ -342,8 +356,8 @@ describe(':luafile', function()
)
end)
it('works with NULL errors', function()
it('works with nil errors', function()
write_file(fname, 'error(nil)')
eq([=[Vim(luafile):E5113: Lua chunk: [NULL]]=], pcall_err(command, 'luafile ' .. fname))
eq([=[Vim(luafile):E5113: Lua chunk: nil]=], pcall_err(command, 'luafile ' .. fname))
end)
end)

View File

@@ -495,7 +495,7 @@ describe('luaeval()', function()
remove_trace(pcall_err(command, [[call luaeval("error('ERROR')")]]))
)
eq(
'Vim(call):E5108: Lua: [NULL]',
'Vim(call):E5108: Lua: nil',
remove_trace(pcall_err(command, [[call luaeval("error(nil)")]]))
)
end)

View File

@@ -4,6 +4,7 @@ local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local eq = t.eq
local matches = t.matches
local NIL = vim.NIL
local feed = n.feed
local clear = n.clear
@@ -105,13 +106,13 @@ describe('print', function()
'Vim(lua):E5108: Lua: Xtest-functional-lua-overrides-luafile:2: 1234',
pcall_err(command, 'lua number_error()')
)
eq('Vim(lua):E5108: Lua: [NULL]', pcall_err(command, 'lua nil_error()'))
eq('Vim(lua):E5108: Lua: [NULL]', pcall_err(command, 'lua table_error()'))
eq('Vim(lua):E5108: Lua: nil', pcall_err(command, 'lua nil_error()'))
matches('^Vim%(lua%):E5108: Lua: table: 0x%x+$', pcall_err(command, 'lua table_error()'))
eq(
'Vim(lua):E5108: Lua: Internal Error [11234] my mistake',
pcall_err(command, 'lua custom_error()')
)
eq('Vim(lua):E5108: Lua: [NULL]', pcall_err(command, 'lua bad_custom_error()'))
eq('Vim(lua):E5108: Lua: [UNPRINTABLE ERROR]', pcall_err(command, 'lua bad_custom_error()'))
end)
it('prints strings with NULs and NLs correctly', function()
api.nvim_set_option_value('more', true, {})

View File

@@ -32,7 +32,7 @@ describe('thread', function()
{1:~ }|*5
{3: }|
{9:Luv thread:} |
{9:[NULL]} |
{9:nil} |
{6:Press ENTER or type command to continue}^ |
]])
feed('<cr>')

View File

@@ -174,7 +174,7 @@ describe('vim.uv', function()
{1:~ }|*5
{3: }|
{9:Lua callback:} |
{9:[NULL]} |
{9:nil} |
{6:Press ENTER or type command to continue}^ |
]]
screen:expect(s)
@@ -209,7 +209,7 @@ describe('vim.uv', function()
screen:expect([[
{3: }|
{9:Lua callback:} |
{9:[NULL]} |
{9:nil} |
{6:Press ENTER or type command to continue}^ |
]])
feed('<cr>')