Commit 75d77cb4fe2 for php
commit 75d77cb4fe2becb8945a29381c6568a5a5ffb2b5
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Sep 30 09:04:59 2026 -0400
Zend: Skip frameless registration for temporary modules
dl() modules are MODULE_TEMPORARY. Their function records are freed at
request shutdown, but zend_flf_functions keeps those pointers for the
process lifetime. A later request can match the stale handler address
and use the freed record. Temporary modules leave the frameless info
unset, so the call uses the ordinary internal path.
Closes GH-24008
diff --git a/NEWS b/NEWS
index ab9bca11cd5..2aa1831288a 100644
--- a/NEWS
+++ b/NEWS
@@ -26,6 +26,8 @@ PHP NEWS
. Fixed GH-23980 (ZEND_ASSERT violation @ ZEND_INCLUDE_OR_EVAL). (ndossche)
. Fixed bug GH-23896 (Assertion failure in zend_call_function() after a
throwing deprecation). (lazerg)
+ . Fixed use-after-free when a dl()-loaded extension declares a frameless
+ function and a later request calls it. (Ilia Alshanetsky)
- DOM:
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 3c9891a00e9..2b3068dadef 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -2976,7 +2976,11 @@ ZEND_API zend_result zend_register_functions(zend_class_entry *scope, const zend
internal_function->prototype = NULL;
internal_function->prop_info = NULL;
internal_function->attributes = NULL;
- internal_function->frameless_function_infos = ptr->frameless_function_infos;
+ if (type == MODULE_TEMPORARY) {
+ internal_function->frameless_function_infos = NULL;
+ } else {
+ internal_function->frameless_function_infos = ptr->frameless_function_infos;
+ }
if (EG(active)) { // at run-time: this ought to only happen if registered with dl() or somehow temporarily at runtime
ZEND_MAP_PTR_INIT(internal_function->run_time_cache, zend_arena_calloc(&CG(arena), 1, zend_internal_run_time_cache_reserved_size()));
} else {
diff --git a/ext/dl_test/dl_test.c b/ext/dl_test/dl_test.c
index 7aebf543133..3492a25a3a2 100644
--- a/ext/dl_test/dl_test.c
+++ b/ext/dl_test/dl_test.c
@@ -52,6 +52,28 @@ PHP_FUNCTION(dl_test_test2)
}
/* }}}*/
+PHP_FUNCTION(dl_test_frameless)
+{
+ zend_long value;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_LONG(value)
+ ZEND_PARSE_PARAMETERS_END();
+
+ RETURN_LONG(value);
+}
+
+ZEND_FRAMELESS_FUNCTION(dl_test_frameless, 1)
+{
+ zend_long value;
+
+ Z_FLF_PARAM_LONG(1, value);
+
+ RETVAL_LONG(value);
+
+flf_clean:;
+}
+
/* {{{ PHP_DL_TEST_USE_REGISTER_FUNCTIONS_DIRECTLY */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dl_test_use_register_functions_directly, 0, 0, IS_STRING, 0)
ZEND_END_ARG_INFO()
diff --git a/ext/dl_test/dl_test.stub.php b/ext/dl_test/dl_test.stub.php
index c2d8ff57778..4f5a445e983 100644
--- a/ext/dl_test/dl_test.stub.php
+++ b/ext/dl_test/dl_test.stub.php
@@ -9,6 +9,11 @@ function dl_test_test1(): void {}
function dl_test_test2(string $str = ""): string {}
+/**
+ * @frameless-function {"arity": 1}
+ */
+function dl_test_frameless(int $value): int {}
+
class DlTest {
public function test(string $str = ""): string {}
}
diff --git a/ext/dl_test/dl_test_arginfo.h b/ext/dl_test/dl_test_arginfo.h
index 84c7c151ae7..bd775221115 100644
Binary files a/ext/dl_test/dl_test_arginfo.h and b/ext/dl_test/dl_test_arginfo.h differ
diff --git a/ext/dl_test/tests/frameless_temporary.inc b/ext/dl_test/tests/frameless_temporary.inc
new file mode 100644
index 00000000000..8b452a5e8dc
--- /dev/null
+++ b/ext/dl_test/tests/frameless_temporary.inc
@@ -0,0 +1,8 @@
+<?php
+var_dump(dl_test_frameless(7));
+try {
+ dl_test_frameless("nope");
+ echo "no throw\n";
+} catch (TypeError $e) {
+ echo $e->getMessage(), "\n";
+}
diff --git a/ext/dl_test/tests/frameless_temporary.phpt b/ext/dl_test/tests/frameless_temporary.phpt
new file mode 100644
index 00000000000..fe38123d99e
--- /dev/null
+++ b/ext/dl_test/tests/frameless_temporary.phpt
@@ -0,0 +1,53 @@
+--TEST--
+dl() of a frameless function does not keep the freed function record
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY === 'Windows') {
+ die('skip setarch -R is required so dl() remaps the extension at the stale handler address');
+}
+if (!getenv('TEST_PHP_CGI_EXECUTABLE')) {
+ die('skip php-cgi not available');
+}
+$setarch = trim((string) shell_exec('command -v setarch'));
+if ($setarch === '') {
+ die('skip setarch -R is required so dl() remaps the extension at the stale handler address');
+}
+$arch = php_uname('m');
+exec($setarch . ' ' . escapeshellarg($arch) . ' -R true', $setarch_out, $setarch_code);
+if ($setarch_code !== 0) {
+ die('skip setarch -R cannot run on ' . $arch);
+}
+$so = ini_get('extension_dir') . DIRECTORY_SEPARATOR . 'dl_test.so';
+if (!file_exists($so)) {
+ die('skip dl_test extension is not built (tried ' . $so . ')');
+}
+?>
+--FILE--
+<?php
+$cmd = 'env -u SCRIPT_FILENAME -u PATH_TRANSLATED -u REDIRECT_STATUS -u REQUEST_METHOD -u QUERY_STRING'
+ . ' USE_ZEND_ALLOC=0 ASAN_OPTIONS=' . escapeshellarg('detect_leaks=0:halt_on_error=1:abort_on_error=1')
+ . ' setarch ' . escapeshellarg(php_uname('m')) . ' -R '
+ . escapeshellarg(getenv('TEST_PHP_CGI_EXECUTABLE'))
+ . ' -n -q -T 2 -d enable_dl=1 -d extension_dir=' . escapeshellarg(ini_get('extension_dir'))
+ . ' ' . escapeshellarg(__DIR__ . '/frameless_temporary_cgi.inc');
+$proc = proc_open($cmd, [
+ 0 => ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+], $pipes);
+fclose($pipes[0]);
+$out = stream_get_contents($pipes[1]);
+stream_get_contents($pipes[2]);
+fclose($pipes[1]);
+fclose($pipes[2]);
+echo $out;
+$code = proc_close($proc);
+if ($code !== 0) {
+ echo "exit:$code\n";
+}
+?>
+--EXPECT--
+int(7)
+dl_test_frameless(): Argument #1 ($value) must be of type int, string given
+int(7)
+dl_test_frameless(): Argument #1 ($value) must be of type int, string given
diff --git a/ext/dl_test/tests/frameless_temporary_cgi.inc b/ext/dl_test/tests/frameless_temporary_cgi.inc
new file mode 100644
index 00000000000..a5a9398f0e4
--- /dev/null
+++ b/ext/dl_test/tests/frameless_temporary_cgi.inc
@@ -0,0 +1,7 @@
+<?php
+$ext = PHP_OS_FAMILY === 'Windows' ? 'php_dl_test.dll' : 'dl_test.so';
+if (!dl($ext)) {
+ echo "dl failed\n";
+ return;
+}
+include __DIR__ . '/frameless_temporary.inc';
diff --git a/ext/dl_test/tests/frameless_temporary_opcache.phpt b/ext/dl_test/tests/frameless_temporary_opcache.phpt
new file mode 100644
index 00000000000..547ee71d214
--- /dev/null
+++ b/ext/dl_test/tests/frameless_temporary_opcache.phpt
@@ -0,0 +1,19 @@
+--TEST--
+dl() does not compile calls to a temporary module's function as frameless
+--SKIPIF--
+<?php include __DIR__ . "/skip.inc"; ?>
+--EXTENSIONS--
+opcache
+--INI--
+enable_dl=1
+opcache.enable_cli=1
+opcache.opt_debug_level=0x10000
+--FILE--
+<?php
+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")
+%Aint(7)
+dl_test_frameless(): Argument #1 ($value) must be of type int, string given