diff --git a/crates/luaur-rt/src/table.rs b/crates/luaur-rt/src/table.rs index 078f7610..a944453b 100644 --- a/crates/luaur-rt/src/table.rs +++ b/crates/luaur-rt/src/table.rs @@ -19,6 +19,24 @@ 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 { + // 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(()) +} + impl Table { pub(crate) fn from_ref(reference: LuaRef) -> Table { Table { @@ -209,6 +227,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 +245,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..d5b3d127 100644 --- a/crates/luaur-rt/tests/mlua_core.rs +++ b/crates/luaur-rt/tests/mlua_core.rs @@ -1283,3 +1283,42 @@ fn test_inspect_stack_deferred() -> Result<()> { Ok(()) } + +// `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_reserve_stack_space() -> Result<()> { + let lua = Lua::new(); + + 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(()) +} 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