diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 1f9f1f120198..4c741931dcb7 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -742,6 +742,15 @@ static zend_always_inline bool zend_fcc_equals(const zend_fcall_info_cache* a, c static zend_always_inline void zend_fcc_addref(zend_fcall_info_cache *fcc) { + ZEND_ASSERT(ZEND_FCC_INITIALIZED(*fcc) && "FCC Not initialized, possibly refetch trampoline freed by ZPP?"); + /* If the cached trampoline is set, free it */ + if (UNEXPECTED(fcc->function_handler == &EG(trampoline))) { + zend_function *copy = (zend_function*)emalloc(sizeof(zend_function)); + + memcpy(copy, fcc->function_handler, sizeof(zend_function)); + fcc->function_handler->common.function_name = NULL; + fcc->function_handler = copy; + } if (fcc->object) { GC_ADDREF(fcc->object); } @@ -758,9 +767,12 @@ static zend_always_inline void zend_fcc_dup(/* restrict */ zend_fcall_info_cache static zend_always_inline void zend_fcc_dtor(zend_fcall_info_cache *fcc) { + ZEND_ASSERT(fcc->function_handler); if (fcc->object) { OBJ_RELEASE(fcc->object); } + /* Need to free potential trampoline (__call/__callStatic) copied function handler before releasing the closure */ + zend_release_fcall_info_cache(fcc); if (fcc->closure) { OBJ_RELEASE(fcc->closure); } @@ -806,7 +818,14 @@ ZEND_API void zend_call_known_function( static zend_always_inline void zend_call_known_fcc( zend_fcall_info_cache *fcc, zval *retval_ptr, uint32_t param_count, zval *params, HashTable *named_params) { - zend_call_known_function(fcc->function_handler, fcc->object, fcc->called_scope, retval_ptr, param_count, params, named_params); + zend_function *func = fcc->function_handler; + /* Need to copy trampolines as they get released after they are called */ + if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE)) { + func = (zend_function*) emalloc(sizeof(zend_function)); + memcpy(func, fcc->function_handler, sizeof(zend_function)); + zend_string_addref(func->op_array.function_name); + } + zend_call_known_function(func, fcc->object, fcc->called_scope, retval_ptr, param_count, params, named_params); } /* Call the provided zend_function instance method on an object. */ diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index 7cb5c61a87db..e4b78939ddd7 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -1057,6 +1057,12 @@ PHP_FUNCTION(libxml_set_external_entity_loader) zend_fcc_dtor(&LIBXML(entity_loader_callback)); } if (ZEND_FCI_INITIALIZED(fci)) { /* argument not null */ + if (!ZEND_FCC_INITIALIZED(fcc)) { + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + } zend_fcc_dup(&LIBXML(entity_loader_callback), &fcc); } RETURN_TRUE; diff --git a/ext/libxml/tests/libxml_set_external_entity_loader_trampoline.phpt b/ext/libxml/tests/libxml_set_external_entity_loader_trampoline.phpt new file mode 100644 index 000000000000..17a6e1a666b3 --- /dev/null +++ b/ext/libxml/tests/libxml_set_external_entity_loader_trampoline.phpt @@ -0,0 +1,56 @@ +--TEST-- +libxml_set_external_entity_loader() trampoline callback +--EXTENSIONS-- +dom +--FILE-- + +bar +XML; + +class TrampolineTest { + const DTD = ''; + + public function __call(string $name, array $arguments) { + echo 'Trampoline for ', $name, PHP_EOL; + var_dump($arguments); + $f = fopen("php://temp", "r+"); + fwrite($f, self::DTD); + rewind($f); + return $f; + } +} +$o = new TrampolineTest(); +$callback = [$o, 'entity_loader']; + +libxml_set_external_entity_loader($callback); + +$dd = new DOMDocument; +$r = $dd->loadXML($xml); +var_dump($dd->validate()); + +echo "Done.\n"; + +?> +--EXPECT-- +Trampoline for entity_loader +array(3) { + [0]=> + string(10) "-//FOO/BAR" + [1]=> + string(25) "http://example.com/foobar" + [2]=> + array(4) { + ["directory"]=> + NULL + ["intSubName"]=> + NULL + ["extSubURI"]=> + NULL + ["extSubSystem"]=> + NULL + } +} +bool(true) +Done. diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c index f70a07b777f0..39791039b1d8 100644 --- a/ext/spl/spl_iterators.c +++ b/ext/spl/spl_iterators.c @@ -1439,6 +1439,12 @@ static spl_dual_it_object* spl_dual_it_construct(INTERNAL_FUNCTION_PARAMETERS, z if (zend_parse_parameters(ZEND_NUM_ARGS(), "Of", &zobject, ce_inner, &fci, &intern->u.callback_filter) == FAILURE) { return NULL; } + if (!ZEND_FCC_INITIALIZED(intern->u.callback_filter)) { + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &intern->u.callback_filter, NULL); + } zend_fcc_addref(&intern->u.callback_filter); break; } diff --git a/ext/spl/tests/RecursiveCallbackFilterIterator_trampoline_usage.phpt b/ext/spl/tests/RecursiveCallbackFilterIterator_trampoline_usage.phpt new file mode 100644 index 000000000000..a00d8d4672c2 --- /dev/null +++ b/ext/spl/tests/RecursiveCallbackFilterIterator_trampoline_usage.phpt @@ -0,0 +1,50 @@ +--TEST-- +RecursiveCallbackFilterIterator with trampoline callback +--FILE-- +hasChildren()) { + return true; + } + printf("%s / %s / %d / %d\n" + , print_r($value, true) + , $key + , $value == $inner->current() + , $key == $inner->key() + ); + return $value === 1 || $value === 4; +} + +class TrampolineTest { + public function __call(string $name, array $arguments) { + echo 'Trampoline for ', $name, PHP_EOL; + return test(...$arguments); + } +} +$o = new TrampolineTest(); +$callback = [$o, 'trampoline']; + +$it = new RecursiveArrayIterator([1, [2, 3], [4, 5]]); +$it = new RecursiveCallbackFilterIterator($it, $callback); +$it = new RecursiveIteratorIterator($it); + +foreach($it as $value) { + echo "=> $value\n"; +} +?> +--EXPECT-- +Trampoline for trampoline +1 / 0 / 1 / 1 +=> 1 +Trampoline for trampoline +Trampoline for trampoline +2 / 0 / 1 / 1 +Trampoline for trampoline +3 / 1 / 1 / 1 +Trampoline for trampoline +Trampoline for trampoline +4 / 0 / 1 / 1 +=> 4 +Trampoline for trampoline +5 / 1 / 1 / 1 diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c index 9ea9af2e2c44..07afb6c3f338 100644 --- a/ext/sqlite3/sqlite3.c +++ b/ext/sqlite3/sqlite3.c @@ -949,6 +949,12 @@ PHP_METHOD(SQLite3, createFunction) if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, flags | SQLITE_UTF8, func, php_sqlite3_callback_func, NULL, NULL) == SQLITE_OK) { func->func_name = estrdup(sql_func); + if (!ZEND_FCC_INITIALIZED(fcc)) { + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + } zend_fcc_dup(&func->func, &fcc); func->argc = sql_func_num_args; @@ -991,7 +997,19 @@ PHP_METHOD(SQLite3, createAggregate) if (sqlite3_create_function(db_obj->db, sql_func, sql_func_num_args, SQLITE_UTF8, func, NULL, php_sqlite3_callback_step, php_sqlite3_callback_final) == SQLITE_OK) { func->func_name = estrdup(sql_func); + if (!ZEND_FCC_INITIALIZED(step_fcc)) { + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + zend_is_callable_ex(&step_fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &step_fcc, NULL); + } zend_fcc_dup(&func->step, &step_fcc); + if (!ZEND_FCC_INITIALIZED(fini_fcc)) { + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + zend_is_callable_ex(&fini_fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fini_fcc, NULL); + } zend_fcc_dup(&func->fini, &fini_fcc); func->argc = sql_func_num_args; @@ -1032,6 +1050,12 @@ PHP_METHOD(SQLite3, createCollation) if (sqlite3_create_collation(db_obj->db, collation_name, SQLITE_UTF8, collation, php_sqlite3_callback_compare) == SQLITE_OK) { collation->collation_name = estrdup(collation_name); + if (!ZEND_FCC_INITIALIZED(fcc)) { + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + } zend_fcc_dup(&collation->cmp_func, &fcc); collation->next = db_obj->collations; @@ -1293,6 +1317,12 @@ PHP_METHOD(SQLite3, setAuthorizer) /* Only enable userland authorizer if argument is not NULL */ if (ZEND_FCI_INITIALIZED(fci)) { + if (!ZEND_FCC_INITIALIZED(fcc)) { + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + /* Call trampoline has been cleared by zpp. Refetch it, because we want to deal + * with it outselves. It is important that it is not refetched on every call, + * because calls may occur from different scopes. */ + } db_obj->authorizer_fcc = fcc; zend_fcc_addref(&db_obj->authorizer_fcc); } diff --git a/ext/sqlite3/tests/sqlite3_trampoline_create_aggregate.phpt b/ext/sqlite3/tests/sqlite3_trampoline_create_aggregate.phpt new file mode 100644 index 000000000000..cda9eee278f0 --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_trampoline_create_aggregate.phpt @@ -0,0 +1,75 @@ +--TEST-- +SQLite3::createAggregate() trampoline callback +--EXTENSIONS-- +sqlite3 +--FILE-- + 0, 'values' => []]; + } + $context['total'] += (int) $arguments[2]; + $context['values'][] = $context['total']; + return $context; + } +} +$o = new TrampolineTest(); +$step = [$o, 'step']; +$finalize = [$o, 'finalize']; + +echo "Creating Table\n"; +var_dump($db->exec('CREATE TABLE test (a INTEGER, b INTEGER)')); + +echo "INSERT into table\n"; +var_dump($db->exec("INSERT INTO test (a, b) VALUES (1, -1)")); +var_dump($db->exec("INSERT INTO test (a, b) VALUES (2, -2)")); +var_dump($db->exec("INSERT INTO test (a, b) VALUES (3, -3)")); +var_dump($db->exec("INSERT INTO test (a, b) VALUES (4, -4)")); +var_dump($db->exec("INSERT INTO test (a, b) VALUES (4, -4)")); + +$db->createAggregate('S', $step, $finalize, 1); + +print_r($db->querySingle("SELECT S(a), S(b) FROM test", true)); + +echo "Closing database\n"; +var_dump($db->close()); +echo "Done\n"; +?> +--EXPECT-- +Creating Table +bool(true) +INSERT into table +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for step +Trampoline for finalize +Trampoline for finalize +Array +( + [S(a)] => 1,3,6,10,14 + [S(b)] => -1,-3,-6,-10,-14 +) +Closing database +bool(true) +Done diff --git a/ext/sqlite3/tests/sqlite3_trampoline_create_collation.phpt b/ext/sqlite3/tests/sqlite3_trampoline_create_collation.phpt new file mode 100644 index 000000000000..33cf8a164b4c --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_trampoline_create_collation.phpt @@ -0,0 +1,57 @@ +--TEST-- +Test SQLite3::createCollation() trampoline callback +--EXTENSIONS-- +sqlite3 +--FILE-- +createCollation('NAT', $callback); + +$db->exec('CREATE TABLE t (s varchar(4))'); + +$stmt = $db->prepare('INSERT INTO t VALUES (?)'); +foreach(array('a1', 'a10', 'a2') as $s){ + $stmt->bindParam(1, $s); + $stmt->execute(); +} + +$defaultSort = $db->query('SELECT s FROM t ORDER BY s'); //memcmp() sort +$naturalSort = $db->query('SELECT s FROM t ORDER BY s COLLATE NAT'); //strnatcmp() sort + +echo "default\n"; +while ($row = $defaultSort->fetchArray()){ + echo $row['s'], "\n"; +} + +echo "natural\n"; +while ($row = $naturalSort->fetchArray()){ + echo $row['s'], "\n"; +} + +$db->close(); + +?> +--EXPECT-- +Trampoline for NAT +Trampoline for NAT +default +a1 +a10 +a2 +natural +Trampoline for NAT +Trampoline for NAT +a1 +a2 +a10 diff --git a/ext/sqlite3/tests/sqlite3_trampoline_createfunction.phpt b/ext/sqlite3/tests/sqlite3_trampoline_createfunction.phpt new file mode 100644 index 000000000000..81245f5b4bef --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_trampoline_createfunction.phpt @@ -0,0 +1,28 @@ +--TEST-- +SQLite3::createFunction trampoline callback +--EXTENSIONS-- +sqlite3 +--FILE-- +createfunction('strtoupper', $callback)); +var_dump($db->querySingle('SELECT strtoupper("test")')); +var_dump($db->querySingle('SELECT strtoupper("test")')); + +?> +--EXPECT-- +bool(true) +Trampoline for strtoupper +string(4) "TEST" +Trampoline for strtoupper +string(4) "TEST" diff --git a/ext/sqlite3/tests/sqlite3_trampoline_setauthorizer.phpt b/ext/sqlite3/tests/sqlite3_trampoline_setauthorizer.phpt new file mode 100644 index 000000000000..919a9895e120 --- /dev/null +++ b/ext/sqlite3/tests/sqlite3_trampoline_setauthorizer.phpt @@ -0,0 +1,41 @@ +--TEST-- +SQLite3 user authorizer trampoline callback +--EXTENSIONS-- +sqlite3 +--FILE-- +enableExceptions(true); + +$db->setAuthorizer($callback); + +// This query should be accepted +var_dump($db->querySingle('SELECT 1;')); + +try { + // This one should fail + var_dump($db->querySingle('CREATE TABLE test (a, b);')); +} catch (\Exception $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +Trampoline for authorizer +int(1) +Trampoline for authorizer +Unable to prepare statement: 23, not authorized