diff --git a/src/error.rs b/src/error.rs index 7813fd7..4a9a6ef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,6 +36,8 @@ pub enum Error { /// The Lua VM returns this error when there is an error running a `__gc` metamethod. #[cfg(any(feature = "lua53", feature = "lua52"))] GarbageCollectorError(StdString), + /// Potentially unsafe action in safe mode. + SafetyError(StdString), /// Setting memory limit is not available. /// /// This error can only happen when Lua state was not created by us and does not have the @@ -153,6 +155,9 @@ impl fmt::Display for Error { Error::GarbageCollectorError(ref msg) => { write!(fmt, "garbage collector error: {}", msg) } + Error::SafetyError(ref msg) => { + write!(fmt, "safety error: {}", msg) + }, Error::MemoryLimitNotAvailable => { write!(fmt, "setting memory limit is not available") } diff --git a/src/lua.rs b/src/lua.rs index 3bb9fbd..5870353 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -43,6 +43,7 @@ pub struct Lua { main_state: *mut ffi::lua_State, extra: Arc>, ephemeral: bool, + safe: bool, // Lua has lots of interior mutability, should not be RefUnwindSafe _no_ref_unwind_safe: PhantomData>, } @@ -108,17 +109,63 @@ impl Drop for Lua { } impl Lua { - /// Creates a new Lua state and loads standard library without the `debug` library. + /// Creates a new Lua state and loads the safe subset of the standard libraries. + /// + /// The created Lua state would have safety guarantees and would not allow to load unsafe + /// standard libraries or C modules. pub fn new() -> Lua { - Self::new_with(StdLib::ALL_NO_DEBUG) + mlua_expect!( + Self::new_with(StdLib::ALL_SAFE), + "can't create new safe Lua state" + ) } - /// Creates a new Lua state and loads the specified set of standard libraries. + /// Creates a new Lua state and loads all the standard libraries. + /// + /// The created Lua state would not have safety guarantees and would allow to load C modules. + pub unsafe fn unsafe_new() -> Lua { + Self::unsafe_new_with(StdLib::ALL) + } + + /// Creates a new Lua state and loads the specified safe subset of the standard libraries. /// /// Use the [`StdLib`] flags to specifiy the libraries you want to load. /// + /// The created Lua state would have safety guarantees and would not allow to load unsafe + /// standard libraries or C modules. + /// /// [`StdLib`]: struct.StdLib.html - pub fn new_with(libs: StdLib) -> Lua { + pub fn new_with(libs: StdLib) -> Result { + if libs.contains(StdLib::DEBUG) { + return Err(Error::SafetyError( + "the unsafe `debug` module can't be loaded using safe `new_with`".to_string(), + )); + } + #[cfg(feature = "luajit")] + { + if libs.contains(StdLib::FFI) { + return Err(Error::SafetyError( + "the unsafe `ffi` module can't be loaded using safe `new_with`".to_string(), + )); + } + } + + let mut lua = unsafe { Self::unsafe_new_with(libs) }; + + mlua_expect!(lua.disable_c_modules(), "Error during disabling C modules"); + lua.safe = true; + + Ok(lua) + } + + /// Creates a new Lua state and loads the specified subset of the standard libraries. + /// + /// Use the [`StdLib`] flags to specifiy the libraries you want to load. + /// + /// The created Lua state would not have safety guarantees and would allow to load C modules. + /// + /// [`StdLib`]: struct.StdLib.html + pub unsafe fn unsafe_new_with(libs: StdLib) -> Lua { unsafe extern "C" fn allocator( extra_data: *mut c_void, ptr: *mut c_void, @@ -173,67 +220,28 @@ impl Lua { new_ptr } - unsafe { - let mem_info = Box::into_raw(Box::new(MemoryInfo { - used_memory: 0, - memory_limit: 0, - })); + let mem_info = Box::into_raw(Box::new(MemoryInfo { + used_memory: 0, + memory_limit: 0, + })); - let state = ffi::lua_newstate(allocator, mem_info as *mut c_void); + let state = ffi::lua_newstate(allocator, mem_info as *mut c_void); - ffi::luaL_requiref(state, cstr!("_G"), ffi::luaopen_base, 1); - ffi::lua_pop(state, 1); + ffi::luaL_requiref(state, cstr!("_G"), ffi::luaopen_base, 1); + ffi::lua_pop(state, 1); - let mut lua = Lua::init_from_ptr(state); - lua.ephemeral = false; - lua.extra.lock().unwrap().mem_info = mem_info; + let mut lua = Lua::init_from_ptr(state); + lua.ephemeral = false; + lua.extra.lock().unwrap().mem_info = mem_info; - mlua_expect!( - protect_lua_closure(lua.main_state, 0, 0, |state| { - load_from_std_lib(state, libs); - }), - "Error during loading standard libraries" - ); - - lua - } - } - - /// Consumes and leaks `Lua` object, returning a static reference `&'static Lua`. - /// - /// This function is useful when the `Lua` object is supposed to live for the remainder - /// of the program's life. - /// In particular in asynchronous context this will allow to spawn Lua tasks to execute - /// in background. - /// - /// Dropping the returned reference will cause a memory leak. If this is not acceptable, - /// the reference should first be wrapped with the [`Lua::from_static`] function producing a `Lua`. - /// This `Lua` object can then be dropped which will properly release the allocated memory. - /// - /// [`Lua::from_static`]: #method.from_static - pub fn into_static(self) -> &'static Self { - Box::leak(Box::new(self)) - } - - /// Constructs a `Lua` from a static reference to it. - /// - /// # Safety - /// This function is unsafe because improper use may lead to memory problems or undefined behavior. - pub unsafe fn from_static(lua: &'static Lua) -> Self { - *Box::from_raw(lua as *const Lua as *mut Lua) - } - - /// Loads the specified set of standard libraries into an existing Lua state. - /// - /// Use the [`StdLib`] flags to specifiy the libraries you want to load. - /// - /// [`StdLib`]: struct.StdLib.html - pub fn load_from_std_lib(&self, libs: StdLib) -> Result<()> { - unsafe { - protect_lua_closure(self.main_state, 0, 0, |state| { + mlua_expect!( + protect_lua_closure(lua.main_state, 0, 0, |state| { load_from_std_lib(state, libs); - }) - } + }), + "Error during loading standard libraries" + ); + + lua } /// Constructs a new Lua instance from the existing state. @@ -292,10 +300,62 @@ impl Lua { main_state: main_state, extra: extra, ephemeral: true, + safe: false, _no_ref_unwind_safe: PhantomData, } } + /// Loads the specified subset of the standard libraries into an existing Lua state. + /// + /// Use the [`StdLib`] flags to specifiy the libraries you want to load. + /// + /// [`StdLib`]: struct.StdLib.html + pub fn load_from_std_lib(&self, libs: StdLib) -> Result<()> { + if self.safe && libs.contains(StdLib::DEBUG) { + return Err(Error::SafetyError( + "the unsafe `debug` module can't be loaded in safe mode".to_string(), + )); + } + #[cfg(feature = "luajit")] + { + if self.safe && libs.contains(StdLib::FFI) { + return Err(Error::SafetyError( + "the unsafe `ffi` module can't be loaded in safe mode".to_string(), + )); + } + } + + unsafe { + protect_lua_closure(self.main_state, 0, 0, |state| { + load_from_std_lib(state, libs); + }) + } + } + + /// Consumes and leaks `Lua` object, returning a static reference `&'static Lua`. + /// + /// This function is useful when the `Lua` object is supposed to live for the remainder + /// of the program's life. + /// In particular in asynchronous context this will allow to spawn Lua tasks to execute + /// in background. + /// + /// Dropping the returned reference will cause a memory leak. If this is not acceptable, + /// the reference should first be wrapped with the [`Lua::from_static`] function producing a `Lua`. + /// This `Lua` object can then be dropped which will properly release the allocated memory. + /// + /// [`Lua::from_static`]: #method.from_static + pub fn into_static(self) -> &'static Self { + Box::leak(Box::new(self)) + } + + /// Constructs a `Lua` from a static reference to it. + /// + /// # Safety + /// This function is unsafe because improper use may lead to memory problems or undefined behavior. + pub unsafe fn from_static(lua: &'static Lua) -> Self { + *Box::from_raw(lua as *const Lua as *mut Lua) + } + // Executes module entrypoint function, which returns only one Value. // The returned value then pushed to the Lua stack. #[doc(hidden)] @@ -1556,9 +1616,36 @@ impl Lua { main_state: self.main_state, extra: self.extra.clone(), ephemeral: true, + safe: self.safe, _no_ref_unwind_safe: PhantomData, } } + + fn disable_c_modules(&self) -> Result<()> { + let package: Table = self.globals().get("package")?; + + package.set( + "loadlib", + self.create_function(|_, ()| -> Result<()> { + Err(Error::SafetyError( + "package.loadlib is disabled in safe mode".to_string(), + )) + })?, + )?; + + #[cfg(any(feature = "lua54", feature = "lua53", feature = "lua52"))] + let searchers: Table = package.get("searchers")?; + #[cfg(any(feature = "lua51", feature = "luajit"))] + let searchers: Table = package.get("loaders")?; + + let loader = self.create_function(|_, ()| Ok("\n\tcan't load C modules in safe mode"))?; + + // The third and fourth searchers looks for a loader as a C library + searchers.raw_set(3, loader.clone())?; + searchers.raw_remove(4)?; + + Ok(()) + } } /// Returned from [`Lua::load`] and is used to finalize loading and executing Lua main chunks. diff --git a/src/stdlib.rs b/src/stdlib.rs index 46b980a..ee4aeed 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -20,12 +20,15 @@ impl StdLib { pub const PACKAGE: StdLib = StdLib(1 << 8); #[cfg(feature = "luajit")] pub const JIT: StdLib = StdLib(1 << 9); + + /// `ffi` (unsafe) module `feature = "luajit"` #[cfg(feature = "luajit")] - pub const FFI: StdLib = StdLib(1 << 10); - pub const DEBUG: StdLib = StdLib(1 << 31); // always highest bit + pub const FFI: StdLib = StdLib(1 << 30); + /// `debug` (unsafe) module + pub const DEBUG: StdLib = StdLib(1 << 31); pub const ALL: StdLib = StdLib(u32::MAX); - pub const ALL_NO_DEBUG: StdLib = StdLib((1 << 31) - 1); + pub const ALL_SAFE: StdLib = StdLib((1 << 30) - 1); pub fn contains(self, lib: Self) -> bool { (self & lib).0 != 0 diff --git a/tests/tests.rs b/tests/tests.rs index d9d182b..bb202e3 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -4,9 +4,49 @@ use std::sync::Arc; use std::{error, f32, f64, fmt}; use mlua::{ - Error, ExternalError, Function, Lua, Nil, Result, String, Table, UserData, Value, Variadic, + Error, ExternalError, Function, Lua, Nil, Result, StdLib, String, Table, UserData, Value, + Variadic, }; +#[test] +fn test_safety() -> Result<()> { + let lua = Lua::new(); + assert!(lua.load(r#"require "debug""#).exec().is_err()); + match lua.load_from_std_lib(StdLib::DEBUG) { + Err(Error::SafetyError(_)) => {} + Err(e) => panic!("expected SafetyError, got {:?}", e), + Ok(_) => panic!("expected SafetyError, got no error"), + } + drop(lua); + + let lua = unsafe { Lua::unsafe_new() }; + assert!(lua.load(r#"require "debug""#).exec().is_ok()); + drop(lua); + + match Lua::new_with(StdLib::DEBUG) { + Err(Error::SafetyError(_)) => {} + Err(e) => panic!("expected SafetyError, got {:?}", e), + Ok(_) => panic!("expected SafetyError, got new Lua state"), + } + + let lua = Lua::new(); + match lua.load(r#"package.loadlib()"#).exec() { + Err(Error::CallbackError { ref cause, .. }) => match cause.as_ref() { + Error::SafetyError(_) => {} + e => panic!("expected SafetyError cause, got {:?}", e), + }, + Err(e) => panic!("expected CallbackError, got {:?}", e), + Ok(_) => panic!("expected CallbackError, got no error"), + }; + match lua.load(r#"require "fake_ffi""#).exec() { + Err(Error::RuntimeError(msg)) => assert!(msg.contains("can't load C modules in safe mode")), + Err(e) => panic!("expected RuntimeError, got {:?}", e), + Ok(_) => panic!("expected RuntimeError, got no error"), + } + + Ok(()) +} + #[test] fn test_load() -> Result<()> { let lua = Lua::new();