Skip to content

Commit

Permalink
Sanify handling of array keys
Browse files Browse the repository at this point in the history
When passing an array from PHP to Lua, stringify integer array keys
beyond the range a lua_Number can represent.

When passing a table from Lua to PHP,
* Convert integers encoded as strings to actual integers
* Convert numbers outside of PHP's int range to strings
* Detect collisions, e.g. { [0] = 'foo', ["0"] = 'bar' }

Bug: T186240
Change-Id: I386245966529fc109c44b24f8a0209bdbfa30ffc
  • Loading branch information
anomiex committed Feb 6, 2018
1 parent 76122a0 commit 122132a
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 9 deletions.
81 changes: 72 additions & 9 deletions data_conversion.c
Expand Up @@ -8,6 +8,7 @@
#include <limits.h>
#include <float.h>
#include <math.h>
#include <inttypes.h>

#include "php.h"
#include "php_luasandbox.h"
Expand Down Expand Up @@ -210,6 +211,10 @@ void luasandbox_push_zval_userdata(lua_State * L, zval * z)
*/
static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable * recursionGuard)
{
#if SIZEOF_LONG > 4
char buffer[MAX_LENGTH_OF_LONG + 1];
#endif

// Recursion requires an arbitrary amount of stack space so we have to
// check the stack.
luaL_checkstack(L, 10, "converting PHP array to Lua");
Expand All @@ -232,6 +237,17 @@ static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable *
int key_type = zend_hash_get_current_key_ex(ht, &key, &key_length,
&lkey, 0, &p);
zend_hash_get_current_data_ex(ht, (void**)&value, &p);

// Lua doesn't represent most integers with absolute value over 2**53,
// so stringify them.
#if SIZEOF_LONG > 4
if (key_type == HASH_KEY_IS_LONG &&
((int64_t)lkey > INT64_C(9007199254740992) || (int64_t)lkey < INT64_C(-9007199254740992))
) {
key_length = snprintf(buffer, sizeof(buffer), "%" PRId64, (int64_t)lkey);
lua_pushlstring(L, buffer, key_length - 1);
} else
#endif
if (key_type == HASH_KEY_IS_STRING) {
lua_pushlstring(L, key, key_length - 1);
} else {
Expand All @@ -253,6 +269,16 @@ static int luasandbox_push_hashtable(lua_State * L, HashTable * ht, HashTable *
zval *value;
ZEND_HASH_FOREACH_KEY_VAL(ht, lkey, key, value)
{
// Lua doesn't represent most integers with absolute value over 2**53,
// so stringify them.
#if SIZEOF_LONG > 4
if (!key &&
((int64_t)lkey > INT64_C(9007199254740992) || (int64_t)lkey < INT64_C(-9007199254740992))
) {
size_t len = snprintf(buffer, sizeof(buffer), "%" PRId64, (int64_t)lkey);
lua_pushlstring(L, buffer, len);
} else
#endif
if (key) {
lua_pushlstring(L, ZSTR_VAL(key), ZSTR_LEN(key));
} else {
Expand Down Expand Up @@ -533,6 +559,7 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L,
const char * str;
size_t length;
lua_Number n;
zend_ulong zn;

#if PHP_VERSION_ID < 70000
zval *value, *valp;
Expand All @@ -553,14 +580,9 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L,
// Convert key, but leave it there
if (lua_type(L, -1) == LUA_TNUMBER) {
n = lua_tonumber(L, -1);
if (isfinite(n) && n == floor(n)) {
// Integer key
#if PHP_VERSION_ID < 70000
zend_hash_index_update(ht, n, (void*)&value, sizeof(zval*), NULL);
#else
zend_hash_index_update(ht, n, valp);
#endif
return 1;
if (isfinite(n) && n == floor(n) && n >= LONG_MIN && n <= LONG_MAX) {
zn = (long)n;
goto add_int_key;
}
}

Expand All @@ -575,15 +597,56 @@ static int luasandbox_lua_pair_to_array(HashTable *ht, lua_State *L,
);
luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC);
efree(message);
return 0;
}
lua_pop(L, 1);

// See if the string is convertable to a number
#if PHP_VERSION_ID < 70000
ZEND_HANDLE_NUMERIC_EX(str, length + 1, zn, goto add_int_key);
#else
if (ZEND_HANDLE_NUMERIC_STR(str, length, zn)) {
goto add_int_key;
}
#endif

// Nope, use it as a string
#if PHP_VERSION_ID < 70000
if (zend_hash_exists(ht, str, length + 1))
#else
if (zend_hash_str_exists(ht, str, length))
#endif
{
// Collision, probably the key is an integer-like string
char *message;
spprintf(&message, 0, "Collision for array key %s when passing data from Lua to PHP", str );
luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC);
efree(message);
return 0;
}
#if PHP_VERSION_ID < 70000
zend_hash_update(ht, str, length + 1, (void*)&value, sizeof(zval*), NULL);
#else
zend_hash_str_update(ht, str, length, valp);
#endif
lua_pop(L, 1);
return 1;

add_int_key:
if (zend_hash_index_exists(ht, zn)) {
// Collision, probably with a integer-like string
char *message;
spprintf(&message, 0, "Collision for array key %" PRId64 " when passing data from Lua to PHP",
(int64_t)zn
);
luasandbox_throw_runtimeerror(L, sandbox_zval, message TSRMLS_CC);
efree(message);
return 0;
}
#if PHP_VERSION_ID < 70000
zend_hash_index_update(ht, zn, (void*)&value, sizeof(zval*), NULL);
#else
zend_hash_index_update(ht, zn, valp);
#endif
return 1;
}
/* }}} */
Expand Down
80 changes: 80 additions & 0 deletions tests/array-key-conversion.phpt
@@ -0,0 +1,80 @@
--TEST--
Array key conversion
--FILE--
<?php

function testPhpToLua( $test, $array ) {
printf( "PHP→Lua %-30s ", "$test:" );

$sandbox = new LuaSandbox;
$sandbox->setMemoryLimit( 100000 );
$sandbox->setCPULimit( 0.1 );
try {
$ret = $sandbox
->loadString( 'local t, r = ..., {}; for k, v in pairs( t ) do r[v] = type(k) end return r' )
->call( $array );
if ( is_array( $ret[0] ) ) {
ksort( $ret[0], SORT_STRING );
}
printf( "%s\n", preg_replace( '/\s+/', ' ', var_export( $ret[0], 1 ) ) );
} catch ( LuaSandboxError $e ) {
printf( "EXCEPTION: %s\n", $e->getMessage() );
}
}

function testLuaToPhp( $test, $lua ) {
printf( "Lua→PHP %-30s ", "$test:" );

$sandbox = new LuaSandbox;
$sandbox->setMemoryLimit( 100000 );
$sandbox->setCPULimit( 0.1 );
try {
$ret = $sandbox->loadString( "return { $lua }" )->call();
if ( is_array( $ret[0] ) ) {
ksort( $ret[0], SORT_STRING );
}
printf( "%s\n", preg_replace( '/\s+/', ' ', var_export( $ret[0], 1 ) ) );
} catch ( LuaSandboxError $e ) {
printf( "EXCEPTION: %s\n", $e->getMessage() );
}
}

if ( PHP_INT_MAX > 9007199254740992 ) {
$a = [
'9007199254740992' => 'max', '9007199254740993' => 'max+1',
'-9007199254740992' => 'min', '-9007199254740993' => 'min-1',
];
$max = '9223372036854775807';
$max2 = '9223372036854775808';
$min = '-9223372036854775808';
$min2 = '-9223372036854775809';
} else {
$a = [
'2147483647' => 'max', '2147483648' => 'max+1',
'-2147483648' => 'min', '-2147483649' => 'min-1',
];
$max = '2147483647';
$max2 = '2147483648';
$min = '-2147483648';
$min2 = '-2147483649';
}

testPhpToLua( 'simple integers', [ -10 => 'minus ten', 0 => 'zero', 10 => 'ten' ] );
testPhpToLua( 'maximal values', $a );

testLuaToPhp( 'simple integers', '[-10] = "minus ten", [0] = "zero", [10] = "ten"' );
testLuaToPhp( 'stringified integers', '["-10"] = "minus ten", ["0"] = "zero", ["10"] = "ten"' );
testLuaToPhp( 'maximal integers', "['$max'] = 'max', ['$max2'] = 'max+1', ['$min'] = 'min', ['$min2'] = 'min-1'" );
testLuaToPhp( 'collision (0)', '[0] = "number zero", ["0"] = "string zero"' );
testLuaToPhp( 'collision (float)', '[1.5] = "number 1.5", ["1.5"] = "string 1.5"' );
testLuaToPhp( 'collision (inf)', '[1/0] = "number inf", ["inf"] = "string inf"' );

--EXPECTF--
PHPLua simple integers: array ( 'minus ten' => 'number', 'ten' => 'number', 'zero' => 'number', )
PHPLua maximal values: array ( 'max' => 'number', 'max+1' => 'string', 'min' => 'number', 'min-1' => 'string', )
Lua→PHP simple integers: array ( -10 => 'minus ten', 0 => 'zero', 10 => 'ten', )
LuaPHP stringified integers: array ( -10 => 'minus ten', 0 => 'zero', 10 => 'ten', )
LuaPHP maximal integers: array ( -%d => 'min', '-%d' => 'min-1', %d => 'max', '%d' => 'max+1', )
LuaPHP collision (0): EXCEPTION: Collision for array key 0 when passing data from Lua to PHP
LuaPHP collision (float): EXCEPTION: Collision for array key 1.5 when passing data from Lua to PHP
LuaPHP collision (inf): EXCEPTION: Collision for array key inf when passing data from Lua to PHP

0 comments on commit 122132a

Please sign in to comment.