luaur_rt/luau_ext.rs
1//! Luau-specific `Lua` extensions: sandboxing, safeenv, fflags, and the
2//! per-VM compiler. Mirrors the Luau-only parts of mlua's `Lua` surface.
3
4use std::cell::RefCell;
5use std::collections::HashMap;
6
7use crate::compiler::Compiler;
8use crate::error::{Error, Result};
9use crate::state::Lua;
10use crate::sys::*;
11use crate::table::Table;
12use crate::thread::Thread;
13
14thread_local! {
15 /// Per-VM saved *original* globals table (used to revert `sandbox(false)`),
16 /// keyed by the global-state pointer.
17 static SANDBOX_SAVED_GLOBALS: RefCell<HashMap<*mut core::ffi::c_void, Table>> =
18 RefCell::new(HashMap::new());
19
20 /// Per-VM compiler installed via `Lua::set_compiler`, keyed by global state.
21 static VM_COMPILERS: RefCell<HashMap<*mut core::ffi::c_void, Compiler>> =
22 RefCell::new(HashMap::new());
23
24 /// Per-VM `catch_rust_panics` option (recorded from `LuaOptions`), keyed by
25 /// global state. Currently recorded for parity; see `set_catch_rust_panics`.
26 static VM_CATCH_PANICS: RefCell<HashMap<*mut core::ffi::c_void, bool>> =
27 RefCell::new(HashMap::new());
28
29 /// Per-VM "is sandboxed" flag, keyed by global state. Mirrors mlua's
30 /// `extra.sandboxed`; consulted by `Lua::set_globals`.
31 static VM_SANDBOXED: RefCell<HashMap<*mut core::ffi::c_void, bool>> =
32 RefCell::new(HashMap::new());
33}
34
35unsafe fn global_key(state: *mut lua_State) -> *mut core::ffi::c_void {
36 unsafe { (*state).global as *mut core::ffi::c_void }
37}
38
39impl Lua {
40 /// Enable or disable sandbox mode. Mirrors `mlua::Lua::sandbox`.
41 ///
42 /// Enabling sets every library table (and the globals table) read-only and
43 /// activates `safeenv`, then installs a fresh proxy global table (via
44 /// `luaL_sandboxthread`) so that script-level global writes go to a
45 /// throwaway table whose `__index` is the original environment. Disabling
46 /// restores the original globals table and clears the read-only/safeenv
47 /// flags.
48 ///
49 /// **DEVIATION:** Luau's standard library (as bundled in luaur) does not
50 /// register `collectgarbage`; mlua's sandbox test additionally checks that
51 /// `collectgarbage` is restricted under the sandbox. That part is not
52 /// exercisable here (see `tests/mlua_luau.rs`).
53 pub fn sandbox(&self, enabled: bool) -> Result<()> {
54 let state = self.state();
55 let key = unsafe { global_key(state) };
56 VM_SANDBOXED.with(|m| {
57 m.borrow_mut().insert(key, enabled);
58 });
59 unsafe {
60 if enabled {
61 // Save the original globals table so we can restore it later.
62 let original = self.globals();
63 SANDBOX_SAVED_GLOBALS.with(|m| {
64 m.borrow_mut().entry(key).or_insert(original);
65 });
66 // Make libraries + base metatables read-only and set safeenv.
67 lua_l_sandbox(state);
68 // Install the proxy global table for script-level writes.
69 lua_l_sandboxthread(state);
70 } else {
71 // Restore the original globals table (dropping the proxy and any
72 // globals written into it).
73 let saved = SANDBOX_SAVED_GLOBALS.with(|m| m.borrow_mut().remove(&key));
74 if let Some(orig) = saved {
75 orig.push_to_stack();
76 lua_replace(state, LUA_GLOBALSINDEX);
77 // Clear read-only + safeenv on the restored globals so it is
78 // writable again.
79 lua_setreadonly(state, LUA_GLOBALSINDEX, 0);
80 lua_setsafeenv(state, LUA_GLOBALSINDEX, 0);
81 // Also clear read-only on the library tables.
82 self.clear_library_readonly();
83 }
84 }
85 }
86 Ok(())
87 }
88
89 /// Clear the read-only flag on every library table reachable from the
90 /// (restored) globals. Used when leaving sandbox mode.
91 fn clear_library_readonly(&self) {
92 let globals = self.globals();
93 if let Ok(pairs) = globals
94 .pairs::<crate::value::Value, crate::value::Value>()
95 .collect::<Result<Vec<_>>>()
96 {
97 for (_, v) in pairs {
98 if let crate::value::Value::Table(t) = v {
99 t.set_readonly(false);
100 }
101 }
102 }
103 }
104
105 /// Set or clear the `safeenv` flag on the globals table. Mirrors
106 /// `mlua::Globals::set_safeenv` applied to the main globals.
107 ///
108 /// `safeenv` lets the VM fast-path global reads; clearing it forces the slow
109 /// path (needed when globals/`__index` may change at runtime).
110 pub fn set_safeenv(&self, enabled: bool) {
111 let state = self.state();
112 unsafe {
113 lua_setsafeenv(state, LUA_GLOBALSINDEX, enabled as c_int);
114 }
115 }
116
117 /// Install a default [`Compiler`] used to compile every chunk loaded by this
118 /// VM (unless a chunk overrides it via
119 /// [`Chunk::set_compiler`](crate::Chunk::set_compiler)). Mirrors
120 /// `mlua::Lua::set_compiler`.
121 pub fn set_compiler(&self, compiler: Compiler) {
122 let state = self.state();
123 let key = unsafe { global_key(state) };
124 VM_COMPILERS.with(|m| {
125 m.borrow_mut().insert(key, compiler);
126 });
127 }
128
129 /// Record the `catch_rust_panics` behavioral option for this VM.
130 ///
131 /// **DEVIATION:** luaur-rt's callback trampoline always catches a Rust panic
132 /// and converts it into a catchable Lua error (so the VM is never left
133 /// half-unwound). The mlua option that lets a panic propagate as a Rust
134 /// unwind across the VM boundary is therefore recorded here but not enforced
135 /// — see the deferred `test_panic` in `tests/mlua_core.rs`.
136 pub(crate) fn set_catch_rust_panics(&self, enabled: bool) {
137 let state = self.state();
138 let key = unsafe { global_key(state) };
139 VM_CATCH_PANICS.with(|m| {
140 m.borrow_mut().insert(key, enabled);
141 });
142 }
143
144 /// Whether this VM is currently sandboxed (set by [`Lua::sandbox`]). Mirrors
145 /// mlua's `extra.sandboxed` flag; consulted by [`Lua::set_globals`].
146 pub(crate) fn is_sandboxed(&self) -> bool {
147 let state = self.state();
148 let key = unsafe { global_key(state) };
149 VM_SANDBOXED.with(|m| m.borrow().get(&key).copied().unwrap_or(false))
150 }
151
152 /// The VM-default compiler installed via [`Lua::set_compiler`], if any.
153 pub(crate) fn vm_compiler(&self) -> Option<Compiler> {
154 let state = self.state();
155 let key = unsafe { global_key(state) };
156 VM_COMPILERS.with(|m| m.borrow().get(&key).cloned())
157 }
158
159 /// Set (or clear) the metatable shared by all values of a Luau built-in
160 /// type `T`. Mirrors `mlua::Lua::set_type_metatable`.
161 ///
162 /// Implemented for [`Vector`](crate::Vector), `bool`, [`Number`](f64),
163 /// [`LuaString`](crate::LuaString), [`Function`](crate::Function),
164 /// [`Thread`](crate::Thread), and
165 /// [`LightUserData`](crate::LightUserData). Setting it installs a metatable
166 /// in the VM's global per-type metatable slot, so e.g. `v.x`/`v:method`
167 /// dispatch through it.
168 pub fn set_type_metatable<T: TypeMetatable>(&self, metatable: Option<Table>) {
169 T::set_type_metatable(self, metatable);
170 }
171
172 /// The metatable shared by all values of a Luau built-in type `T`, if one
173 /// has been installed. Mirrors `mlua::Lua::type_metatable`.
174 pub fn type_metatable<T: TypeMetatable>(&self) -> Option<Table> {
175 T::type_metatable(self)
176 }
177
178 /// Set a Luau fast-flag (FFlag) by name. Mirrors `mlua::Lua::set_fflag`.
179 ///
180 /// **DEVIATION:** luaur's FastFlags are a fixed, compile-time `FFlag` enum
181 /// rather than a string-keyed registry, so there is no way to look a flag up
182 /// by an arbitrary name. This therefore always reports the name as unknown
183 /// (`Err`) — which matches mlua's contract for an unrecognized flag (the
184 /// only behavior its `test_fflags` asserts). Known flags are configured at
185 /// VM-construction time via `luaur_common::set_all_flags`.
186 pub fn set_fflag(name: &str, _enabled: bool) -> Result<()> {
187 Err(Error::runtime(format!("fflag '{name}' is not supported")))
188 }
189}
190
191impl Thread {
192 /// Sandbox this coroutine: install a fresh proxy global table on its own
193 /// state so global writes inside the coroutine stay local to it. Mirrors
194 /// `mlua::Thread::sandbox`.
195 pub fn sandbox(&self) -> Result<()> {
196 let co = self.thread_state;
197 unsafe {
198 lua_l_sandboxthread(co);
199 }
200 Ok(())
201 }
202}
203
204/// Luau built-in types that have a shared, per-type metatable settable via
205/// [`Lua::set_type_metatable`]. Mirrors mlua's sealed `LuauType` trait.
206pub trait TypeMetatable: private::Sealed {
207 /// Push a representative value of this type onto the stack (so the VM's
208 /// `lua_setmetatable`/`lua_getmetatable` operate on the type's global slot).
209 #[doc(hidden)]
210 unsafe fn push_representative(state: *mut lua_State);
211
212 /// Install (or clear) the shared metatable for this type.
213 fn set_type_metatable(lua: &Lua, metatable: Option<Table>) {
214 let state = lua.state();
215 unsafe {
216 Self::push_representative(state);
217 match metatable {
218 Some(mt) => mt.push_to_stack(),
219 None => crate::sys::lua_pushnil(state),
220 }
221 // For a non-table/non-userdata value, `lua_setmetatable` stores the
222 // metatable in the VM's global per-type slot (`g->mt[type]`).
223 crate::sys::lua_setmetatable(state, -2);
224 // Pop the representative value left on the stack.
225 crate::sys::lua_pop(state, 1);
226 }
227 }
228
229 /// The shared metatable for this type, if installed.
230 fn type_metatable(lua: &Lua) -> Option<Table> {
231 let state = lua.state();
232 unsafe {
233 Self::push_representative(state);
234 let has = crate::sys::lua_getmetatable(state, -1);
235 if has == 0 {
236 // No metatable: pop the representative value.
237 crate::sys::lua_pop(state, 1);
238 return None;
239 }
240 // stack: [value, metatable]
241 let mt = Table::from_ref(lua.pop_ref());
242 crate::sys::lua_pop(state, 1); // pop the representative value
243 Some(mt)
244 }
245 }
246}
247
248mod private {
249 pub trait Sealed {}
250 impl Sealed for crate::vector::Vector {}
251 impl Sealed for bool {}
252 impl Sealed for f64 {}
253 impl Sealed for crate::string::LuaString {}
254 impl Sealed for crate::function::Function {}
255 impl Sealed for crate::thread::Thread {}
256 impl Sealed for crate::light_userdata::LightUserData {}
257}
258
259impl TypeMetatable for crate::vector::Vector {
260 unsafe fn push_representative(state: *mut lua_State) {
261 unsafe {
262 crate::sys::lua_pushvector_lua_state_f32_f32_f32_f32(state, 0.0, 0.0, 0.0, 0.0);
263 }
264 }
265}
266
267impl TypeMetatable for bool {
268 unsafe fn push_representative(state: *mut lua_State) {
269 unsafe { crate::sys::lua_pushboolean(state, 0) }
270 }
271}
272
273impl TypeMetatable for f64 {
274 unsafe fn push_representative(state: *mut lua_State) {
275 unsafe { crate::sys::lua_pushnumber(state, 0.0) }
276 }
277}
278
279impl TypeMetatable for crate::string::LuaString {
280 unsafe fn push_representative(state: *mut lua_State) {
281 unsafe {
282 let s = c"";
283 crate::sys::lua_pushlstring(state, s.as_ptr() as *const c_char, 0);
284 }
285 }
286}
287
288impl TypeMetatable for crate::function::Function {
289 unsafe fn push_representative(state: *mut lua_State) {
290 // Push a throwaway C function so `lua_setmetatable` targets the global
291 // function-type slot.
292 unsafe {
293 crate::sys::lua_pushcclosurek(state, Some(noop_cfn), c"".as_ptr(), 0, None);
294 }
295 }
296}
297
298impl TypeMetatable for crate::thread::Thread {
299 unsafe fn push_representative(state: *mut lua_State) {
300 // A fresh thread targets the global thread-type slot.
301 unsafe {
302 crate::sys::lua_newthread(state);
303 }
304 }
305}
306
307impl TypeMetatable for crate::light_userdata::LightUserData {
308 unsafe fn push_representative(state: *mut lua_State) {
309 crate::sys::lua_pushlightuserdatatagged(state, core::ptr::null_mut(), 0);
310 }
311}
312
313/// A do-nothing C function used as the representative value for the
314/// function-type metatable slot.
315unsafe fn noop_cfn(_state: *mut lua_State) -> c_int {
316 0
317}