From 6c2e0588d99a24a289b35abf7535c7025fa0c271 Mon Sep 17 00:00:00 2001 From: vi2q Date: Sat, 29 Aug 2026 07:49:48 +0900 Subject: [PATCH 1/2] fix(rt): reserve stack slots in Table raw operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit raw_set/raw_get push up to three stack slots (plus the extra copy value_from_stack adds for reference types) without calling lua_checkstack. When the caller's call-info frame margin is already thin — for example after a long sequence of VM operations, like compiling many scripts in one VM — lua_pushvalue trips the top < ci.top LUAU_ASSERT and aborts the process (EXC_BREAKPOINT) instead of returning an error. Add an ensure_stack helper that reserves the needed slots up front and returns a MemoryError when the reservation fails, and use it in raw_set and raw_get. --- crates/luaur-rt/src/table.rs | 18 ++++++++++++++ crates/luaur-rt/tests/mlua_core.rs | 40 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/crates/luaur-rt/src/table.rs b/crates/luaur-rt/src/table.rs index 078f7610..1929127e 100644 --- a/crates/luaur-rt/src/table.rs +++ b/crates/luaur-rt/src/table.rs @@ -19,6 +19,22 @@ pub struct Table { pub(crate) _not_sync: NotSync, } +/// Reserve `slots` stack slots for a raw table operation. +/// +/// `lua_checkstack` raises the stack top inside the current call-info frame; +/// without it, `lua_pushvalue` (and the extra copy `value_from_stack` pushes +/// for reference types) can trip the `top < ci.top` LUAU_ASSERT and abort the +/// process when the caller's stack margin is already thin (for example after +/// a long sequence of VM operations in the same frame). +pub(crate) fn ensure_stack(state: *mut lua_State, slots: c_int) -> Result<()> { + if unsafe { lua_checkstack(state, slots) } == 0 { + return Err(crate::error::Error::MemoryError( + "stack overflow while preparing a table operation".to_string(), + )); + } + Ok(()) +} + impl Table { pub(crate) fn from_ref(reference: LuaRef) -> Table { Table { @@ -209,6 +225,7 @@ impl Table { "attempt to modify a readonly table".to_string(), )); } + ensure_stack(state, 3)?; unsafe { self.reference.push(); // table lua.push_value(&k)?; // key @@ -226,6 +243,7 @@ impl Table { let lua = self.lua(); let state = lua.state(); let k = key.into_lua(&lua)?; + ensure_stack(state, 3)?; let value = unsafe { self.reference.push(); // table lua.push_value(&k)?; // key diff --git a/crates/luaur-rt/tests/mlua_core.rs b/crates/luaur-rt/tests/mlua_core.rs index 1ccad287..f28b47b5 100644 --- a/crates/luaur-rt/tests/mlua_core.rs +++ b/crates/luaur-rt/tests/mlua_core.rs @@ -1283,3 +1283,43 @@ fn test_inspect_stack_deferred() -> Result<()> { Ok(()) } + +// Raw table operations must reserve stack space instead of tripping the VM's +// `top < ci.top` assertion when the caller's frame margin is thin. Regression +// test: run enough VM work in one call frame to drain the margin, then hammer +// raw_get/raw_set. +#[test] +fn test_raw_table_ops_survive_a_thin_stack_margin() -> Result<()> { + let lua = Lua::new(); + + // Burn interpreter budget inside one protected call so the following raw + // operations run with a narrow stack margin, like a deep compile sequence. + let table = lua.create_table(); + for i in 0..2000i64 { + table.raw_set(format!("k{i}"), i)?; + } + for i in 0..2000i64 { + let v: i64 = table.raw_get(format!("k{i}"))?; + assert_eq!(v, i); + } + + // Deep recursion inside the VM, then raw ops from the host right after. + lua.load( + r#" + function deep(n) + if n == 0 then return 0 end + return 1 + deep(n - 1) + end + return deep(150) + "#, + ) + .eval::()?; + + for i in 0..500i64 { + table.raw_set(format!("post{i}"), i)?; + let v: i64 = table.raw_get(format!("post{i}"))?; + assert_eq!(v, i); + } + + Ok(()) +} From 2edef5f761305f60b4f438e642258eb46820b954 Mon Sep 17 00:00:00 2001 From: pawel Date: Mon, 31 Aug 2026 10:25:42 +0200 Subject: [PATCH 2/2] review: use RuntimeError for the raw-op stack guard and scope the test claim - Match the crate's existing convention (Function::call, Thread::resume, callback results): a stack reservation failure is a catchable RuntimeError("stack overflow: ..."), not a MemoryError. - Rename/re-comment the new test: it exercises sustained raw traffic, but a C frame always gets LUA_MINSTACK headroom, so it does not by itself drive the margin low enough to trip the assertion. --- crates/luaur-rt/src/table.rs | 6 ++++-- crates/luaur-rt/tests/mlua_core.rs | 13 ++++++------- fuzz/seeds/metamorphic/alias | 2 ++ fuzz/seeds/metamorphic/annot | 2 ++ fuzz/seeds/metamorphic/control | 2 ++ fuzz/seeds/metamorphic/expr | 1 + fuzz/seeds/metamorphic/func | 1 + fuzz/seeds/metamorphic/table | 2 ++ fuzz/seeds/optdiff/alias | 2 ++ fuzz/seeds/optdiff/annot | 2 ++ fuzz/seeds/optdiff/control | 2 ++ fuzz/seeds/optdiff/expr | 1 + fuzz/seeds/optdiff/func | 1 + fuzz/seeds/optdiff/table | 2 ++ fuzz/seeds/roundtrip/alias | 2 ++ fuzz/seeds/roundtrip/annot | 2 ++ fuzz/seeds/roundtrip/control | 2 ++ fuzz/seeds/roundtrip/expr | 1 + fuzz/seeds/roundtrip/func | 1 + fuzz/seeds/roundtrip/table | 2 ++ fuzz/seeds/splice/alias | 2 ++ fuzz/seeds/splice/annot | 2 ++ fuzz/seeds/splice/control | 2 ++ fuzz/seeds/splice/expr | 1 + fuzz/seeds/splice/func | 1 + fuzz/seeds/splice/table | 2 ++ fuzz/seeds/typeck_typed/alias | 2 ++ fuzz/seeds/typeck_typed/annot | 2 ++ fuzz/seeds/typeck_typed/control | 2 ++ fuzz/seeds/typeck_typed/expr | 1 + fuzz/seeds/typeck_typed/func | 1 + fuzz/seeds/typeck_typed/table | 2 ++ 32 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 fuzz/seeds/metamorphic/alias create mode 100644 fuzz/seeds/metamorphic/annot create mode 100644 fuzz/seeds/metamorphic/control create mode 100644 fuzz/seeds/metamorphic/expr create mode 100644 fuzz/seeds/metamorphic/func create mode 100644 fuzz/seeds/metamorphic/table create mode 100644 fuzz/seeds/optdiff/alias create mode 100644 fuzz/seeds/optdiff/annot create mode 100644 fuzz/seeds/optdiff/control create mode 100644 fuzz/seeds/optdiff/expr create mode 100644 fuzz/seeds/optdiff/func create mode 100644 fuzz/seeds/optdiff/table create mode 100644 fuzz/seeds/roundtrip/alias create mode 100644 fuzz/seeds/roundtrip/annot create mode 100644 fuzz/seeds/roundtrip/control create mode 100644 fuzz/seeds/roundtrip/expr create mode 100644 fuzz/seeds/roundtrip/func create mode 100644 fuzz/seeds/roundtrip/table create mode 100644 fuzz/seeds/splice/alias create mode 100644 fuzz/seeds/splice/annot create mode 100644 fuzz/seeds/splice/control create mode 100644 fuzz/seeds/splice/expr create mode 100644 fuzz/seeds/splice/func create mode 100644 fuzz/seeds/splice/table create mode 100644 fuzz/seeds/typeck_typed/alias create mode 100644 fuzz/seeds/typeck_typed/annot create mode 100644 fuzz/seeds/typeck_typed/control create mode 100644 fuzz/seeds/typeck_typed/expr create mode 100644 fuzz/seeds/typeck_typed/func create mode 100644 fuzz/seeds/typeck_typed/table diff --git a/crates/luaur-rt/src/table.rs b/crates/luaur-rt/src/table.rs index 1929127e..a944453b 100644 --- a/crates/luaur-rt/src/table.rs +++ b/crates/luaur-rt/src/table.rs @@ -28,8 +28,10 @@ pub struct Table { /// a long sequence of VM operations in the same frame). pub(crate) fn ensure_stack(state: *mut lua_State, slots: c_int) -> Result<()> { if unsafe { lua_checkstack(state, slots) } == 0 { - return Err(crate::error::Error::MemoryError( - "stack overflow while preparing a table operation".to_string(), + // Same shape as the guards in `Function::call` / `Thread::resume`: + // a catchable `RuntimeError` rather than a VM abort. + return Err(crate::error::Error::RuntimeError( + "stack overflow: not enough stack space for a raw table operation".to_string(), )); } Ok(()) diff --git a/crates/luaur-rt/tests/mlua_core.rs b/crates/luaur-rt/tests/mlua_core.rs index f28b47b5..d5b3d127 100644 --- a/crates/luaur-rt/tests/mlua_core.rs +++ b/crates/luaur-rt/tests/mlua_core.rs @@ -1284,16 +1284,15 @@ fn test_inspect_stack_deferred() -> Result<()> { Ok(()) } -// Raw table operations must reserve stack space instead of tripping the VM's -// `top < ci.top` assertion when the caller's frame margin is thin. Regression -// test: run enough VM work in one call frame to drain the margin, then hammer -// raw_get/raw_set. +// `raw_set` / `raw_get` reserve their stack slots up front (`lua_checkstack`) +// instead of pushing blind, matching the guards in `Function::call` / +// `Thread::resume`. Coverage for sustained raw traffic interleaved with VM +// work; it does not by itself drive the frame margin low enough to trip the +// VM's `top < ci.top` assertion (a C frame always gets LUA_MINSTACK headroom). #[test] -fn test_raw_table_ops_survive_a_thin_stack_margin() -> Result<()> { +fn test_raw_table_ops_reserve_stack_space() -> Result<()> { let lua = Lua::new(); - // Burn interpreter budget inside one protected call so the following raw - // operations run with a narrow stack margin, like a deep compile sequence. let table = lua.create_table(); for i in 0..2000i64 { table.raw_set(format!("k{i}"), i)?; diff --git a/fuzz/seeds/metamorphic/alias b/fuzz/seeds/metamorphic/alias new file mode 100644 index 00000000..d6ff7900 --- /dev/null +++ b/fuzz/seeds/metamorphic/alias @@ -0,0 +1,2 @@ +type T = {x: number} +local v: T = {x=1} \ No newline at end of file diff --git a/fuzz/seeds/metamorphic/annot b/fuzz/seeds/metamorphic/annot new file mode 100644 index 00000000..4070d3de --- /dev/null +++ b/fuzz/seeds/metamorphic/annot @@ -0,0 +1,2 @@ +local x: number = 1 +return x \ No newline at end of file diff --git a/fuzz/seeds/metamorphic/control b/fuzz/seeds/metamorphic/control new file mode 100644 index 00000000..fd90dec0 --- /dev/null +++ b/fuzz/seeds/metamorphic/control @@ -0,0 +1,2 @@ +for i=1,3 do end +while true do break end \ No newline at end of file diff --git a/fuzz/seeds/metamorphic/expr b/fuzz/seeds/metamorphic/expr new file mode 100644 index 00000000..ff7cac51 --- /dev/null +++ b/fuzz/seeds/metamorphic/expr @@ -0,0 +1 @@ +return 1 + 2 \ No newline at end of file diff --git a/fuzz/seeds/metamorphic/func b/fuzz/seeds/metamorphic/func new file mode 100644 index 00000000..5394f136 --- /dev/null +++ b/fuzz/seeds/metamorphic/func @@ -0,0 +1 @@ +function f(a: string): number return #a end \ No newline at end of file diff --git a/fuzz/seeds/metamorphic/table b/fuzz/seeds/metamorphic/table new file mode 100644 index 00000000..6f0fcb7d --- /dev/null +++ b/fuzz/seeds/metamorphic/table @@ -0,0 +1,2 @@ +local t = {a=1, b="s"} +return t.a \ No newline at end of file diff --git a/fuzz/seeds/optdiff/alias b/fuzz/seeds/optdiff/alias new file mode 100644 index 00000000..d6ff7900 --- /dev/null +++ b/fuzz/seeds/optdiff/alias @@ -0,0 +1,2 @@ +type T = {x: number} +local v: T = {x=1} \ No newline at end of file diff --git a/fuzz/seeds/optdiff/annot b/fuzz/seeds/optdiff/annot new file mode 100644 index 00000000..4070d3de --- /dev/null +++ b/fuzz/seeds/optdiff/annot @@ -0,0 +1,2 @@ +local x: number = 1 +return x \ No newline at end of file diff --git a/fuzz/seeds/optdiff/control b/fuzz/seeds/optdiff/control new file mode 100644 index 00000000..fd90dec0 --- /dev/null +++ b/fuzz/seeds/optdiff/control @@ -0,0 +1,2 @@ +for i=1,3 do end +while true do break end \ No newline at end of file diff --git a/fuzz/seeds/optdiff/expr b/fuzz/seeds/optdiff/expr new file mode 100644 index 00000000..ff7cac51 --- /dev/null +++ b/fuzz/seeds/optdiff/expr @@ -0,0 +1 @@ +return 1 + 2 \ No newline at end of file diff --git a/fuzz/seeds/optdiff/func b/fuzz/seeds/optdiff/func new file mode 100644 index 00000000..5394f136 --- /dev/null +++ b/fuzz/seeds/optdiff/func @@ -0,0 +1 @@ +function f(a: string): number return #a end \ No newline at end of file diff --git a/fuzz/seeds/optdiff/table b/fuzz/seeds/optdiff/table new file mode 100644 index 00000000..6f0fcb7d --- /dev/null +++ b/fuzz/seeds/optdiff/table @@ -0,0 +1,2 @@ +local t = {a=1, b="s"} +return t.a \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/alias b/fuzz/seeds/roundtrip/alias new file mode 100644 index 00000000..d6ff7900 --- /dev/null +++ b/fuzz/seeds/roundtrip/alias @@ -0,0 +1,2 @@ +type T = {x: number} +local v: T = {x=1} \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/annot b/fuzz/seeds/roundtrip/annot new file mode 100644 index 00000000..4070d3de --- /dev/null +++ b/fuzz/seeds/roundtrip/annot @@ -0,0 +1,2 @@ +local x: number = 1 +return x \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/control b/fuzz/seeds/roundtrip/control new file mode 100644 index 00000000..fd90dec0 --- /dev/null +++ b/fuzz/seeds/roundtrip/control @@ -0,0 +1,2 @@ +for i=1,3 do end +while true do break end \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/expr b/fuzz/seeds/roundtrip/expr new file mode 100644 index 00000000..ff7cac51 --- /dev/null +++ b/fuzz/seeds/roundtrip/expr @@ -0,0 +1 @@ +return 1 + 2 \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/func b/fuzz/seeds/roundtrip/func new file mode 100644 index 00000000..5394f136 --- /dev/null +++ b/fuzz/seeds/roundtrip/func @@ -0,0 +1 @@ +function f(a: string): number return #a end \ No newline at end of file diff --git a/fuzz/seeds/roundtrip/table b/fuzz/seeds/roundtrip/table new file mode 100644 index 00000000..6f0fcb7d --- /dev/null +++ b/fuzz/seeds/roundtrip/table @@ -0,0 +1,2 @@ +local t = {a=1, b="s"} +return t.a \ No newline at end of file diff --git a/fuzz/seeds/splice/alias b/fuzz/seeds/splice/alias new file mode 100644 index 00000000..d6ff7900 --- /dev/null +++ b/fuzz/seeds/splice/alias @@ -0,0 +1,2 @@ +type T = {x: number} +local v: T = {x=1} \ No newline at end of file diff --git a/fuzz/seeds/splice/annot b/fuzz/seeds/splice/annot new file mode 100644 index 00000000..4070d3de --- /dev/null +++ b/fuzz/seeds/splice/annot @@ -0,0 +1,2 @@ +local x: number = 1 +return x \ No newline at end of file diff --git a/fuzz/seeds/splice/control b/fuzz/seeds/splice/control new file mode 100644 index 00000000..fd90dec0 --- /dev/null +++ b/fuzz/seeds/splice/control @@ -0,0 +1,2 @@ +for i=1,3 do end +while true do break end \ No newline at end of file diff --git a/fuzz/seeds/splice/expr b/fuzz/seeds/splice/expr new file mode 100644 index 00000000..ff7cac51 --- /dev/null +++ b/fuzz/seeds/splice/expr @@ -0,0 +1 @@ +return 1 + 2 \ No newline at end of file diff --git a/fuzz/seeds/splice/func b/fuzz/seeds/splice/func new file mode 100644 index 00000000..5394f136 --- /dev/null +++ b/fuzz/seeds/splice/func @@ -0,0 +1 @@ +function f(a: string): number return #a end \ No newline at end of file diff --git a/fuzz/seeds/splice/table b/fuzz/seeds/splice/table new file mode 100644 index 00000000..6f0fcb7d --- /dev/null +++ b/fuzz/seeds/splice/table @@ -0,0 +1,2 @@ +local t = {a=1, b="s"} +return t.a \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/alias b/fuzz/seeds/typeck_typed/alias new file mode 100644 index 00000000..d6ff7900 --- /dev/null +++ b/fuzz/seeds/typeck_typed/alias @@ -0,0 +1,2 @@ +type T = {x: number} +local v: T = {x=1} \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/annot b/fuzz/seeds/typeck_typed/annot new file mode 100644 index 00000000..4070d3de --- /dev/null +++ b/fuzz/seeds/typeck_typed/annot @@ -0,0 +1,2 @@ +local x: number = 1 +return x \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/control b/fuzz/seeds/typeck_typed/control new file mode 100644 index 00000000..fd90dec0 --- /dev/null +++ b/fuzz/seeds/typeck_typed/control @@ -0,0 +1,2 @@ +for i=1,3 do end +while true do break end \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/expr b/fuzz/seeds/typeck_typed/expr new file mode 100644 index 00000000..ff7cac51 --- /dev/null +++ b/fuzz/seeds/typeck_typed/expr @@ -0,0 +1 @@ +return 1 + 2 \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/func b/fuzz/seeds/typeck_typed/func new file mode 100644 index 00000000..5394f136 --- /dev/null +++ b/fuzz/seeds/typeck_typed/func @@ -0,0 +1 @@ +function f(a: string): number return #a end \ No newline at end of file diff --git a/fuzz/seeds/typeck_typed/table b/fuzz/seeds/typeck_typed/table new file mode 100644 index 00000000..6f0fcb7d --- /dev/null +++ b/fuzz/seeds/typeck_typed/table @@ -0,0 +1,2 @@ +local t = {a=1, b="s"} +return t.a \ No newline at end of file