Skip to content

Commit 9c34d4b

Browse files
committed
Fix soundness problems with rlua
setmetatable now wraps a __gc method in a cclosure that aborts on error, also 'debug' library is no longer provided. We could provide just the subset of the debug library that is sound, though.
1 parent 820c38e commit 9c34d4b

File tree

4 files changed

+111
-16
lines changed

4 files changed

+111
-16
lines changed

src/ffi.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,26 @@ extern "C" {
121121
pub fn lua_error(state: *mut lua_State) -> !;
122122
pub fn lua_atpanic(state: *mut lua_State, panic: lua_CFunction) -> lua_CFunction;
123123

124+
pub fn luaopen_base(state: *mut lua_State) -> c_int;
125+
pub fn luaopen_coroutine(state: *mut lua_State) -> c_int;
126+
pub fn luaopen_table(state: *mut lua_State) -> c_int;
127+
pub fn luaopen_io(state: *mut lua_State) -> c_int;
128+
pub fn luaopen_os(state: *mut lua_State) -> c_int;
129+
pub fn luaopen_string(state: *mut lua_State) -> c_int;
130+
pub fn luaopen_utf8(state: *mut lua_State) -> c_int;
131+
pub fn luaopen_math(state: *mut lua_State) -> c_int;
132+
pub fn luaopen_debug(state: *mut lua_State) -> c_int;
133+
pub fn luaopen_package(state: *mut lua_State) -> c_int;
134+
124135
pub fn luaL_newstate() -> *mut lua_State;
125136
pub fn luaL_openlibs(state: *mut lua_State);
137+
pub fn luaL_requiref(
138+
state: *mut lua_State,
139+
modname: *const c_char,
140+
openf: lua_CFunction,
141+
glb: c_int,
142+
);
143+
126144
pub fn luaL_loadbufferx(
127145
state: *mut lua_State,
128146
buf: *const c_char,

src/lua.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,7 +1302,18 @@ impl Lua {
13021302
let state = ffi::luaL_newstate();
13031303

13041304
stack_guard(state, 0, || {
1305-
ffi::luaL_openlibs(state);
1305+
// Do not open the debug library, currently it can be used to cause unsafety.
1306+
ffi::luaL_requiref(state, cstr!("_G"), ffi::luaopen_base, 1);
1307+
ffi::luaL_requiref(state, cstr!("base"), ffi::luaopen_base, 1);
1308+
ffi::luaL_requiref(state, cstr!("coroutine"), ffi::luaopen_coroutine, 1);
1309+
ffi::luaL_requiref(state, cstr!("table"), ffi::luaopen_table, 1);
1310+
ffi::luaL_requiref(state, cstr!("io"), ffi::luaopen_io, 1);
1311+
ffi::luaL_requiref(state, cstr!("os"), ffi::luaopen_os, 1);
1312+
ffi::luaL_requiref(state, cstr!("string"), ffi::luaopen_string, 1);
1313+
ffi::luaL_requiref(state, cstr!("utf8"), ffi::luaopen_utf8, 1);
1314+
ffi::luaL_requiref(state, cstr!("math"), ffi::luaopen_math, 1);
1315+
ffi::luaL_requiref(state, cstr!("package"), ffi::luaopen_package, 1);
1316+
ffi::lua_pop(state, 10);
13061317

13071318
// Create the userdata registry table
13081319

@@ -1348,7 +1359,8 @@ impl Lua {
13481359

13491360
ffi::lua_rawset(state, ffi::LUA_REGISTRYINDEX);
13501361

1351-
// Override pcall / xpcall
1362+
// Override pcall, xpcall, and setmetatable with versions that cannot be used to
1363+
// cause unsafety.
13521364

13531365
ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_RIDX_GLOBALS);
13541366

@@ -1360,6 +1372,10 @@ impl Lua {
13601372
ffi::lua_pushcfunction(state, safe_xpcall);
13611373
ffi::lua_rawset(state, -3);
13621374

1375+
push_string(state, "setmetatable");
1376+
ffi::lua_pushcfunction(state, safe_setmetatable);
1377+
ffi::lua_rawset(state, -3);
1378+
13631379
ffi::lua_pop(state, 1);
13641380
});
13651381

src/tests.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,33 @@ fn test_pcall_xpcall() {
919919
);
920920
}
921921

922+
#[test]
923+
fn test_setmetatable_gc() {
924+
let lua = Lua::new();
925+
lua.exec::<()>(
926+
r#"
927+
val = nil
928+
table = {}
929+
setmetatable(table, {
930+
__gc = function()
931+
val = "gcwascalled"
932+
end
933+
})
934+
table_badgc = {}
935+
setmetatable(table_badgc, {
936+
__gc = "what's a gc"
937+
})
938+
table = nil
939+
table_badgc = nil
940+
collectgarbage("collect")
941+
"#,
942+
None,
943+
).unwrap();
944+
945+
let globals = lua.globals();
946+
assert_eq!(globals.get::<_, String>("val").unwrap(), "gcwascalled");
947+
}
948+
922949
// Need to use compiletest-rs or similar to make sure these don't compile.
923950
/*
924951
#[test]

src/util.rs

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -290,22 +290,19 @@ pub unsafe fn handle_error(state: *mut ffi::lua_State, err: c_int) -> Result<()>
290290
}
291291
ffi::LUA_ERRERR => Error::ErrorError(err_string),
292292
ffi::LUA_ERRMEM => {
293-
// This is not impossible to hit, but this library is not set up
294-
// to handle this properly. Lua does a longjmp on out of memory
295-
// (like all lua errors), but it can do this from a huge number
296-
// of lua functions, and it is extremely difficult to set up the
297-
// pcall protection for every lua function that might allocate.
298-
// If lua does this in an unprotected context, it will abort
299-
// anyway, so the best we can do right now is guarantee an abort
300-
// even in a protected context.
301-
println!("Lua memory error, aborting!");
293+
// TODO: We should provide lua with custom allocators that guarantee aborting on
294+
// OOM, instead of relying on the system allocator. Currently, this is a
295+
// theoretical soundness bug, because an OOM could trigger a longjmp out of
296+
// arbitrary rust code that is unprotectedly calling a lua function marked as
297+
// potentially causing memory errors, which is most of them.
298+
eprintln!("Lua memory error, aborting!");
302299
process::abort()
303300
}
304301
ffi::LUA_ERRGCMM => {
305-
// This should be impossible, or at least is indicative of an
306-
// internal bug. Similarly to LUA_ERRMEM, this could indicate a
307-
// longjmp out of rust code, so we just abort.
308-
println!("Lua error during __gc, aborting!");
302+
// This should be impossible, since we wrap setmetatable to protect __gc
303+
// metamethods, but if we do end up here then the same logic as setmetatable
304+
// applies and we must abort.
305+
eprintln!("Lua error during __gc, aborting!");
309306
process::abort()
310307
}
311308
_ => lua_panic!(state, "internal error: unrecognized lua error code"),
@@ -489,7 +486,44 @@ pub unsafe extern "C" fn safe_xpcall(state: *mut ffi::lua_State) -> c_int {
489486
}
490487
}
491488

492-
/// Does not call checkstack, uses 1 stack space
489+
// Safely call setmetatable, if a __gc function is given, will wrap it in pcall, and panic on error.
490+
pub unsafe extern "C" fn safe_setmetatable(state: *mut ffi::lua_State) -> c_int {
491+
if ffi::lua_gettop(state) < 2 {
492+
push_string(state, "not enough arguments to setmetatable");
493+
ffi::lua_error(state);
494+
}
495+
496+
// Wrapping the __gc method in setmetatable ONLY works because Lua 5.3 only honors the __gc
497+
// method when it exists upon calling setmetatable, and ignores it if it is set later.
498+
push_string(state, "__gc");
499+
if ffi::lua_rawget(state, -2) == ffi::LUA_TFUNCTION {
500+
unsafe extern "C" fn safe_gc(state: *mut ffi::lua_State) -> c_int {
501+
ffi::lua_pushvalue(state, ffi::lua_upvalueindex(1));
502+
ffi::lua_insert(state, 1);
503+
if ffi::lua_pcall(state, 1, 0, 0) != ffi::LUA_OK {
504+
// If a user supplied __gc metamethod causes an error, we must always abort. We may
505+
// be inside a protected context due to being in a callback, but inside an
506+
// unprotected ffi call that can cause memory errors, so may be at risk of
507+
// longjmping over arbitrary rust.
508+
eprintln!("Lua error during __gc, aborting!");
509+
process::abort()
510+
} else {
511+
ffi::lua_gettop(state)
512+
}
513+
}
514+
515+
ffi::lua_pushcclosure(state, safe_gc, 1);
516+
push_string(state, "__gc");
517+
ffi::lua_insert(state, -2);
518+
ffi::lua_rawset(state, -3);
519+
} else {
520+
ffi::lua_pop(state, 1);
521+
}
522+
ffi::lua_setmetatable(state, -2);
523+
0
524+
}
525+
526+
// Does not call checkstack, uses 1 stack space
493527
pub unsafe fn main_state(state: *mut ffi::lua_State) -> *mut ffi::lua_State {
494528
ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_RIDX_MAINTHREAD);
495529
let main_state = ffi::lua_tothread(state, -1);

0 commit comments

Comments
 (0)