Skip to content

Handle trampolines correctly in new FCC API + usages #9877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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. */
Expand Down
6 changes: 6 additions & 0 deletions ext/libxml/libxml.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
libxml_set_external_entity_loader() trampoline callback
--EXTENSIONS--
dom
--FILE--
<?php
$xml = <<<XML
<!DOCTYPE foo PUBLIC "-//FOO/BAR" "http://example.com/foobar">
<foo>bar</foo>
XML;

class TrampolineTest {
const DTD = '<!ELEMENT foo (#PCDATA)>';

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.
6 changes: 6 additions & 0 deletions ext/spl/spl_iterators.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
--TEST--
RecursiveCallbackFilterIterator with trampoline callback
--FILE--
<?php

function test($value, $key, $inner) {
if ($inner->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
30 changes: 30 additions & 0 deletions ext/sqlite3/sqlite3.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
75 changes: 75 additions & 0 deletions ext/sqlite3/tests/sqlite3_trampoline_create_aggregate.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
--TEST--
SQLite3::createAggregate() trampoline callback
--EXTENSIONS--
sqlite3
--FILE--
<?php

require_once(__DIR__ . '/new_db.inc');

class TrampolineTest {
public function __call(string $name, array $arguments) {
echo 'Trampoline for ', $name, PHP_EOL;
$context = $arguments[0];
if ($name === 'finalize') {
return implode(',', $context['values']);
}
if (empty($context)) {
$context = ['total' => 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
57 changes: 57 additions & 0 deletions ext/sqlite3/tests/sqlite3_trampoline_create_collation.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
Test SQLite3::createCollation() trampoline callback
--EXTENSIONS--
sqlite3
--FILE--
<?php

require_once __DIR__ . '/new_db.inc';

class TrampolineTest {
public function __call(string $name, array $arguments) {
echo 'Trampoline for ', $name, PHP_EOL;
return strnatcmp(...$arguments);
}
}
$o = new TrampolineTest();
$callback = [$o, 'NAT'];

$db->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
Loading