From 0c3e6e1b0e6eecceff441691e98ffa6aeb894bb2 Mon Sep 17 00:00:00 2001 From: Szymon Wilczek Date: Thu, 7 May 2026 14:39:07 +0200 Subject: [PATCH] fix(treesitter): crash in ts_parser_delete after gc #39497 Problem: parser_gc() calls ts_parser_delete() but leaves the userdata pointer pointing to freed memory. If the GC finalizer runs at an unexpected time (e.g. inside nvim_buf_get_lines #39411), a stale pointer could cause a crash. Solution: - NULL out `*ud` after ts_parser_delete() in parser_gc() - Update parser_check() to handle NULL with a clear error message, guarding all parser methods against UAF Co-authored-by: Lewis Russell Signed-off-by: Szymon Wilczek --- src/nvim/lua/treesitter.c | 13 ++++++++----- test/functional/treesitter/parser_spec.lua | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index e1f4aca74d..03f472dd51 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -399,10 +399,10 @@ static int tslua_push_parser(lua_State *L) return 1; } -static TSParser *parser_check(lua_State *L, uint16_t index) +static TSParser *parser_check(lua_State *L, int index) { TSParser **ud = luaL_checkudata(L, index, TS_META_PARSER); - luaL_argcheck(L, *ud, index, "TSParser expected"); + luaL_argcheck(L, *ud != NULL, index, "Parser has been deleted"); return *ud; } @@ -419,9 +419,12 @@ static void logger_gc(TSLogger logger) static int parser_gc(lua_State *L) { - TSParser *p = parser_check(L, 1); - logger_gc(ts_parser_logger(p)); - ts_parser_delete(p); + TSParser **ud = luaL_checkudata(L, 1, TS_META_PARSER); + if (*ud) { + logger_gc(ts_parser_logger(*ud)); + ts_parser_delete(*ud); + *ud = NULL; + } return 0; } diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index e8f4fe5e57..56182590c6 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -8,6 +8,8 @@ local eq = t.eq local insert = n.insert local exec_lua = n.exec_lua local feed = n.feed +local matches = t.matches +local pcall_err = t.pcall_err local run_query = ts_t.run_query local assert_alive = n.assert_alive @@ -107,6 +109,24 @@ describe('treesitter parser API', function() ) end) + it('ignores repeated parser finalization', function() + matches( + 'Parser has been deleted', + pcall_err(exec_lua, function() + vim.treesitter.language.add('c') + + local parser = vim._create_ts_parser('c') + -- The parser metatable exposes __gc through __index, so this simulates a finalizer + -- running before all Lua references to the userdata are gone. + parser:__gc() + parser:__gc() + + parser:parse(nil, 'int x;', false) + end) + ) + assert_alive() + end) + it('respects eol settings when parsing buffer', function() insert([[ int main() {