mirror of
https://github.com/neovim/neovim.git
synced 2026-10-25 07:07:55 -05:00
fix(mappings): replaying a deleted Lua mapping is UB
Problem:
Replaying a deleted Lua mapping, may call an arbitrary function.
RHS of a Lua mapping embeds its LuaRef (`<K_LUA><ref><CR>`). The raw
keys may outlive the ref (redobuff ".", CmdAtom `keys`). If the mapping
is deleted, replaying it either (1) dereferences a freed registry slot,
or (2) calls whatever callback reused the slot (autocmd, timer, other
mapping).
Solution:
Assign a monotonic (never recycled) id to Lua mappings and encode the
mapping keys as `<K_LUA><id><CR>`.
Note: in the case of Vimscript, a deleted function raises E117, but if
the function is redefined with the same name, the mapping will find it.
Alternatives?:
- Globally ensure `LuaRef` ids are not recycled.
- Problem: could exhaust `int` in a long-lived Nvim session? Also,
difficult to impl bc the "recycling" is done by `luaL_ref` itself.
ref: 5ac2e47acc
This commit is contained in:
1 parent
bc16be3cd9
commit
25f7c87a70
9 files changed
+93
-21
No files matched your search
@@ -833,6 +833,12 @@ compilation error message.
|
||||
Failed to execute a Lua debug command string. {error} contains the runtime
|
||||
error message and traceback.
|
||||
|
||||
*E5117* >
|
||||
Lua mapping no longer exists
|
||||
|
||||
Replayed keys (|.|, |CmdAtom| `keys`, or keys captured by |vim.on_key()|)
|
||||
referenced a Lua-callback mapping that was deleted or redefined since.
|
||||
|
||||
==============================================================================
|
||||
Messages *messages*
|
||||
|
||||
|
||||
@@ -463,13 +463,13 @@ struct file_buffer {
|
||||
// bitset with 4*64=256 bits: 1 bit per character 0-255.
|
||||
uint64_t b_chartab[4];
|
||||
|
||||
// Table used for mappings local to a buffer.
|
||||
// Buffer-local mappings.
|
||||
mapblock_T *(b_maphash[MAX_MAPHASH]);
|
||||
|
||||
// First abbreviation local to a buffer.
|
||||
// Buffer-local abbreviations.
|
||||
mapblock_T *b_first_abbr;
|
||||
// User commands local to the buffer.
|
||||
// Buffer-local user commands.
|
||||
garray_T b_ucmds;
|
||||
|
||||
// start and end of an operator, also used for '[ and ']
|
||||
pos_T b_op_start;
|
||||
pos_T b_op_start_orig; // used for Ins.start_orig
|
||||
|
||||
+13
-6
@@ -196,6 +196,7 @@ static const char e_cmd_mapping_must_end_with_cr[]
|
||||
= N_("E1255: <Cmd> mapping must end with <CR>");
|
||||
static const char e_cmd_mapping_must_end_with_cr_before_second_cmd[]
|
||||
= N_("E1136: <Cmd> mapping must end with <CR> before second <Cmd>");
|
||||
static const char e_lua_mapping_gone[] = N_("E5117: Lua mapping no longer exists");
|
||||
|
||||
/// Frees the buffer's memory; it becomes empty.
|
||||
static void free_buff(StuffBuf *buf)
|
||||
@@ -3418,12 +3419,12 @@ char *getcmdkeycmd(int promptc, void *cookie, int indent, bool do_concat)
|
||||
return line_ga.ga_data;
|
||||
}
|
||||
|
||||
/// Handle a Lua mapping: get its LuaRef from typeahead and execute it.
|
||||
/// Handle a Lua mapping: get its id from typeahead and execute its callback.
|
||||
///
|
||||
/// @param may_repeat save the LuaRef for redoing with "." later
|
||||
/// @param discard discard the keys instead of executing the LuaRef
|
||||
/// @param may_repeat Save the mapping-id for redoing with "." later
|
||||
/// @param discard Discard the keys instead of executing the callback.
|
||||
///
|
||||
/// @return false if getting the LuaRef was aborted, true otherwise
|
||||
/// @return false if getting the id was aborted or mapping no longer exists, true otherwise.
|
||||
bool map_execute_lua(bool may_repeat, bool discard)
|
||||
{
|
||||
garray_T line_ga;
|
||||
@@ -3455,9 +3456,15 @@ bool map_execute_lua(bool may_repeat, bool discard)
|
||||
return !aborted;
|
||||
}
|
||||
|
||||
LuaRef ref = (LuaRef)atoi(line_ga.ga_data);
|
||||
const int id = atoi(line_ga.ga_data);
|
||||
const LuaRef ref = map_luaid_get(id);
|
||||
if (ref == LUA_NOREF) {
|
||||
ga_clear(&line_ga);
|
||||
emsg(_(e_lua_mapping_gone));
|
||||
return false;
|
||||
}
|
||||
if (may_repeat) {
|
||||
repeat_luaref = ref;
|
||||
repeat_luamap = id;
|
||||
}
|
||||
|
||||
Error err = ERROR_INIT;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "nvim/keycodes.h"
|
||||
#include "nvim/log.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mapping.h"
|
||||
#include "nvim/mbyte.h"
|
||||
#include "nvim/memory.h"
|
||||
#include "nvim/normal.h"
|
||||
@@ -1120,7 +1121,7 @@ void atom_capture_op(oparg_T *oap, cmdarg_T *cap, bool redo_yank)
|
||||
}
|
||||
} else if (cap->cmdchar == K_LUA) {
|
||||
char buf[NUMBUFLEN];
|
||||
redo_append_str(buf, snprintf(buf, sizeof(buf), "%d", repeat_luaref));
|
||||
redo_append_str(buf, snprintf(buf, sizeof(buf), "%d", repeat_luamap));
|
||||
redo_append_str(S_LEN(NL_STR));
|
||||
}
|
||||
} else if (Visual.active && redoable && oap->motion_force == NUL) {
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
#include "nvim/lua/treesitter.h"
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/main.h"
|
||||
#include "nvim/mapping.h"
|
||||
#include "nvim/mark.h"
|
||||
#include "nvim/memline.h"
|
||||
#include "nvim/memory.h"
|
||||
|
||||
+35
-6
@@ -60,11 +60,11 @@
|
||||
#include "nvim/ui_defs.h"
|
||||
#include "nvim/vim_defs.h"
|
||||
|
||||
/// List used for abbreviations.
|
||||
static mapblock_T *first_abbr = NULL; // first entry in abbrlist
|
||||
/// Global abbreviations (first entry in the linkedlist).
|
||||
static mapblock_T *first_abbr = NULL;
|
||||
|
||||
// Each mapping is put in one of the MAX_MAPHASH hash lists,
|
||||
// to speed up finding it.
|
||||
/// Global mappings.
|
||||
/// Each mapping is put in one of the MAX_MAPHASH hash lists, to speed up finding it.
|
||||
static mapblock_T *(maphash[MAX_MAPHASH]) = { 0 };
|
||||
|
||||
// Make a hash value for a mapping.
|
||||
@@ -146,6 +146,34 @@ mapblock_T *get_buf_maphash_list(int state, int c)
|
||||
return curbuf->b_maphash[MAP_HASH(state, c)];
|
||||
}
|
||||
|
||||
/// Finds the Lua mapping whose `m_str` embeds `id`.
|
||||
static mapblock_T *map_luaid_scan(mapblock_T *const *mappings, size_t size, int id)
|
||||
{
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
for (mapblock_T *mp = mappings[i]; mp != NULL; mp = mp->m_next) {
|
||||
// Find `id` in "<K_LUA><id><CR>".
|
||||
if (mp->m_luaref != LUA_NOREF && atoi(mp->m_str + 3) == id) {
|
||||
return mp;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/// Resolves a Lua-mapping id to its callback.
|
||||
///
|
||||
/// @return Lua callback, or LUA_NOREF if the mapping no longer exists.
|
||||
LuaRef map_luaid_get(int n)
|
||||
{
|
||||
mapblock_T *mp = map_luaid_scan(maphash, MAX_MAPHASH, n);
|
||||
mp = mp != NULL ? mp : map_luaid_scan(&first_abbr, 1, n);
|
||||
FOR_ALL_BUFFERS(fb) {
|
||||
mp = mp != NULL ? mp : map_luaid_scan(fb->b_maphash, MAX_MAPHASH, n);
|
||||
mp = mp != NULL ? mp : map_luaid_scan(&fb->b_first_abbr, 1, n);
|
||||
}
|
||||
return mp != NULL ? mp->m_luaref : LUA_NOREF;
|
||||
}
|
||||
|
||||
/// Delete one entry from the abbrlist or maphash[].
|
||||
/// "mpp" is a pointer to the m_next field of the PREVIOUS entry!
|
||||
static void mapblock_free(mapblock_T **mpp)
|
||||
@@ -371,9 +399,10 @@ static void set_maparg_rhs(const char *const orig_rhs, const size_t orig_rhs_len
|
||||
// orig_rhs is not used for Lua mappings, but still needs to be a string.
|
||||
mapargs->orig_rhs = xcalloc(1, sizeof(char));
|
||||
mapargs->orig_rhs_len = 0;
|
||||
// stores <lua>ref_no<cr> in map_str
|
||||
// Stores "<K_LUA><lua-mapping-id><CR>" in map_str.
|
||||
static int map_luaid = 0;
|
||||
mapargs->rhs_len = (size_t)vim_snprintf(S_LEN(tmp_buf), "%c%c%c%d\r", K_SPECIAL,
|
||||
KS_EXTRA, KE_LUA, rhs_lua);
|
||||
KS_EXTRA, KE_LUA, ++map_luaid);
|
||||
mapargs->rhs = xstrdup(tmp_buf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "nvim/cmdexpand_defs.h" // IWYU pragma: keep
|
||||
#include "nvim/eval/typval_defs.h" // IWYU pragma: keep
|
||||
#include "nvim/ex_cmds_defs.h" // IWYU pragma: keep
|
||||
#include "nvim/macros_defs.h"
|
||||
#include "nvim/mapping_defs.h" // IWYU pragma: keep
|
||||
#include "nvim/option_defs.h" // IWYU pragma: keep
|
||||
#include "nvim/regexp_defs.h" // IWYU pragma: keep
|
||||
@@ -15,6 +16,8 @@
|
||||
|
||||
#include "mapping.h.generated.h"
|
||||
|
||||
EXTERN int repeat_luamap INIT( = 0); ///< Lua-mapping id for dot-repeat. 0: none.
|
||||
|
||||
/// Used for the first argument of do_map()
|
||||
enum {
|
||||
MAPTYPE_MAP = 0,
|
||||
|
||||
@@ -52,5 +52,3 @@ enum {
|
||||
|
||||
#include "ops.h.generated.h"
|
||||
#include "ops.h.inline.generated.h"
|
||||
|
||||
EXTERN LuaRef repeat_luaref INIT( = LUA_NOREF); ///< LuaRef for "."
|
||||
@@ -1140,10 +1140,37 @@ describe('CmdAtom', function()
|
||||
}, atoms_tail(2, 'type', 'keys', 'lhs'))
|
||||
end)
|
||||
|
||||
it('replay of deleted/redefined Lua mapping fails (E5117)', function()
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('o', 'gt', function()
|
||||
vim.cmd('normal! viw')
|
||||
end)
|
||||
]])
|
||||
fn.setline(1, { 'k1 k2', 'k3 k4' })
|
||||
atoms_start()
|
||||
feed('gg0dgt')
|
||||
eq(' k2', fn.getline(1))
|
||||
local ev = atom_last()
|
||||
n.exec_lua([[
|
||||
vim.keymap.del('o', 'gt')
|
||||
vim.keymap.set('o', 'gt', function()
|
||||
vim.cmd('normal! viw')
|
||||
end)
|
||||
]])
|
||||
feed('j0')
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(ev.keys))
|
||||
eq('k3 k4', fn.getline(2))
|
||||
t.matches('E5117', fn.execute('messages'))
|
||||
-- Running the (re-defined) mapping emits CmdAtom.keys with the updated id.
|
||||
feed('dgt')
|
||||
eq(' k4', fn.getline(2))
|
||||
n.exec_lua(([[vim.api.nvim_feedkeys(%q, 'nx', false)]]):format(atom_last().keys))
|
||||
eq('k4', fn.getline(2))
|
||||
end)
|
||||
|
||||
it('operator completed by a Lua textobject (:omap) #41482', function()
|
||||
-- Lua :omap textobject (starts Visual mode, |omap-info|) captures the operator: `keys` is the
|
||||
-- redobuff (K_LUA + luaref), replayable like "." repeating it. `cmd` is omitted (a Lua
|
||||
-- callback has no notation).
|
||||
-- redobuff (K_LUA + mapping-id).
|
||||
n.exec_lua([[
|
||||
vim.keymap.set('o', 'gt', function()
|
||||
vim.cmd('normal! viw')
|
||||
|
||||
Reference in new issue
Block a user