Provide safe and unsafe Lua modes:

- In safe mode Lua would not have ability to load C code via `require` or `package.loadlib`
- Unsafe mode allows everything.
This commit is contained in:
Alex Orlenko 2020-05-10 16:56:19 +01:00
parent 1b2b94c808
commit 5a9a308790
4 changed files with 199 additions and 64 deletions

View File

@ -36,6 +36,8 @@ pub enum Error {
/// The Lua VM returns this error when there is an error running a `__gc` metamethod. /// The Lua VM returns this error when there is an error running a `__gc` metamethod.
#[cfg(any(feature = "lua53", feature = "lua52"))] #[cfg(any(feature = "lua53", feature = "lua52"))]
GarbageCollectorError(StdString), GarbageCollectorError(StdString),
/// Potentially unsafe action in safe mode.
SafetyError(StdString),
/// Setting memory limit is not available. /// Setting memory limit is not available.
/// ///
/// This error can only happen when Lua state was not created by us and does not have the /// 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) => { Error::GarbageCollectorError(ref msg) => {
write!(fmt, "garbage collector error: {}", msg) write!(fmt, "garbage collector error: {}", msg)
} }
Error::SafetyError(ref msg) => {
write!(fmt, "safety error: {}", msg)
},
Error::MemoryLimitNotAvailable => { Error::MemoryLimitNotAvailable => {
write!(fmt, "setting memory limit is not available") write!(fmt, "setting memory limit is not available")
} }

View File

@ -43,6 +43,7 @@ pub struct Lua {
main_state: *mut ffi::lua_State, main_state: *mut ffi::lua_State,
extra: Arc<Mutex<ExtraData>>, extra: Arc<Mutex<ExtraData>>,
ephemeral: bool, ephemeral: bool,
safe: bool,
// Lua has lots of interior mutability, should not be RefUnwindSafe // Lua has lots of interior mutability, should not be RefUnwindSafe
_no_ref_unwind_safe: PhantomData<UnsafeCell<()>>, _no_ref_unwind_safe: PhantomData<UnsafeCell<()>>,
} }
@ -108,17 +109,63 @@ impl Drop for Lua {
} }
impl 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 { 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. /// 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 /// [`StdLib`]: struct.StdLib.html
pub fn new_with(libs: StdLib) -> Lua { pub fn new_with(libs: StdLib) -> Result<Lua> {
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( unsafe extern "C" fn allocator(
extra_data: *mut c_void, extra_data: *mut c_void,
ptr: *mut c_void, ptr: *mut c_void,
@ -173,7 +220,6 @@ impl Lua {
new_ptr new_ptr
} }
unsafe {
let mem_info = Box::into_raw(Box::new(MemoryInfo { let mem_info = Box::into_raw(Box::new(MemoryInfo {
used_memory: 0, used_memory: 0,
memory_limit: 0, memory_limit: 0,
@ -197,44 +243,6 @@ impl Lua {
lua 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| {
load_from_std_lib(state, libs);
})
}
}
/// Constructs a new Lua instance from the existing state. /// Constructs a new Lua instance from the existing state.
pub unsafe fn init_from_ptr(state: *mut ffi::lua_State) -> Lua { pub unsafe fn init_from_ptr(state: *mut ffi::lua_State) -> Lua {
@ -292,10 +300,62 @@ impl Lua {
main_state: main_state, main_state: main_state,
extra: extra, extra: extra,
ephemeral: true, ephemeral: true,
safe: false,
_no_ref_unwind_safe: PhantomData, _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. // Executes module entrypoint function, which returns only one Value.
// The returned value then pushed to the Lua stack. // The returned value then pushed to the Lua stack.
#[doc(hidden)] #[doc(hidden)]
@ -1556,9 +1616,36 @@ impl Lua {
main_state: self.main_state, main_state: self.main_state,
extra: self.extra.clone(), extra: self.extra.clone(),
ephemeral: true, ephemeral: true,
safe: self.safe,
_no_ref_unwind_safe: PhantomData, _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. /// Returned from [`Lua::load`] and is used to finalize loading and executing Lua main chunks.

View File

@ -20,12 +20,15 @@ impl StdLib {
pub const PACKAGE: StdLib = StdLib(1 << 8); pub const PACKAGE: StdLib = StdLib(1 << 8);
#[cfg(feature = "luajit")] #[cfg(feature = "luajit")]
pub const JIT: StdLib = StdLib(1 << 9); pub const JIT: StdLib = StdLib(1 << 9);
/// `ffi` (unsafe) module `feature = "luajit"`
#[cfg(feature = "luajit")] #[cfg(feature = "luajit")]
pub const FFI: StdLib = StdLib(1 << 10); pub const FFI: StdLib = StdLib(1 << 30);
pub const DEBUG: StdLib = StdLib(1 << 31); // always highest bit /// `debug` (unsafe) module
pub const DEBUG: StdLib = StdLib(1 << 31);
pub const ALL: StdLib = StdLib(u32::MAX); 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 { pub fn contains(self, lib: Self) -> bool {
(self & lib).0 != 0 (self & lib).0 != 0

View File

@ -4,9 +4,49 @@ use std::sync::Arc;
use std::{error, f32, f64, fmt}; use std::{error, f32, f64, fmt};
use mlua::{ 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] #[test]
fn test_load() -> Result<()> { fn test_load() -> Result<()> {
let lua = Lua::new(); let lua = Lua::new();