Skip to content
Open
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
24 changes: 22 additions & 2 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -4014,7 +4014,12 @@ ZEND_VM_HOT_HANDLER(61, ZEND_INIT_FCALL, NUM, CONST, NUM|CACHE_SLOT)
if (UNEXPECTED(fbc == NULL)) {
fname = (zval*)RT_CONSTANT(opline, opline->op2);
func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(fname));
ZEND_ASSERT(func != NULL && "Function existence must be checked at compile time");
/* The function table can differ from the one the script was compiled with.
* A call compiled for an internal function (op2.extra is its offset) can't go to a user function. */
if (UNEXPECTED(func == NULL)
|| (Z_EXTRA_P(fname) && UNEXPECTED(Z_FUNC_P(func)->type != ZEND_INTERNAL_FUNCTION))) {
ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper);
}
fbc = Z_FUNC_P(func);
if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) {
init_func_run_time_cache(&fbc->op_array);
Expand All @@ -4038,7 +4043,22 @@ ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_INIT_FCALL, Z_EXTRA_P(RT_CONSTANT(op, op->op2
zend_execute_data *call;
fbc = CACHED_PTR(opline->result.num);
if (UNEXPECTED(fbc == NULL)) {
fbc = Z_PTR(EG(function_table)->arData[Z_EXTRA_P(RT_CONSTANT(opline, opline->op2))].val);
zval *fname = (zval*)RT_CONSTANT(opline, opline->op2);
uint32_t offset = Z_EXTRA_P(fname);
Bucket *p = EG(function_table)->arData;

/* The offset is stale if the function table differs from the one the script was compiled with. */
if (EXPECTED(offset < EG(function_table)->nNumUsed)
&& EXPECTED(p[offset].key && zend_string_equals(p[offset].key, Z_STR_P(fname)))) {
fbc = Z_PTR(p[offset].val);
} else {
zval *func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(fname));
fbc = func ? Z_FUNC_P(func) : NULL;
}
/* The call was compiled for an internal function and can't go to a user function. */
if (UNEXPECTED(fbc == NULL) || UNEXPECTED(fbc->type != ZEND_INTERNAL_FUNCTION)) {
ZEND_VM_DISPATCH_TO_HELPER(zend_undefined_function_helper);
}
CACHE_PTR(opline->result.num, fbc);
}
call = _zend_vm_stack_push_call_frame_ex(
Expand Down
24 changes: 22 additions & 2 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions ext/opcache/tests/gh24661.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
--TEST--
GH-24661 (Wrong function called or crash if the function table changed)
--EXTENSIONS--
opcache
--SKIPIF--
<?php
$dl_test = PHP_OS_FAMILY === 'Windows' ? 'php_dl_test.dll' : 'dl_test.so';
if (!file_exists(ini_get('extension_dir') . DIRECTORY_SEPARATOR . $dl_test)) {
die('skip dl_test extension is not built');
}
?>
--FILE--
<?php
$cache = __DIR__ . DIRECTORY_SEPARATOR . 'gh24661';
$script = $cache . DIRECTORY_SEPARATOR . 'main.php';
mkdir($cache);
file_put_contents($script, <<<'PHP'
<?php
if (($argv[1] ?? '') === 'dl') {
dl('dl_test');
} elseif (($argv[1] ?? '') === 'user') {
include __DIR__ . '/user.php';
}
include __DIR__ . '/call.php';
PHP);
file_put_contents($cache . DIRECTORY_SEPARATOR . 'user.php', <<<'PHP'
<?php
function dl_test_test2(string $str = ''): string {
return "User $str";
}
PHP);
/* call.php is compiled after dl(). The try/catch keeps the call in f() from being
* specialized to ZEND_INIT_FCALL_OFFSET. Both calls are compiled for the internal
* function and must not go to a user function of the same name. */
file_put_contents($cache . DIRECTORY_SEPARATOR . 'call.php', <<<'PHP'
<?php
set_exception_handler(function (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), "\n";
});
function f() {
try {
return dl_test_test2('f');
} catch (Error $e) {
return $e::class . ": " . $e->getMessage();
}
}
echo f(), "\n";
echo dl_test_test2('main'), "\n";
PHP);

function run(string $cache, array $args): void {
$ext = ini_get('extension_dir');
$opcache = PHP_OS_FAMILY === 'Windows' ? 'php_opcache.dll' : 'opcache.so';
$cmd = [PHP_BINARY, '-n', '-d', 'extension_dir="' . $ext . '"'];
if (file_exists($ext . DIRECTORY_SEPARATOR . $opcache)) {
array_push($cmd, '-d', 'zend_extension=opcache');
}
array_push($cmd,
'-d', 'opcache.enable_cli=1',
'-d', 'opcache.file_cache="' . $cache . '"',
'-d', 'opcache.file_cache_only=1',
'-d', 'opcache.file_update_protection=0',
...$args,
);
$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
echo stream_get_contents($pipes[1]);
stream_get_contents($pipes[2]);
proc_close($proc);
}

echo "--- Compiled with dl_test loaded ---\n";
run($cache, [$script, 'dl']);
echo "--- Cached, run without dl_test ---\n";
run($cache, [$script]);
echo "--- Cached, run with a user function of the same name ---\n";
run($cache, [$script, 'user']);
?>
--CLEAN--
<?php
$cache = __DIR__ . DIRECTORY_SEPARATOR . 'gh24661';
if (is_dir($cache)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($cache, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $file) {
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
}
rmdir($cache);
}
?>
--EXPECT--
--- Compiled with dl_test loaded ---
Hello f
Hello main
--- Cached, run without dl_test ---
Error: Call to undefined function dl_test_test2()
Error: Call to undefined function dl_test_test2()
--- Cached, run with a user function of the same name ---
Error: Call to undefined function dl_test_test2()
Error: Call to undefined function dl_test_test2()
77 changes: 77 additions & 0 deletions sapi/fpm/tests/gh24661-opcache-pools-disable-functions.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
FPM: GH-24661 - Script cached by a pool with other disable_functions calls the wrong function
--EXTENSIONS--
opcache
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php

require_once "tester.inc";

$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[unrestricted]
listen = {{ADDR[unrestricted]}}
pm = static
pm.max_children = 1
[restricted]
listen = {{ADDR[restricted]}}
pm = static
pm.max_children = 1
php_admin_value[disable_functions] = getmypid
EOT;

/* Disabling getmypid() moves var_export() in the function table. The try/catch
* keeps the calls in f() from being specialized to ZEND_INIT_FCALL_OFFSET. */
$code = <<<'EOT'
<?php
set_exception_handler(function (Throwable $e) {
echo $e->getMessage(), "\n";
});
function f() {
try {
return gettype(getmypid());
} catch (Error $e) {
return $e->getMessage();
}
}
echo f(), "\n";
echo var_export(true, true), "\n";
echo gettype(getmypid()), "\n";
EOT;

$expected = [
'unrestricted' => ['integer', 'true', 'integer'],
'restricted' => [
'Call to undefined function getmypid()',
'true',
'Call to undefined function getmypid()',
],
];

$opcache = ini_get('extension_dir') . DIRECTORY_SEPARATOR . 'opcache.' . PHP_SHLIB_SUFFIX;
$extraArgs = is_file($opcache) ? ['-dzend_extension=' . $opcache] : [];

foreach ([['unrestricted', 'restricted'], ['restricted', 'unrestricted']] as $pools) {
$tester = new FPM\Tester($cfg, $code);
$tester->start($extraArgs, iniEntries: ['opcache.file_update_protection' => '0']);
$tester->expectLogStartNotices();
foreach ([...$pools, ...$pools] as $pool) {
$tester->request(address: "{{ADDR[$pool]}}")->expectBody($expected[$pool]);
}
$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();
}

?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>
61 changes: 61 additions & 0 deletions sapi/fpm/tests/gh24661-opcache-preload-disable-functions.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
--TEST--
FPM: GH-24661 - Preloaded script calls the wrong function in a pool with disable_functions
--EXTENSIONS--
opcache
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php

require_once "tester.inc";

$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[restricted]
listen = {{ADDR}}
pm = static
pm.max_children = 1
php_admin_value[disable_functions] = getmypid
EOT;

/* The master preloads the functions before the pool disables getmypid(). */
$code = <<<'EOT'
<?php
set_exception_handler(function (Throwable $e) {
echo $e->getMessage(), "\n";
});
echo preloaded_f(), "\n";
preloaded_g();
EOT;

$opcache = ini_get('extension_dir') . DIRECTORY_SEPARATOR . 'opcache.' . PHP_SHLIB_SUFFIX;
$extraArgs = is_file($opcache) ? ['-dzend_extension=' . $opcache] : [];
$iniEntries = ['opcache.preload' => __DIR__ . '/gh24661-preload.inc'];
if (getenv('TEST_FPM_RUN_AS_ROOT')) {
$iniEntries['opcache.preload_user'] = 'root';
}

$tester = new FPM\Tester($cfg, $code);
$tester->start($extraArgs, iniEntries: $iniEntries);
$tester->expectLogStartNotices();
$expected = [
'Call to undefined function getmypid()',
'true',
'Call to undefined function getmypid()',
];
$tester->request()->expectBody($expected);
$tester->request()->expectBody($expected);
$tester->terminate();
$tester->expectLogTerminatingNotices();
$tester->close();

?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>
12 changes: 12 additions & 0 deletions sapi/fpm/tests/gh24661-preload.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
function preloaded_f() {
try {
return gettype(getmypid());
} catch (Error $e) {
return $e->getMessage();
}
}
function preloaded_g() {
echo var_export(true, true), "\n";
echo gettype(getmypid()), "\n";
}
Loading