Commit 1bffb94a2f9 for php
commit 1bffb94a2f9f6d64f3c1973e5221e99711b4b39e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Thu Oct 1 06:42:36 2026 -0400
Zend: Include frameless functions in the system id
FRAMELESS_ICALL stores an index into zend_flf_handlers, whose layout
depends on the loaded extensions, but zend_system_id did not cover it. A
file cache warmed with a frameless-declaring extension then crashed in a
process without it, or one that loads it with dl(), which registers no
frameless handlers. Each persistent frameless function now adds its name
and arity to the system id when it registers, before disable_functions can
free the function record.
Closes GH-24047
diff --git a/NEWS b/NEWS
index fb0f05f502b..0052e326ce8 100644
--- a/NEWS
+++ b/NEWS
@@ -112,6 +112,8 @@ PHP NEWS
arnaud-lb)
. Fixed bug GH-24063 (OPcache optimizer folds `$cond ? -0.0 : 0.0` into a
single `-0.0` constant). (lazerg)
+ . Fixed crash when a file cache is reused with a different set of
+ extensions declaring frameless functions. (Ilia Alshanetsky)
- OpenSSL:
. Fixed stream_socket_enable_crypto() leaving the socket non-blocking
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 2a61b689106..732dc6dd79b 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -34,6 +34,7 @@
#include "zend_enum.h"
#include "zend_object_handlers.h"
#include "zend_observer.h"
+#include "zend_system_id.h"
#include <stdarg.h>
@@ -3111,6 +3112,7 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
}
zend_flf_handlers[zend_flf_count] = flf_info->handler;
zend_flf_functions[zend_flf_count] = (zend_function *)reg_function;
+ zend_add_system_entropy("frameless", ZSTR_VAL(reg_function->function_name), &flf_info->num_args, sizeof(flf_info->num_args));
zend_flf_count++;
flf_info++;
}
diff --git a/ext/opcache/tests/file_cache_frameless_handler.phpt b/ext/opcache/tests/file_cache_frameless_handler.phpt
new file mode 100644
index 00000000000..eb557f1e718
--- /dev/null
+++ b/ext/opcache/tests/file_cache_frameless_handler.phpt
@@ -0,0 +1,81 @@
+--TEST--
+opcache file cache must not reuse frameless function indices across extension sets
+--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 . 'file_cache_frameless_handler';
+$script = $cache . DIRECTORY_SEPARATOR . 'call.php';
+mkdir($cache);
+file_put_contents($script, <<<'PHP'
+<?php
+if (isset($argv[1])) {
+ dl('dl_test');
+}
+try {
+ var_dump(dl_test_frameless(7));
+} catch (Error $e) {
+ echo $e::class, ": ", $e->getMessage(), "\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);
+}
+
+function system_ids(string $cache): int {
+ $pattern = PHP_OS_FAMILY === 'Windows' ? '/*/*' : '/*';
+ return count(glob($cache . $pattern, GLOB_ONLYDIR));
+}
+
+run($cache, ['-d', 'extension=dl_test', $script]);
+var_dump(system_ids($cache));
+run($cache, [$script, 'dl']);
+var_dump(system_ids($cache));
+run($cache, [$script]);
+var_dump(system_ids($cache));
+?>
+--CLEAN--
+<?php
+$cache = __DIR__ . DIRECTORY_SEPARATOR . 'file_cache_frameless_handler';
+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--
+int(7)
+int(1)
+int(7)
+int(2)
+Error: Call to undefined function dl_test_frameless()
+int(2)