From: Szymon Wilczek <[email protected]>
Date: 29 Apr 2026
Subject: fix(treesitter): zero parser userdata pointer after ts_parser_delete
Pull request: #39497
Merged: 7 May 2026, by Justin M. Keyes
signed, reported, fixes
- Author Szymon Wilczek author
- Merged-by Justin M. Keyes maintainer · merged it
diff
@@ -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; } @@ -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() { discussion · on the pull request
v1 · 14 messages
szymonwilczek author 29 Apr 2026
fix(treesitter): zero parser userdata pointer after ts_parser_delete
szymonwilczek author 29 Apr 2026
barrettruth reviewer 5 May 2026
szymonwilczek author 5 May 2026
int to align with the luaL_checkudata signature which takes an int. Small cleanup while I was touching the function.
Also, thanks for the advice, but I'm keeping is as draft on purpose; until @justinmk has a chance to look - we discussed the approach in #39411 and I want his sign-off before requesting the final reviewjustinmk maintainer 5 May 2026
lewis6991 reviewer 7 May 2026
diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua
index 8678761e6e..9704507411 100644
--- a/test/functional/treesitter/parser_spec.lua
+++ b/test/functional/treesitter/parser_spec.lua
@@ -107,6 +107,27 @@ describe('treesitter parser API', function()
)
end)
+ it('ignores repeated parser finalization', function()
+ eq(
+ { false, true },
+ 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()
+
+ local ok, err = pcall(function()
+ parser:parse(nil, 'int x;', false)
+ end)
+ return { ok, tostring(err):find('Parser has been deleted', 1, true) ~= nil }
+ end)
+ )
+ assert_alive()
+ end)
+
it('respects eol settings when parsing buffer', function()
insert([[
int main() {szymonwilczek author 7 May 2026
justinmk maintainer 7 May 2026
Parser not found ?
"has been deleted" assumes that it existed, is that a valid assumption?justinmk maintainer 7 May 2026
parser:parse(nil, 'int x;', false)
and then use t.pcall_err(exec_lua, function() ...) in the test call, so we can assert the message more clearlyszymonwilczek author 7 May 2026
justinmk maintainer 7 May 2026
szymonwilczek author 7 May 2026
lewis6991 reviewer 7 May 2026
uint*_t types where we can use them.neovim-backports[bot] bot 7 May 2026
release-0.12:
- #39651