fix(treesitter): fix TSNode:tree() double free (#24796)

Problem: `push_tree`, every time its called for the same TSTree with
`do_copy=false` argument, creates a new userdata for it. Each userdata,
when garbage collected, frees the same TSTree C object.

Solution: Add flag to userdata, which indicates, should C object,
which userdata points to, be freed, when userdata is garbage collected.
This commit is contained in:
nwounkn 2023-08-29 13:48:23 +05:00 committed by Christian Clason
parent e3389c1533
commit a3c963adfc
2 changed files with 43 additions and 30 deletions

View File

@ -43,6 +43,11 @@ typedef struct {
int max_match_id; int max_match_id;
} TSLua_cursor; } TSLua_cursor;
typedef struct {
TSTree *tree;
bool gc;
} TSLuaTree;
#ifdef INCLUDE_GENERATED_DECLARATIONS #ifdef INCLUDE_GENERATED_DECLARATIONS
# include "lua/treesitter.c.generated.h" # include "lua/treesitter.c.generated.h"
#endif #endif
@ -402,8 +407,8 @@ static int parser_parse(lua_State *L)
TSTree *old_tree = NULL; TSTree *old_tree = NULL;
if (!lua_isnil(L, 2)) { if (!lua_isnil(L, 2)) {
TSTree **tmp = tree_check(L, 2); TSLuaTree *ud = tree_check(L, 2);
old_tree = tmp ? *tmp : NULL; old_tree = ud ? ud->tree : NULL;
} }
TSTree *new_tree = NULL; TSTree *new_tree = NULL;
@ -456,7 +461,7 @@ static int parser_parse(lua_State *L)
uint32_t n_ranges = 0; uint32_t n_ranges = 0;
TSRange *changed = old_tree ? ts_tree_get_changed_ranges(old_tree, new_tree, &n_ranges) : NULL; TSRange *changed = old_tree ? ts_tree_get_changed_ranges(old_tree, new_tree, &n_ranges) : NULL;
push_tree(L, new_tree, false); // [tree] push_tree(L, new_tree, true); // [tree]
push_ranges(L, changed, n_ranges, include_bytes); // [tree, ranges] push_ranges(L, changed, n_ranges, include_bytes); // [tree, ranges]
@ -476,12 +481,13 @@ static int parser_reset(lua_State *L)
static int tree_copy(lua_State *L) static int tree_copy(lua_State *L)
{ {
TSTree **tree = tree_check(L, 1); TSLuaTree *ud = tree_check(L, 1);
if (!(*tree)) { if (!ud) {
return 0; return 0;
} }
push_tree(L, *tree, true); // [tree] TSTree *copy = ts_tree_copy(ud->tree);
push_tree(L, copy, true); // [tree]
return 1; return 1;
} }
@ -493,8 +499,8 @@ static int tree_edit(lua_State *L)
return lua_error(L); return lua_error(L);
} }
TSTree **tree = tree_check(L, 1); TSLuaTree *ud = tree_check(L, 1);
if (!(*tree)) { if (!ud) {
return 0; return 0;
} }
@ -508,22 +514,22 @@ static int tree_edit(lua_State *L)
TSInputEdit edit = { start_byte, old_end_byte, new_end_byte, TSInputEdit edit = { start_byte, old_end_byte, new_end_byte,
start_point, old_end_point, new_end_point }; start_point, old_end_point, new_end_point };
ts_tree_edit(*tree, &edit); ts_tree_edit(ud->tree, &edit);
return 0; return 0;
} }
static int tree_get_ranges(lua_State *L) static int tree_get_ranges(lua_State *L)
{ {
TSTree **tree = tree_check(L, 1); TSLuaTree *ud = tree_check(L, 1);
if (!(*tree)) { if (!ud) {
return 0; return 0;
} }
bool include_bytes = (lua_gettop(L) >= 2) && lua_toboolean(L, 2); bool include_bytes = (lua_gettop(L) >= 2) && lua_toboolean(L, 2);
uint32_t len; uint32_t len;
TSRange *ranges = ts_tree_included_ranges(*tree, &len); TSRange *ranges = ts_tree_included_ranges(ud->tree, &len);
push_ranges(L, ranges, len, include_bytes); push_ranges(L, ranges, len, include_bytes);
@ -676,20 +682,17 @@ static int parser_get_timeout(lua_State *L)
/// push tree interface on lua stack. /// push tree interface on lua stack.
/// ///
/// This makes a copy of the tree, so ownership of the argument is unaffected. /// The tree is garbage collected if gc is true
void push_tree(lua_State *L, TSTree *tree, bool do_copy) void push_tree(lua_State *L, TSTree *tree, bool gc)
{ {
if (tree == NULL) { if (tree == NULL) {
lua_pushnil(L); lua_pushnil(L);
return; return;
} }
TSTree **ud = lua_newuserdata(L, sizeof(TSTree *)); // [udata] TSLuaTree *ud = lua_newuserdata(L, sizeof(TSLuaTree)); // [udata]
if (do_copy) { ud->tree = tree;
*ud = ts_tree_copy(tree); ud->gc = gc;
} else {
*ud = tree;
}
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_TREE); // [udata, meta] lua_getfield(L, LUA_REGISTRYINDEX, TS_META_TREE); // [udata, meta]
lua_setmetatable(L, -2); // [udata] lua_setmetatable(L, -2); // [udata]
@ -703,20 +706,18 @@ void push_tree(lua_State *L, TSTree *tree, bool do_copy)
lua_setfenv(L, -2); // [udata] lua_setfenv(L, -2); // [udata]
} }
static TSTree **tree_check(lua_State *L, int index) static TSLuaTree *tree_check(lua_State *L, int index)
{ {
TSTree **ud = luaL_checkudata(L, index, TS_META_TREE); TSLuaTree *ud = luaL_checkudata(L, index, TS_META_TREE);
return ud; return ud;
} }
static int tree_gc(lua_State *L) static int tree_gc(lua_State *L)
{ {
TSTree **tree = tree_check(L, 1); TSLuaTree *ud = tree_check(L, 1);
if (!tree) { if (ud && ud->gc) {
return 0; ts_tree_delete(ud->tree);
} }
ts_tree_delete(*tree);
return 0; return 0;
} }
@ -728,11 +729,11 @@ static int tree_tostring(lua_State *L)
static int tree_root(lua_State *L) static int tree_root(lua_State *L)
{ {
TSTree **tree = tree_check(L, 1); TSLuaTree *ud = tree_check(L, 1);
if (!tree) { if (!ud) {
return 0; return 0;
} }
TSNode root = ts_tree_root_node(*tree); TSNode root = ts_tree_root_node(ud->tree);
push_node(L, root, 1); push_node(L, root, 1);
return 1; return 1;
} }

View File

@ -4,6 +4,7 @@ local clear = helpers.clear
local eq = helpers.eq local eq = helpers.eq
local exec_lua = helpers.exec_lua local exec_lua = helpers.exec_lua
local insert = helpers.insert local insert = helpers.insert
local assert_alive = helpers.assert_alive
before_each(clear) before_each(clear)
@ -14,6 +15,17 @@ end
describe('treesitter node API', function() describe('treesitter node API', function()
clear() clear()
it('double free tree', function()
insert('F')
exec_lua([[
vim.treesitter.start(0, 'lua')
vim.treesitter.get_node():tree()
vim.treesitter.get_node():tree()
collectgarbage()
]])
assert_alive()
end)
it('can move between siblings', function() it('can move between siblings', function()
insert([[ insert([[
int main(int x, int y, int z) { int main(int x, int y, int z) {