From 8acd642dc15cdc9c4353f7557dde93ab2f1d88f6 Mon Sep 17 00:00:00 2001 From: Enfilade Date: Sat, 7 Nov 2026 23:45:14 +0300 Subject: [PATCH] Fix GH-25296: Segfault or wrong function called with dl() and opcache.file_cache When opcache compiles a script after dl(), calls to functions from the loaded extension are bound like calls to any other internal function, and the compiler stores the function's offset in a literal. When the script later runs from the file cache, the function may be at another offset, may not be loaded at all, or a user function may have taken its name. dl() doesn't change the system ID, so the cached script is still used. Since d6541df63 the file cache no longer resets the offset, so the first case crashes or calls the wrong function. The other two already crashed before that commit. Don't bind calls to functions from temporary modules when compiling for opcache, as is already done for user functions from other files, so that they are resolved by name at run time. The optimizer skips them too, or it would bind them again. As these calls are now ZEND_INIT_FCALL_BY_NAME, the CFG checks that opcode too, so that db2_execute() from an extension loaded with dl() is still flagged as indirect variable access. Co-Authored-By: Claude Opus 5.5 --- Zend/Optimizer/zend_cfg.c | 6 +- Zend/Optimizer/zend_optimizer.c | 2 +- Zend/zend_compile.c | 4 +- .../tests/frameless_temporary_opcache.phpt | 2 +- ext/opcache/tests/gh25296.phpt | 105 ++++++++++++++++++ 5 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 ext/opcache/tests/gh25296.phpt diff --git a/Zend/Optimizer/zend_cfg.c b/Zend/Optimizer/zend_cfg.c index a0d08b67aa70..5c8a4c0a96c3 100644 --- a/Zend/Optimizer/zend_cfg.c +++ b/Zend/Optimizer/zend_cfg.c @@ -335,9 +335,13 @@ ZEND_API void zend_build_cfg(zend_arena **arena, const zend_op_array *op_array, flags |= ZEND_FUNC_HAS_CALLS; break; case ZEND_INIT_FCALL: + case ZEND_INIT_FCALL_BY_NAME: case ZEND_INIT_NS_FCALL_BY_NAME: zv = CRT_CONSTANT(opline->op2); - if (opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { + if (opline->opcode == ZEND_INIT_FCALL_BY_NAME) { + /* The second literal is the lowercased name */ + zv += 1; + } else if (opline->opcode == ZEND_INIT_NS_FCALL_BY_NAME) { /* The third literal is the lowercased unqualified name */ zv += 2; } diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index 77af67fe6d73..9f8a77bee34c 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -817,7 +817,7 @@ static bool zend_optimizer_ignore_function(zval *fbc_zv, zend_string *filename) zend_function *fbc = Z_PTR_P(fbc_zv); if (fbc->type == ZEND_INTERNAL_FUNCTION) { - return false; + return fbc->internal_function.module->type == MODULE_TEMPORARY; } else if (fbc->type == ZEND_USER_FUNCTION) { if (fbc->op_array.fn_flags & ZEND_ACC_PRELOADED) { if (CG(compiler_options) & ZEND_COMPILE_WITH_FILE_CACHE) { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 7e7e602f5895..fe8939f9c721 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4234,7 +4234,9 @@ static bool zend_compile_ignore_class(zend_class_entry *ce, zend_string *filenam static bool zend_compile_ignore_function(zend_function *fbc, zend_string *filename) { if (fbc->type == ZEND_INTERNAL_FUNCTION) { - return CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS; + return (CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_FUNCTIONS) + || ((CG(compiler_options) & ZEND_COMPILE_IGNORE_OTHER_FILES) + && fbc->internal_function.module->type == MODULE_TEMPORARY); } else { return (CG(compiler_options) & ZEND_COMPILE_IGNORE_USER_FUNCTIONS) || ((CG(compiler_options) & ZEND_COMPILE_IGNORE_OTHER_FILES) diff --git a/ext/dl_test/tests/frameless_temporary_opcache.phpt b/ext/dl_test/tests/frameless_temporary_opcache.phpt index 547ee71d2145..994f0c3b0086 100644 --- a/ext/dl_test/tests/frameless_temporary_opcache.phpt +++ b/ext/dl_test/tests/frameless_temporary_opcache.phpt @@ -14,6 +14,6 @@ dl(PHP_OS_FAMILY === 'Windows' ? 'php_dl_test.dll' : 'dl_test.so'); include __DIR__ . '/frameless_temporary.inc'; ?> --EXPECTF-- -%A0001 INIT_FCALL 1 %d string("dl_test_frameless") +%A0001 INIT_FCALL_BY_NAME 1 string("dl_test_frameless") %Aint(7) dl_test_frameless(): Argument #1 ($value) must be of type int, string given diff --git a/ext/opcache/tests/gh25296.phpt b/ext/opcache/tests/gh25296.phpt new file mode 100644 index 000000000000..0d4ecf019b92 --- /dev/null +++ b/ext/opcache/tests/gh25296.phpt @@ -0,0 +1,105 @@ +--TEST-- +GH-25296 (Segfault or wrong function called with dl() and opcache.file_cache) +--EXTENSIONS-- +opcache +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +}); +if ($argv[1] === 'shifted') { + function a() {} + function b() {} +} +if ($argv[1] === 'user') { + function dl_test_test2(string $str): string { + return "User $str"; + } +} elseif ($argv[1] !== 'missing') { + dl('dl_test'); +} +include __DIR__ . '/call.php'; +PHP); + +function write_script(string $script, int $version): void { + file_put_contents($script, << ['pipe', 'w'], 2 => ['redirect', 1]], $pipes); + echo stream_get_contents($pipes[1]); + proc_close($proc); +} + +write_script($call, 1); +run($cache, $main, 'shifted'); + +write_script($call, 2); +echo "--- Function at another position ---\n"; +run($cache, $main, 'dl'); + +echo "--- Function not loaded ---\n"; +run($cache, $main, 'missing'); + +echo "--- User function of the same name ---\n"; +run($cache, $main, 'user'); +?> +--CLEAN-- +isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname()); + } + rmdir($cache); +} +?> +--EXPECT-- +v1 +Hello hi +--- Function at another position --- +v1 +Hello hi +--- Function not loaded --- +v1 +Error: Call to undefined function dl_test_test2() +--- User function of the same name --- +v1 +User hi