[php-src] master: Zend: Skip frameless registration for temporary modules
| From: | Ilia Alshanetsky | Date: | Wed, 30 Sep 2026 15:07:06 +0000 |
| Subject: | [php-src] master: Zend: Skip frameless registration for temporary modules | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141799@lists.php.net to get a copy of this message | ||
Author: Ilia Alshanetsky (iliaal)
Date: 2026-09-30T10:59:46-04:00
Commit: https://github.com/php/php-src/commit/75d77cb4fe2becb8945a29381c6568a5a5ffb2b5
Raw diff: https://github.com/php/php-src/commit/75d77cb4fe2becb8945a29381c6568a5a5ffb2b5.diff
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
Changed paths:
A ext/dl_test/tests/frameless_temporary.inc
A ext/dl_test/tests/frameless_temporary.phpt
A ext/dl_test/tests/frameless_temporary_cgi.inc
A ext/dl_test/tests/frameless_temporary_opcache.phpt
M NEWS
M Zend/zend_API.c
M ext/dl_test/dl_test.c
M ext/dl_test/dl_test.stub.php
M ext/dl_test/dl_test_arginfo.h
Diff:
diff --git a/NEWS b/NEWS
index ab9bca11cd56..2aa1831288a1 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 3c9891a00e92..2b3068dadefc 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 7aebf5431338..3492a25a3a2f 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 c2d8ff577780..4f5a445e9837 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 84c7c151ae79..bd7752211150 100644
--- a/ext/dl_test/dl_test_arginfo.h
+++ b/ext/dl_test/dl_test_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 0641a8eeff00e6c8083fe4a8639f970e3ba80db9 */
+ * Stub hash: 0ab7bc0f3289a77ecb1850a1cc7056b7c5297fda */
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dl_test_test1, 0, 0, IS_VOID, 0)
ZEND_END_ARG_INFO()
@@ -8,18 +8,31 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dl_test_test2, 0, 0, IS_STRING,
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, str, IS_STRING, 0, "\"\"")
ZEND_END_ARG_INFO()
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_dl_test_frameless, 0, 1, IS_LONG, 0)
+ ZEND_ARG_TYPE_INFO(0, value, IS_LONG, 0)
+ZEND_END_ARG_INFO()
+
#define arginfo_class_DlTest_test arginfo_dl_test_test2
#define arginfo_class_DlTestSuperClass_test arginfo_dl_test_test2
+
+ZEND_FRAMELESS_FUNCTION(dl_test_frameless, 1);
+static const zend_frameless_function_info frameless_function_infos_dl_test_frameless[] = {
+ { ZEND_FRAMELESS_FUNCTION_NAME(dl_test_frameless, 1), 1 },
+ { 0 },
+};
+
ZEND_FUNCTION(dl_test_test1);
ZEND_FUNCTION(dl_test_test2);
+ZEND_FUNCTION(dl_test_frameless);
ZEND_METHOD(DlTest, test);
ZEND_METHOD(DlTestSuperClass, test);
static const zend_function_entry ext_functions[] = {
ZEND_FE(dl_test_test1, arginfo_dl_test_test1)
ZEND_FE(dl_test_test2, arginfo_dl_test_test2)
+ ZEND_RAW_FENTRY("dl_test_frameless", zif_dl_test_frameless, arginfo_dl_test_frameless,
0, frameless_function_infos_dl_test_frameless, NULL)
ZEND_FE_END
};
diff --git a/ext/dl_test/tests/frameless_temporary.inc b/ext/dl_test/tests/frameless_temporary.inc
new file mode 100644
index 000000000000..8b452a5e8dce
--- /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 000000000000..fe38123d99e8
--- /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 000000000000..a5a9398f0e4a
--- /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 000000000000..547ee71d2145
--- /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