diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index df255c5a868f..1bfb4986f7f5 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -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); @@ -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( diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 4b313f20cd39..aff93d3a31dc 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4031,7 +4031,12 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CO 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_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } 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); @@ -4055,7 +4060,22 @@ static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_OFFSET_ 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_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)); + } CACHE_PTR(opline->result.num, fbc); } call = _zend_vm_stack_push_call_frame_ex( diff --git a/ext/opcache/tests/gh24661.phpt b/ext/opcache/tests/gh24661.phpt new file mode 100644 index 000000000000..97984fb378bf --- /dev/null +++ b/ext/opcache/tests/gh24661.phpt @@ -0,0 +1,101 @@ +--TEST-- +GH-24661 (Wrong function called or crash if the function table changed) +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +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-- +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() diff --git a/sapi/fpm/tests/gh24661-opcache-pools-disable-functions.phpt b/sapi/fpm/tests/gh24661-opcache-pools-disable-functions.phpt new file mode 100644 index 000000000000..c3321742282f --- /dev/null +++ b/sapi/fpm/tests/gh24661-opcache-pools-disable-functions.phpt @@ -0,0 +1,77 @@ +--TEST-- +FPM: GH-24661 - Script cached by a pool with other disable_functions calls the wrong function +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +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-- + diff --git a/sapi/fpm/tests/gh24661-opcache-preload-disable-functions.phpt b/sapi/fpm/tests/gh24661-opcache-preload-disable-functions.phpt new file mode 100644 index 000000000000..20047d48310d --- /dev/null +++ b/sapi/fpm/tests/gh24661-opcache-preload-disable-functions.phpt @@ -0,0 +1,61 @@ +--TEST-- +FPM: GH-24661 - Preloaded script calls the wrong function in a pool with disable_functions +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +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-- + diff --git a/sapi/fpm/tests/gh24661-preload.inc b/sapi/fpm/tests/gh24661-preload.inc new file mode 100644 index 000000000000..f979a8721db5 --- /dev/null +++ b/sapi/fpm/tests/gh24661-preload.inc @@ -0,0 +1,12 @@ +getMessage(); + } +} +function preloaded_g() { + echo var_export(true, true), "\n"; + echo gettype(getmypid()), "\n"; +}