https://www.mediawiki.org/wiki/Special:Code/MediaWiki/114624
Revision: 114624 Author: tstarling Date: 2012-03-30 03:30:56 +0000 (Fri, 30 Mar 2012) Log Message: ----------- * Implemented pcall() and xpcall() per Victor's request. Added tests. * Refactored timeout error generation, since it's a bit more complicated now * Used "static inline" instead of plain "inline" functions in headers per the recommendation at http://www.greenend.org.uk/rjk/tech/inline.html Modified Paths: -------------- trunk/php/luasandbox/data_conversion.c trunk/php/luasandbox/library.c trunk/php/luasandbox/luasandbox.c trunk/php/luasandbox/php_luasandbox.h trunk/php/luasandbox/timer.c Added Paths: ----------- trunk/php/luasandbox/tests/pcall.phpt trunk/php/luasandbox/tests/xpcall.phpt Modified: trunk/php/luasandbox/data_conversion.c =================================================================== --- trunk/php/luasandbox/data_conversion.c 2012-03-30 01:26:34 UTC (rev 114623) +++ trunk/php/luasandbox/data_conversion.c 2012-03-30 03:30:56 UTC (rev 114624) @@ -19,6 +19,13 @@ extern zend_class_entry *luasandboxfunction_ce; extern zend_class_entry *luasandboxplaceholder_ce; +/** + * An int, the address of which is used as a fatal error marker. The value is + * not used. + */ +int luasandbox_fatal_error_marker = 0; + + /** {{{ luasandbox_data_conversion_init * * Set up a lua_State so that this module can work with it. @@ -339,3 +346,79 @@ } /* }}} */ +/** {{{ luasandbox_wrap_fatal + * + * Pop a value off the top of the stack, and push a fatal error wrapper + * containing the value. + */ +void luasandbox_wrap_fatal(lua_State * L) +{ + // Create the table and put the marker in it as element 1 + lua_createtable(L, 0, 2); + lua_pushlightuserdata(L, &luasandbox_fatal_error_marker); + lua_rawseti(L, -2, 1); + + // Swap the table with the input value, so that the value is on the top, + // then put the value in the table as element 2 + lua_insert(L, -2); + lua_rawseti(L, -2, 2); +} +/* }}} */ + +/** {{{ luasandbox_is_fatal + * + * Check if the value at the given stack index is a fatal error wrapper + * created by luasandbox_wrap_fatal(). Return 1 if it is, 0 otherwise. + */ +int luasandbox_is_fatal(lua_State * L, int index) +{ + void * ud; + int haveIndex2 = 0; + + if (!lua_istable(L, index)) { + return 0; + } + + lua_rawgeti(L, index, 1); + ud = lua_touserdata(L, -1); + lua_pop(L, 1); + if (ud != &luasandbox_fatal_error_marker) { + return 0; + } + + lua_rawgeti(L, index, 2); + haveIndex2 = !lua_isnil(L, -1); + lua_pop(L, 1); + return haveIndex2; +} +/* }}} */ + +/** {{{ + * + * If the value at the given stack index is a fatal error wrapper, convert + * the error object it wraps to a string. If the value is anything else, + * convert it directly to a string. If the error object is not convertible + * to a string, return "unknown error". + * + * This calls lua_tolstring() and will corrupt the value on the stack as + * described in that function's documentation. The string is valid until the + * Lua value is destroyed. + */ +char * luasandbox_error_to_string(lua_State * L, int index) +{ + char * s; + if (luasandbox_is_fatal(L, index)) { + lua_rawgeti(L, index, 2); + s = lua_tostring(L, -1); + lua_pop(L, 1); + } else { + s = lua_tostring(L, index); + } + if (!s) { + return "unknown error"; + } else { + return s; + } +} +/* }}} */ + Modified: trunk/php/luasandbox/library.c =================================================================== --- trunk/php/luasandbox/library.c 2012-03-30 01:26:34 UTC (rev 114623) +++ trunk/php/luasandbox/library.c 2012-03-30 03:30:56 UTC (rev 114624) @@ -20,11 +20,13 @@ static int luasandbox_base_tostring(lua_State * L); static int luasandbox_math_random(lua_State * L); static int luasandbox_math_randomseed(lua_State * L); +static int luasandbox_base_pcall(lua_State * L); +static int luasandbox_base_xpcall (lua_State *L); /** * Allowed global variables. Omissions are: - * * pcall, xpcall: Changing the protected environment won't work with our - * current timeout method. + * * pcall, xpcall: We have our own versions which don't allow interception of + * timeout etc. errors. * * loadfile: insecure. * * load, loadstring: Probably creates a protected environment so has * the same problem as pcall. Also omitting these makes analysis of the @@ -32,7 +34,7 @@ * * print: Not compatible with a sandbox environment * * tostring: Provides addresses of tables and functions, which provides an * easy ASLR workaround or heap address discovery mechanism for a memory - * corruption exploit. + * corruption exploit. We have our own version. * * Any new or undocumented functions like newproxy. * * package: cpath, loadlib etc. are insecure. * * coroutine: Not useful for our application so unreviewed at present. @@ -104,10 +106,15 @@ } } - // Install our own version of tostring + // Install our own version of tostring, pcall, xpcall lua_pushcfunction(L, luasandbox_base_tostring); lua_setglobal(L, "tostring"); + lua_pushcfunction(L, luasandbox_base_pcall); + lua_setglobal(L, "pcall"); + lua_pushcfunction(L, luasandbox_base_xpcall); + lua_setglobal(L, "xpcall"); + // Remove string.dump: may expose private data lua_getglobal(L, "string"); lua_pushnil(L); @@ -235,3 +242,105 @@ } /* }}} */ +/** {{{ luasandbox_lib_rethrow_fatal + * + * If the error on the top of the stack with the error return code given as a + * parameter is a fatal, rethrow the error. If the error is rethrown, the + * function does not return. + */ +static void luasandbox_lib_rethrow_fatal(lua_State * L, int status) +{ + switch (status) { + case 0: + // No error + return; + case LUA_ERRRUN: + // A runtime error which we can rethrow in a normal way + if (luasandbox_is_fatal(L, -1)) { + lua_error(L); + } + break; + case LUA_ERRMEM: + case LUA_ERRERR: + // Lua doesn't provide a public API for rethrowing these, so we + // have to convert them to our own fatal error type + if (!luasandbox_is_fatal(L, -1)) { + luasandbox_wrap_fatal(L); + } + lua_error(L); + break; // not reached + } +} +/* }}} */ + +/** {{{ luasandbox_lib_error_wrapper + * + * Wrapper for the xpcall error function + */ +static int luasandbox_lib_error_wrapper(lua_State * L) +{ + int status; + luaL_checkany(L, 1); + + // This function is only called from luaG_errormsg(), which will later + // unconditionally set the status code to LUA_ERRRUN, so we can assume + // that the error type is equivalent to LUA_ERRRUN. + if (luasandbox_is_fatal(L, 1)) { + // Just return to whatever called lua_pcall(), don't call the user + // function + return lua_gettop(L); + } + + // Put the user error function at the bottom of the stack + lua_pushvalue(L, lua_upvalueindex(1)); + lua_insert(L, 1); + // Call it, passing through the arguments to this function + status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); + if (status != 0) { + luasandbox_lib_rethrow_fatal(L, LUA_ERRERR); + } + return lua_gettop(L); +} +/* }}} */ + +/** {{{ luasandbox_base_pcall + * + * This is our implementation of the Lua function pcall(). It allows Lua code + * to handle its own errors, but forces internal errors to be rethrown. + */ +static int luasandbox_base_pcall(lua_State * L) +{ + int status; + luaL_checkany(L, 1); + status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); + luasandbox_lib_rethrow_fatal(L, status); + lua_pushboolean(L, (status == 0)); + lua_insert(L, 1); + return lua_gettop(L); // return status + all results +} +/* }}} */ + +/** {{{ luasandbox_base_xpcall + * + * This is our implementation of the Lua function xpcall(). It allows Lua code + * to handle its own errors, but forces internal errors to be rethrown. + */ +static int luasandbox_base_xpcall (lua_State *L) +{ + int status; + luaL_checkany(L, 2); + lua_settop(L, 2); + + // We wrap the error function in a C closure. The error function already + // happens to be at the top of the stack, so we don't need to push it before + // calling lua_pushcfunction to make it an upvalue + lua_pushcclosure(L, luasandbox_lib_error_wrapper, 1); + lua_insert(L, 1); // put error function under function to be called + + status = lua_pcall(L, 0, LUA_MULTRET, 1); + luasandbox_lib_rethrow_fatal(L, status); + lua_pushboolean(L, (status == 0)); + lua_replace(L, 1); + return lua_gettop(L); // return status + all results +} +/* }}} */ Modified: trunk/php/luasandbox/luasandbox.c =================================================================== --- trunk/php/luasandbox/luasandbox.c 2012-03-30 01:26:34 UTC (rev 114623) +++ trunk/php/luasandbox/luasandbox.c 2012-03-30 03:30:56 UTC (rev 114624) @@ -44,8 +44,6 @@ static int luasandbox_call_php(lua_State * L); static int luasandbox_dump_writer(lua_State * L, const void * p, size_t sz, void * ud); -char luasandbox_timeout_message[] = "The maximum execution time for this script was exceeded"; - zend_class_entry *luasandbox_ce; zend_class_entry *luasandboxerror_ce; zend_class_entry *luasandboxemergencytimeout_ce; @@ -393,7 +391,7 @@ { php_error_docref(NULL TSRMLS_CC, E_ERROR, "PANIC: unprotected error in call to Lua API (%s)", - lua_tostring(L, -1)); + luasandbox_error_to_string(L, -1)); return 0; } /* }}} */ @@ -535,7 +533,7 @@ */ static void luasandbox_handle_error(lua_State * L, int status) { - const char * errorMsg = lua_tostring(L, -1); + const char * errorMsg = luasandbox_error_to_string(L, -1); lua_pop(L, 1); if (!EG(exception)) { zend_throw_exception(luasandboxerror_ce, (char*)errorMsg, status); @@ -1117,7 +1115,9 @@ // If an exception occurred, convert it to a Lua error (just to unwind the stack) if (EG(exception)) { - luaL_error(L, "[exception]"); + lua_pushstring(L, "[exception]"); + luasandbox_wrap_fatal(L); + lua_error(L); } return num_results; } Modified: trunk/php/luasandbox/php_luasandbox.h =================================================================== --- trunk/php/luasandbox/php_luasandbox.h 2012-03-30 01:26:34 UTC (rev 114623) +++ trunk/php/luasandbox/php_luasandbox.h 2012-03-30 03:30:56 UTC (rev 114624) @@ -22,7 +22,6 @@ /* luasandbox.c */ extern zend_module_entry luasandbox_module_entry; -extern char luasandbox_timeout_message[]; #define phpext_luasandbox_ptr &luasandbox_module_entry @@ -104,12 +103,12 @@ * unsafe to call longjmp() to return control to PHP. If the flag is not * correctly set, memory may be corrupted and security compromised. */ -inline void luasandbox_enter_php(lua_State * L, php_luasandbox_obj * intern) +static inline void luasandbox_enter_php(lua_State * L, php_luasandbox_obj * intern) { intern->in_php ++; if (intern->timed_out) { intern->in_php --; - luaL_error(L, luasandbox_timeout_message); + luasandbox_timer_timeout_error(L); } } /* }}} */ @@ -119,7 +118,7 @@ * This function must be called after luasandbox_enter_php, before the callback * from Lua returns. */ -inline void luasandbox_leave_php(lua_State * L, php_luasandbox_obj * intern) +static inline void luasandbox_leave_php(lua_State * L, php_luasandbox_obj * intern) { intern->in_php --; } @@ -138,6 +137,10 @@ void luasandbox_push_zval_userdata(lua_State * L, zval * z); void luasandbox_lua_to_zval(zval * z, lua_State * L, int index, zval * sandbox_zval, HashTable * recursionGuard TSRMLS_DC); +void luasandbox_wrap_fatal(lua_State * L); +int luasandbox_is_fatal(lua_State * L, int index); +char * luasandbox_error_to_string(lua_State * L, int index); + #endif /* PHP_LUASANDBOX_H */ Added: trunk/php/luasandbox/tests/pcall.phpt =================================================================== --- trunk/php/luasandbox/tests/pcall.phpt (rev 0) +++ trunk/php/luasandbox/tests/pcall.phpt 2012-03-30 03:30:56 UTC (rev 114624) @@ -0,0 +1,46 @@ +--TEST-- +pcall() catching various errors +--FILE-- +<?php +$sandbox = new LuaSandbox; +$lua = <<<LUA + function pcall_test(f) + local status, msg + status, msg = pcall(f) + if not status then + return "Caught: " .. msg + else + return "success" + end + end +LUA; + +$tests = array( + 'Normal' => 'return 1', + 'User error' => 'error("runtime error")', + 'Argument check error' => 'string.byte()', + 'Infinite recursion' => 'function foo() foo() end foo()', + 'Infinite loop (timeout)' => 'while true do end', + 'Out of memory' => 'string.rep("x", 1000000)' +); + +$sandbox->loadString( $lua )->call(); +$sandbox->setCPULimit( 0.25 ); +$sandbox->setMemoryLimit( 100000 ); + +foreach ( $tests as $desc => $code ) { + echo "$desc: "; + try { + print implode("\n", + $sandbox->callFunction( 'pcall_test', $sandbox->loadString( $code ) ) ) . "\n"; + } catch ( LuaSandboxError $e ) { + echo "LuaSandboxError: " . $e->getMessage() . "\n"; + } +} +--EXPECT-- +Normal: success +User error: Caught: [string ""]:1: runtime error +Argument check error: Caught: [string ""]:1: bad argument #1 to 'byte' (string expected, got no value) +Infinite recursion: LuaSandboxError: not enough memory +Infinite loop (timeout): LuaSandboxError: The maximum execution time for this script was exceeded +Out of memory: LuaSandboxError: not enough memory Added: trunk/php/luasandbox/tests/xpcall.phpt =================================================================== --- trunk/php/luasandbox/tests/xpcall.phpt (rev 0) +++ trunk/php/luasandbox/tests/xpcall.phpt 2012-03-30 03:30:56 UTC (rev 114624) @@ -0,0 +1,102 @@ +--TEST-- +xpcall() basic behaviour +--FILE-- +<?php + +$lua = <<<LUA + function xpcall_test(f, err) + local status, msg + status, msg = xpcall(f, err) + if not status then + return msg + else + return "success" + end + end +LUA; + +$xperr = 'return "xp: " .. msg'; + +$tests = array( + 'Normal' => array( + 'return 1', + $xperr + ), + 'User error' => array( + 'error("runtime error")', + $xperr + ), + 'Error in error handler' => array( + 'error("original error")', + 'error("error in handler")' + ), + 'Unconvertible error in error handler' => array( + 'error("original error")', + 'error({})' + ), + 'Numeric error in error handler' => array( + 'error("original error")', + 'error(2)', + ), + 'Argument check error' => array( + 'string.byte()', + $xperr + ), + 'Protected infinite recursion' => array( + 'function foo() foo() end foo()', + $xperr + ), + 'Infinite recursion in handler' => array( + 'error("x")', + 'function foo() foo() end foo()' + ), + 'Protected infinite loop' => array( + 'while true do end', + $xperr, + ), + 'Infinite loop in handler' => array( + 'error("x")', + 'while true do end', + ), + 'Out of memory in handler' => array( + 'error("x")', + 'string.rep("x", 1000000)' + ), +); + +$sandbox = new LuaSandbox; +$sandbox->loadString( $lua )->call(); +$sandbox->setCPULimit( 0.25 ); +$sandbox->setMemoryLimit( 100000 ); + +foreach ( $tests as $desc => $info ) { + $sandbox = new LuaSandbox; + $sandbox->loadString( $lua )->call(); + $sandbox->setCPULimit( 0.25 ); + $sandbox->setMemoryLimit( 100000 ); + echo "$desc: "; + list( $code, $errorCode ) = $info; + $func = $sandbox->loadString( $code ); + $errorCode = "return function(msg) $errorCode end"; + $ret = $sandbox->loadString( $errorCode )->call(); + $errorFunc = $ret[0]; + + try { + print implode("\n", + $sandbox->callFunction( 'xpcall_test', $func, $errorFunc ) ) . "\n"; + } catch ( LuaSandboxError $e ) { + echo "LuaSandboxError: " . $e->getMessage() . "\n"; + } +} +--EXPECT-- +Normal: success +User error: xp: [string ""]:1: runtime error +Error in error handler: LuaSandboxError: [string ""]:1: error in handler +Unconvertible error in error handler: LuaSandboxError: unknown error +Numeric error in error handler: LuaSandboxError: [string ""]:1: 2 +Argument check error: xp: [string ""]:1: bad argument #1 to 'byte' (string expected, got no value) +Protected infinite recursion: LuaSandboxError: not enough memory +Infinite recursion in handler: LuaSandboxError: not enough memory +Protected infinite loop: LuaSandboxError: The maximum execution time for this script was exceeded +Infinite loop in handler: LuaSandboxError: The maximum execution time for this script was exceeded +Out of memory in handler: LuaSandboxError: not enough memory Modified: trunk/php/luasandbox/timer.c =================================================================== --- trunk/php/luasandbox/timer.c 2012-03-30 01:26:34 UTC (rev 114623) +++ trunk/php/luasandbox/timer.c 2012-03-30 03:30:56 UTC (rev 114624) @@ -20,6 +20,7 @@ struct timespec * normal_timeout, struct timespec * emergency_timeout) {} void luasandbox_timer_stop(luasandbox_timer_set * lts) {} +void luasandbox_timer_timeout_error(lua_State *L) {} #else @@ -29,6 +30,8 @@ static void luasandbox_timer_timeout_hook(lua_State *L, lua_Debug *ar); static void luasandbox_timer_settime(luasandbox_timer * lt, struct timespec * ts); +char luasandbox_timeout_message[] = "The maximum execution time for this script was exceeded"; + void luasandbox_timer_install_handler(struct sigaction * oldact) { struct sigaction newact; @@ -46,6 +49,7 @@ static void luasandbox_timer_handle(int signo, siginfo_t * info, void * context) { luasandbox_timer_callback_data * data; + lua_State * L; if (signo != LUASANDBOX_SIGNAL || info->si_code != SI_TIMER @@ -56,6 +60,7 @@ data = (luasandbox_timer_callback_data*)info->si_value.sival_ptr; data->sandbox->timed_out = 1; + L = data->sandbox->state; if (data->emergency) { sigset_t set; sigemptyset(&set); @@ -68,11 +73,13 @@ "was exceeded and a PHP callback failed to return"); } else { // The Lua state is dirty now and can't be used again. - luaL_error(data->sandbox->state, "emergency timeout"); + lua_pushstring(L, "emergency timeout"); + luasandbox_wrap_fatal(L); + lua_error(L); } } else { // Set a hook which will terminate the script execution in a graceful way - lua_sethook(data->sandbox->state, luasandbox_timer_timeout_hook, + lua_sethook(L, luasandbox_timer_timeout_hook, LUA_MASKCALL | LUA_MASKRET | LUA_MASKLINE, 1); } } @@ -82,9 +89,16 @@ // Avoid infinite recursion lua_sethook(L, luasandbox_timer_timeout_hook, 0, 0); // Do a longjmp to report the timeout error - luaL_error(L, luasandbox_timeout_message); + luasandbox_timer_timeout_error(L); } +void luasandbox_timer_timeout_error(lua_State *L) +{ + lua_pushstring(L, luasandbox_timeout_message); + luasandbox_wrap_fatal(L); + lua_error(L); +} + void luasandbox_timer_start(luasandbox_timer_set * lts, php_luasandbox_obj * sandbox, struct timespec * normal_timeout, _______________________________________________ MediaWiki-CVS mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs
