fix(treesitter): ensure TSNode's tree is immutable

Problem:

TSNode contains a `const TSTree*` and a `const void *id`. The `id`
points to Tree-sitter's internal type `Subtree`, which resides inside
the `TSTree` but may be deallocated if the `TSTree` is mutated (which
is likely why it is `const`).

The Lua method `TSTree:edit()` mutates the tree, which can deallocate
`id`.

See #25254 and #31758.

Solution:

To avoid this, we now make a copy of the tree before pushing its root to
the Lua stack. This also removes the fenv from TSLuaTree, as it was only
used when pushing the tree root to the Lua stack.

We also copy the tree in `node_tree`.

`ts_tree_copy()` just increments a couple of reference counters, so it's
relatively cheap to call.

(cherry picked from commit 99e6294819)
This commit is contained in:
Rodrigodd
2025-06-03 21:53:58 -03:00
committed by github-actions[bot]
parent c13eba5254
commit 07d9197840

View File

@ -774,20 +774,9 @@ static void push_tree(lua_State *L, TSTree *tree)
}
TSLuaTree *ud = lua_newuserdata(L, sizeof(TSLuaTree)); // [udata]
ud->tree = tree;
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_TREE); // [udata, meta]
lua_setmetatable(L, -2); // [udata]
// To prevent the tree from being garbage collected, create a reference to it
// in the fenv which will be passed to userdata nodes of the tree.
// Note: environments (fenvs) associated with userdata have no meaning in Lua
// and are only used to associate a table.
lua_createtable(L, 1, 0); // [udata, reftable]
lua_pushvalue(L, -2); // [udata, reftable, udata]
lua_rawseti(L, -2, 1); // [udata, reftable]
lua_setfenv(L, -2); // [udata]
}
static int tree_copy(lua_State *L)
@ -854,8 +843,27 @@ static int tree_tostring(lua_State *L)
static int tree_root(lua_State *L)
{
TSLuaTree *ud = luaL_checkudata(L, 1, TS_META_TREE);
TSNode root = ts_tree_root_node(ud->tree);
push_node(L, root, 1);
// The tree may be mutate by tree_edit, but node needs that its tree is not
// mutated while the node is alive. So we make a copy of tree here.
TSTree *tree_copy = ts_tree_copy(ud->tree);
TSNode root = ts_tree_root_node(tree_copy);
TSNode *node_ud = lua_newuserdata(L, sizeof(TSNode)); // [node]
*node_ud = root;
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_NODE); // [node, meta]
lua_setmetatable(L, -2); // [node]
// To prevent the tree from being garbage collected, create a reference to it
// in the fenv which will be passed to userdata nodes of the tree.
// Note: environments (fenvs) associated with userdata have no meaning in Lua
// and are only used to associate a table.
lua_createtable(L, 1, 0); // [node, reftable]
push_tree(L, tree_copy); // [node, reftable, tree]
lua_rawseti(L, -2, 1); // [node, reftable]
lua_setfenv(L, -2); // [node]
return 1;
}
@ -902,9 +910,9 @@ static struct luaL_Reg node_meta[] = {
/// Push node interface on to the Lua stack
///
/// Top of stack must either be the tree this node belongs to or another node
/// of the same tree! This value is not popped. Can only be called inside a
/// cfunction with the tslua environment.
/// Stack at `uindex` must have a value with a fenv with a reference to node's
/// tree. This value is not popped. Can only be called inside a cfunction with
/// the tslua environment.
static void push_node(lua_State *L, TSNode node, int uindex)
{
assert(uindex > 0 || uindex < -LUA_MINSTACK);
@ -912,12 +920,13 @@ static void push_node(lua_State *L, TSNode node, int uindex)
lua_pushnil(L); // [nil]
return;
}
TSNode *ud = lua_newuserdata(L, sizeof(TSNode)); // [udata]
*ud = node;
lua_getfield(L, LUA_REGISTRYINDEX, TS_META_NODE); // [udata, meta]
lua_setmetatable(L, -2); // [udata]
// Copy the fenv which contains the nodes tree.
// Copy the fenv to keep alive a reference to the node's tree.
lua_getfenv(L, uindex); // [udata, reftable]
lua_setfenv(L, -2); // [udata]
}
@ -1303,9 +1312,13 @@ static int node_root(lua_State *L)
static int node_tree(lua_State *L)
{
node_check(L, 1);
lua_getfenv(L, 1); // [udata, reftable]
lua_rawgeti(L, -1, 1); // [udata, reftable, tree_udata]
TSNode node = node_check(L, 1);
// The node's tree must not be mutated, but `tree_edit` may violate that. So
// we make a copy of the tree before pushing it to the LUA stack.
TSTree *tree = ts_tree_copy(node.tree);
push_tree(L, tree);
return 1;
}