Skip to content

Commit 85c7c6f

Browse files
committed
Guard against running a C function multiple times without an interrupt check in C functions accepting callbacks
1 parent 557f067 commit 85c7c6f

File tree

5 files changed

+142
-92
lines changed

5 files changed

+142
-92
lines changed

VM/src/lstrlib.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ static void add_value(MatchState* ms, luaL_Strbuf* b, const char* s, const char*
808808
int n;
809809
lua_pushvalue(L, 3);
810810
n = push_captures(ms, s, e);
811+
// ServerLua: Check for interrupt to allow pre-emptive abort before calling replacement function
812+
luau_callinterrupthandler(L, -4);
811813
lua_call(L, n, 1);
812814
break;
813815
}

VM/src/ltablib.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ static int foreachi(lua_State* L)
2424
lua_pushvalue(L, 2); // function
2525
lua_pushinteger(L, i); // 1st argument
2626
lua_rawgeti(L, 1, i); // 2nd argument
27+
// ServerLua: Check for interrupt to allow pre-emptive abort before calling user iterator function
28+
luau_callinterrupthandler(L, -4);
2729
lua_call(L, 2, 1);
2830
if (!lua_isnil(L, -1))
2931
return 1;
@@ -42,6 +44,8 @@ static int foreach (lua_State* L)
4244
lua_pushvalue(L, 2); // function
4345
lua_pushvalue(L, -3); // key
4446
lua_pushvalue(L, -3); // value
47+
// ServerLua: Check for interrupt to allow pre-emptive abort before calling user iterator function
48+
luau_callinterrupthandler(L, -4);
4549
lua_call(L, 2, 1);
4650
if (!lua_isnil(L, -1))
4751
return 1;
@@ -329,6 +333,8 @@ static int sort_func(lua_State* L, const TValue* l, const TValue* r)
329333
setobj2s(L, L->top + 1, l);
330334
setobj2s(L, L->top + 2, r);
331335
L->top += 3; // safe because of LUA_MINSTACK guarantee
336+
// ServerLua: Check for interrupt to allow pre-emptive abort before calling user comparison function
337+
luau_callinterrupthandler(L, -4);
332338
luaD_call(L, L->top - 3, 1);
333339
L->top -= 1; // maintain stack depth
334340

tests/SLConformance.test.cpp

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ typedef struct SLTestRuntimeState : lua_SLRuntimeState
5252

5353
// Flags for interrupt check testing
5454
bool interrupt_should_clear = false;
55-
bool metamethod_ran = false;
55+
bool callback_ran = false;
5656
} RuntimeState;
5757

5858

@@ -729,37 +729,53 @@ static int reset_interrupt_test(lua_State* L)
729729
{
730730
RuntimeState* state = (RuntimeState*)L->userdata;
731731
state->interrupt_should_clear = true;
732-
state->metamethod_ran = false;
732+
state->callback_ran = false;
733733
return 0;
734734
}
735735

736-
static int check_metamethod_ran(lua_State* L)
736+
static int check_callback_ran(lua_State* L)
737737
{
738738
RuntimeState* state = (RuntimeState*)L->userdata;
739-
lua_pushboolean(L, state->metamethod_ran);
739+
lua_pushboolean(L, state->callback_ran);
740740
return 1;
741741
}
742742

743-
static int test_metamethod(lua_State* L)
743+
static int test_callback(lua_State* L)
744744
{
745745
RuntimeState* state = (RuntimeState*)L->userdata;
746746

747747
if (state->interrupt_should_clear)
748748
{
749-
luaL_error(L, "Interrupt handler did not run before metamethod!");
749+
luaL_error(L, "Interrupt handler did not run before callback!");
750750
}
751751

752-
state->metamethod_ran = true;
752+
state->callback_ran = true;
753753
lua_pushstring(L, "ok");
754754
return 1;
755755
}
756756

757-
TEST_CASE("Metamethods receive interrupt checks")
757+
static int test_sort_callback(lua_State* L)
758758
{
759-
runConformance("metamethod_interrupts.lua", nullptr, [](lua_State* L) {
760-
// Set up interrupt handler that clears the flag only for metamethod interrupts (-3)
759+
RuntimeState* state = (RuntimeState*)L->userdata;
760+
761+
if (state->interrupt_should_clear)
762+
{
763+
luaL_error(L, "Interrupt handler did not run before sort comparison!");
764+
}
765+
766+
state->callback_ran = true;
767+
// `table.sort()` gets very angry if we don't do something that looks
768+
// like sorting _eventually_
769+
lua_pushboolean(L, lua_lessthan(L, 1, 2));
770+
return 1;
771+
}
772+
773+
TEST_CASE("Metamethods and library callbacks receive interrupt checks")
774+
{
775+
runConformance("metamethod_and_callback_interrupts.lua", nullptr, [](lua_State* L) {
776+
// Set up interrupt handler that clears the flag for metamethod (-3) and library callback (-4) interrupts
761777
lua_callbacks(L)->interrupt = [](lua_State* L, int gc) {
762-
if (gc != -3)
778+
if (gc != -3 && gc != -4)
763779
return;
764780

765781
RuntimeState* state = (RuntimeState*)L->userdata;
@@ -770,11 +786,14 @@ TEST_CASE("Metamethods receive interrupt checks")
770786
lua_pushcfunction(L, reset_interrupt_test, "reset_interrupt_test");
771787
lua_setglobal(L, "reset_interrupt_test");
772788

773-
lua_pushcfunction(L, check_metamethod_ran, "check_metamethod_ran");
774-
lua_setglobal(L, "check_metamethod_ran");
789+
lua_pushcfunction(L, check_callback_ran, "check_callback_ran");
790+
lua_setglobal(L, "check_callback_ran");
791+
792+
lua_pushcfunction(L, test_callback, "test_callback");
793+
lua_setglobal(L, "test_callback");
775794

776-
lua_pushcfunction(L, test_metamethod, "test_metamethod");
777-
lua_setglobal(L, "test_metamethod");
795+
lua_pushcfunction(L, test_sort_callback, "test_sort_callback");
796+
lua_setglobal(L, "test_sort_callback");
778797
});
779798
}
780799

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
-- Test that interrupt handlers are called before metamethods and library callbacks
2+
-- The C side sets up an interrupt handler that clears a flag,
3+
-- and callbacks that throw if the flag wasn't cleared.
4+
5+
-- Create a test object with test_callback as various metamethods
6+
local function create_test_object()
7+
local obj = {}
8+
local mt = {
9+
__tostring = test_callback,
10+
__tojson = test_callback,
11+
__len = test_callback,
12+
__add = test_callback,
13+
__sub = test_callback,
14+
__mul = test_callback,
15+
__div = test_callback,
16+
__unm = test_callback,
17+
__index = test_callback,
18+
__newindex = test_callback,
19+
}
20+
return setmetatable(obj, mt)
21+
end
22+
23+
-- Create test object once to avoid intermediate function calls
24+
local obj = create_test_object()
25+
local obj_len = {}
26+
setmetatable(obj_len, {__len = test_callback})
27+
28+
-- Test __tostring (via luaL_callmeta path)
29+
reset_interrupt_test()
30+
local result = tostring(obj)
31+
assert(result == "ok", "__tostring should return 'ok'")
32+
assert(check_callback_ran(), "__tostring metamethod should have run")
33+
34+
-- Test __tojson (via JSON encoder)
35+
reset_interrupt_test()
36+
result = lljson.encode(obj)
37+
assert(result == '"ok"', "__tojson should return '\"ok\"'")
38+
assert(check_callback_ran(), "__tojson metamethod should have run")
39+
40+
-- Test __len (via JSON encoder when no __tojson)
41+
reset_interrupt_test()
42+
result = lljson.encode(obj_len)
43+
assert(check_callback_ran(), "__len metamethod should have run")
44+
45+
-- Test arithmetic metamethods (__add, __sub, __mul, __div, __unm)
46+
-- These go through callTMres or luaV_callTM paths
47+
reset_interrupt_test()
48+
local _ = obj + obj
49+
assert(check_callback_ran(), "__add metamethod should have run")
50+
51+
reset_interrupt_test()
52+
_ = obj - obj
53+
assert(check_callback_ran(), "__sub metamethod should have run")
54+
55+
reset_interrupt_test()
56+
_ = obj * obj
57+
assert(check_callback_ran(), "__mul metamethod should have run")
58+
59+
reset_interrupt_test()
60+
_ = obj / obj
61+
assert(check_callback_ran(), "__div metamethod should have run")
62+
63+
reset_interrupt_test()
64+
_ = -obj
65+
assert(check_callback_ran(), "__unm metamethod should have run")
66+
67+
-- Test __index (via callTMres path)
68+
reset_interrupt_test()
69+
_ = obj.some_field
70+
assert(check_callback_ran(), "__index metamethod should have run")
71+
72+
-- Test __newindex (via callTM path)
73+
reset_interrupt_test()
74+
obj.some_field = 123
75+
assert(check_callback_ran(), "__newindex metamethod should have run")
76+
77+
-- Test library function callbacks
78+
79+
-- Create test table and data
80+
local test_table = {1, 2, 3, 4, 5}
81+
local test_dict = {a = 1, b = 2, c = 3, d = 4, e = 5}
82+
local test_string = "aabbbcccc"
83+
84+
reset_interrupt_test()
85+
table.sort(test_table, test_sort_callback)
86+
assert(check_callback_ran(), "table.sort comparison function should have run")
87+
88+
reset_interrupt_test()
89+
table.foreachi(test_table, test_callback)
90+
assert(check_callback_ran(), "table.foreachi callback should have run")
91+
92+
reset_interrupt_test()
93+
table.foreach(test_dict, test_callback)
94+
assert(check_callback_ran(), "table.foreach callback should have run")
95+
96+
reset_interrupt_test()
97+
local result = string.gsub(test_string, "a", test_callback)
98+
assert(check_callback_ran(), "string.gsub replacement function should have run")
99+
100+
return "OK"

tests/conformance/metamethod_interrupts.lua

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)