From 87c10ca93dec7189330bdb0aad2daed5a1cd78fe Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Mon, 28 Mar 2022 23:42:35 +0100 Subject: [PATCH] Sandboxing support --- src/ffi/luau/lauxlib.rs | 28 +++++++++++++++ src/ffi/luau/lualib.rs | 4 --- src/lua.rs | 79 +++++++++++++++++++++++++++++++++++++++-- src/table.rs | 8 ++++- src/thread.rs | 60 ++++++++++++++++++++++++++++++- tests/luau.rs | 60 ++++++++++++++++++++++++++++++- 6 files changed, 230 insertions(+), 9 deletions(-) diff --git a/src/ffi/luau/lauxlib.rs b/src/ffi/luau/lauxlib.rs index 669a08f..af7d1c2 100644 --- a/src/ffi/luau/lauxlib.rs +++ b/src/ffi/luau/lauxlib.rs @@ -72,6 +72,11 @@ extern "C" { // TODO: luaL_findtable pub fn luaL_typename(L: *mut lua_State, idx: c_int) -> *const c_char; + + // sandbox libraries and globals + #[link_name = "luaL_sandbox"] + pub fn luaL_sandbox_(L: *mut lua_State); + pub fn luaL_sandboxthread(L: *mut lua_State); } // @@ -123,6 +128,29 @@ pub unsafe fn luaL_unref(L: *mut lua_State, t: c_int, r#ref: c_int) { lua::lua_unref(L, r#ref) } +pub unsafe fn luaL_sandbox(L: *mut lua_State, enabled: c_int) { + use super::lua::*; + + // set all libraries to read-only + lua_pushnil(L); + while lua_next(L, LUA_GLOBALSINDEX) != 0 { + if lua_istable(L, -1) != 0 { + lua_setreadonly(L, -1, enabled); + } + lua_pop(L, 1); + } + + // set all builtin metatables to read-only + lua_pushliteral(L, ""); + lua_getmetatable(L, -1); + lua_setreadonly(L, -1, enabled); + lua_pop(L, 2); + + // set globals to readonly and activate safeenv since the env is immutable + lua_setreadonly(L, LUA_GLOBALSINDEX, enabled); + lua_setsafeenv(L, LUA_GLOBALSINDEX, enabled); +} + // // TODO: Generic Buffer Manipulation // diff --git a/src/ffi/luau/lualib.rs b/src/ffi/luau/lualib.rs index 2f1ba7e..92b8642 100644 --- a/src/ffi/luau/lualib.rs +++ b/src/ffi/luau/lualib.rs @@ -26,8 +26,4 @@ extern "C" { // open all builtin libraries pub fn luaL_openlibs(L: *mut lua_State); - - // sandbox libraries and globals - pub fn luaL_sandbox(L: *mut lua_State); - pub fn luaL_sandboxthread(L: *mut lua_State); } diff --git a/src/lua.rs b/src/lua.rs index 9687e7a..9bfbebb 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -108,6 +108,9 @@ struct ExtraData { hook_callback: Option, #[cfg(feature = "lua54")] warn_callback: Option, + + #[cfg(feature = "luau")] + sandboxed: bool, } #[cfg_attr(any(feature = "lua51", feature = "luajit"), allow(dead_code))] @@ -548,6 +551,8 @@ impl Lua { hook_callback: None, #[cfg(feature = "lua54")] warn_callback: None, + #[cfg(feature = "luau")] + sandboxed: false, })); mlua_expect!( @@ -750,6 +755,60 @@ impl Lua { self.entrypoint(move |lua, _: ()| func(lua)) } + /// Enables (or disables) sandbox mode on this Lua instance. + /// + /// This method, in particular: + /// - Set all libraries to read-only + /// - Set all builtin metatables to read-only + /// - Set globals to read-only (and activates safeenv) + /// - Setup local environment table that performs writes locally and proxies reads + /// to the global environment. + /// + /// # Examples + /// + /// ``` + /// # use mlua::{Lua, Result}; + /// # fn main() -> Result<()> { + /// let lua = Lua::new(); + /// + /// lua.sandbox(true)?; + /// lua.load("var = 123").exec()?; + /// assert_eq!(lua.globals().get::<_, u32>("var")?, 123); + /// + /// // Restore the global environment (clear changes made in sandbox) + /// lua.sandbox(false)?; + /// assert_eq!(lua.globals().get::<_, Option>("var")?, None); + /// # Ok(()) + /// # } + /// ``` + /// + /// Requires `feature = "luau"` + #[cfg(feature = "luau")] + pub fn sandbox(&self, enabled: bool) -> Result<()> { + unsafe { + let extra = &mut *self.extra.get(); + if extra.sandboxed != enabled { + let state = self.main_state.ok_or(Error::MainThreadNotAvailable)?; + check_stack(state, 3)?; + protect_lua!(state, 0, 0, |state| { + if enabled { + ffi::luaL_sandbox(state, 1); + ffi::luaL_sandboxthread(state); + } else { + // Restore original `LUA_GLOBALSINDEX` + self.ref_thread_exec(|ref_thread| { + ffi::lua_xpush(ref_thread, state, ffi::LUA_GLOBALSINDEX); + ffi::lua_replace(state, ffi::LUA_GLOBALSINDEX); + }); + ffi::luaL_sandbox(state, 0); + } + })?; + extra.sandboxed = enabled; + } + Ok(()) + } + } + /// Sets a 'hook' function that will periodically be called as Lua code executes. /// /// When exactly the hook function is called depends on the contents of the `triggers` @@ -1424,7 +1483,11 @@ impl Lua { &'lua self, func: Function<'lua>, ) -> Result> { - #[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))] + #[cfg(any( + feature = "lua54", + all(feature = "luajit", feature = "vendored"), + feature = "luau", + ))] unsafe { let _sg = StackGuard::new(self.state); check_stack(self.state, 1)?; @@ -1434,6 +1497,14 @@ impl Lua { let thread_state = ffi::lua_tothread(extra.ref_thread, index); self.push_ref(&func.0); ffi::lua_xmove(self.state, thread_state, 1); + + #[cfg(feature = "luau")] + { + // Inherit `LUA_GLOBALSINDEX` from the caller + ffi::lua_xpush(self.state, thread_state, ffi::LUA_GLOBALSINDEX); + ffi::lua_replace(thread_state, ffi::LUA_GLOBALSINDEX); + } + return Ok(Thread(LuaRef { lua: self, index })); } }; @@ -1442,7 +1513,11 @@ impl Lua { /// Resets thread (coroutine) and returns to the cache for later use. #[cfg(feature = "async")] - #[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))] + #[cfg(any( + feature = "lua54", + all(feature = "luajit", feature = "vendored"), + feature = "luau", + ))] pub(crate) unsafe fn recycle_thread(&self, thread: &mut Thread) { let extra = &mut *self.extra.get(); let thread_state = ffi::lua_tothread(extra.ref_thread, thread.0.index); diff --git a/src/table.rs b/src/table.rs index 4be837c..cb1e85c 100644 --- a/src/table.rs +++ b/src/table.rs @@ -356,7 +356,13 @@ impl<'lua> Table<'lua> { pub fn set_readonly(&self, enabled: bool) { let lua = self.0.lua; unsafe { - lua.ref_thread_exec(|refthr| ffi::lua_setreadonly(refthr, self.0.index, enabled as _)); + lua.ref_thread_exec(|refthr| { + ffi::lua_setreadonly(refthr, self.0.index, enabled as _); + if !enabled { + // Reset "safeenv" flag + ffi::lua_setsafeenv(refthr, self.0.index, 0); + } + }); } } diff --git a/src/thread.rs b/src/thread.rs index 0be0664..d710c92 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -214,6 +214,13 @@ impl<'lua> Thread<'lua> { lua.push_ref(&func.0); ffi::lua_xmove(lua.state, thread_state, 1); + #[cfg(feature = "luau")] + { + // Inherit `LUA_GLOBALSINDEX` from the caller + ffi::lua_xpush(lua.state, thread_state, ffi::LUA_GLOBALSINDEX); + ffi::lua_replace(thread_state, ffi::LUA_GLOBALSINDEX); + } + Ok(()) } } @@ -278,6 +285,53 @@ impl<'lua> Thread<'lua> { recycle: false, } } + + /// Enables sandbox mode on this thread. + /// + /// Under the hood replaces the global environment table with a new table, + /// that performs writes locally and proxies reads to caller's global environment. + /// + /// This mode ideally should be used together with the global sandbox mode [`Lua::sandbox()`]. + /// + /// Please note that Luau links environment table with chunk when loading it into Lua state. + /// Therefore you need to load chunks into a thread to link with the thread environment. + /// + /// # Examples + /// + /// ``` + /// # use mlua::{Lua, Result}; + /// # fn main() -> Result<()> { + /// let lua = Lua::new(); + /// let thread = lua.create_thread(lua.create_function(|lua2, ()| { + /// lua2.load("var = 123").exec()?; + /// assert_eq!(lua2.globals().get::<_, u32>("var")?, 123); + /// Ok(()) + /// })?)?; + /// thread.sandbox()?; + /// thread.resume(())?; + /// + /// // The global environment should be unchanged + /// assert_eq!(lua.globals().get::<_, Option>("var")?, None); + /// # Ok(()) + /// # } + /// ``` + /// + /// Requires `feature = "luau"` + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[doc(hidden)] + pub fn sandbox(&self) -> Result<()> { + let lua = self.0.lua; + unsafe { + let thread = lua.ref_thread_exec(|t| ffi::lua_tothread(t, self.0.index)); + check_stack(thread, 1)?; + check_stack(lua.state, 3)?; + // Inherit `LUA_GLOBALSINDEX` from the caller + ffi::lua_xpush(lua.state, thread, ffi::LUA_GLOBALSINDEX); + ffi::lua_replace(thread, ffi::LUA_GLOBALSINDEX); + protect_lua!(lua.state, 0, 0, |_| ffi::luaL_sandboxthread(thread)) + } + } } impl<'lua> PartialEq for Thread<'lua> { @@ -295,7 +349,11 @@ impl<'lua, R> AsyncThread<'lua, R> { } #[cfg(feature = "async")] -#[cfg(any(feature = "lua54", all(feature = "luajit", feature = "vendored")))] +#[cfg(any( + feature = "lua54", + all(feature = "luajit", feature = "vendored"), + feature = "luau", +))] impl<'lua, R> Drop for AsyncThread<'lua, R> { fn drop(&mut self) { if self.recycle { diff --git a/tests/luau.rs b/tests/luau.rs index 5621174..ac6c290 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -3,7 +3,7 @@ use std::env; use std::fs; -use mlua::{Error, Lua, Result, Value}; +use mlua::{Error, Lua, Result, Table, Value}; #[test] fn test_require() -> Result<()> { @@ -67,3 +67,61 @@ fn test_readonly_table() -> Result<()> { Ok(()) } + +#[test] +fn test_sandbox() -> Result<()> { + let lua = Lua::new(); + + lua.sandbox(true)?; + + lua.load("global = 123").exec()?; + let n: i32 = lua.load("return global").eval()?; + assert_eq!(n, 123); + assert_eq!(lua.globals().get::<_, Option>("global")?, Some(123)); + + // Threads should inherit "main" globals + let f = lua.create_function(|lua, ()| lua.globals().get::<_, i32>("global"))?; + let co = lua.create_thread(f.clone())?; + assert_eq!(co.resume::<_, Option>(())?, Some(123)); + + // Sandboxed threads should also inherit "main" globals + let co = lua.create_thread(f)?; + co.sandbox()?; + assert_eq!(co.resume::<_, Option>(())?, Some(123)); + + lua.sandbox(false)?; + + // Previously set variable `global` should be cleared now + assert_eq!(lua.globals().get::<_, Option>("global")?, None); + + // Readonly flags should be cleared as well + let table = lua.globals().get::<_, Table>("table")?; + table.set("test", "test")?; + + Ok(()) +} + +#[test] +fn test_sandbox_threads() -> Result<()> { + let lua = Lua::new(); + + let f = lua.create_function(|lua, v: Value| lua.globals().set("global", v))?; + + let co = lua.create_thread(f.clone())?; + co.resume(321)?; + // The main state should see the `global` variable (as the thread is not sandboxed) + assert_eq!(lua.globals().get::<_, Option>("global")?, Some(321)); + + let co = lua.create_thread(f.clone())?; + co.sandbox()?; + co.resume(123)?; + // The main state should see the previous `global` value (as the thread is sandboxed) + assert_eq!(lua.globals().get::<_, Option>("global")?, Some(321)); + + // Try to reset the (sandboxed) thread + co.reset(f)?; + co.resume(111)?; + assert_eq!(lua.globals().get::<_, Option>("global")?, Some(111)); + + Ok(()) +}