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

Reply via email to