Commit 77dbc2ca543 for php
commit 77dbc2ca543fbf2d05a4506820a646e7781e79ac
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sun Oct 18 19:57:29 2026 -0400
ext/opcache: Mark dl()-created persistent strings as thread-local
During a request, opcache's interned-string handlers return a plain
persistent string when the name is not already in shared memory. dl()
registers a temporary module's function, class and property names this
way, so under ZEND_RC_DEBUG the first refcount change on one of them
(building an argument TypeError, ReflectionFunction::getName(),
get_object_vars()) trips the persistent-string assertion. The strings
belong to the request's temporary module; mark them GC_PERSISTENT_LOCAL,
but only while zend_rc_debug is off, as dl() turns it off, so the check
keeps covering strings that may later be shared.
Closes GH-24421
diff --git a/ext/dl_test/dl_test.stub.php b/ext/dl_test/dl_test.stub.php
index 4f5a445e983..24ca1d323b4 100644
--- a/ext/dl_test/dl_test.stub.php
+++ b/ext/dl_test/dl_test.stub.php
@@ -20,6 +20,7 @@ public function test(string $str = ""): string {}
class DlTestSuperClass {
public int $a;
+ public int $temporary = 0;
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 bd775221115..323bd1d8028 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/opcache_temporary_strings.inc b/ext/dl_test/tests/opcache_temporary_strings.inc
new file mode 100644
index 00000000000..2da8426961c
--- /dev/null
+++ b/ext/dl_test/tests/opcache_temporary_strings.inc
@@ -0,0 +1,16 @@
+<?php
+try {
+ dl_test_test2([]);
+} catch (TypeError $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+try {
+ (new DlTest)->test([]);
+} catch (TypeError $e) {
+ echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+var_dump((new ReflectionFunction('dl_test_test2'))->getName());
+
+var_dump(get_object_vars(new DlTestSubClass));
diff --git a/ext/dl_test/tests/opcache_temporary_strings.phpt b/ext/dl_test/tests/opcache_temporary_strings.phpt
new file mode 100644
index 00000000000..e51a2b7247c
--- /dev/null
+++ b/ext/dl_test/tests/opcache_temporary_strings.phpt
@@ -0,0 +1,22 @@
+--TEST--
+dl() under opcache: refcounting names declared by a temporary module
+--SKIPIF--
+<?php include __DIR__ . "/skip.inc"; ?>
+--EXTENSIONS--
+opcache
+--INI--
+enable_dl=1
+opcache.enable_cli=1
+--FILE--
+<?php
+dl(PHP_OS_FAMILY === 'Windows' ? 'php_dl_test.dll' : 'dl_test.so');
+include __DIR__ . '/opcache_temporary_strings.inc';
+?>
+--EXPECT--
+TypeError: dl_test_test2(): Argument #1 ($str) must be of type string, array given
+TypeError: DlTest::test(): Argument #1 ($str) must be of type string, array given
+string(13) "dl_test_test2"
+array(1) {
+ ["temporary"]=>
+ int(0)
+}
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 79c7668ba5e..7d35ef5c147 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -578,6 +578,13 @@ static zend_string* ZEND_FASTCALL accel_new_interned_string_for_php(zend_string
return ret;
}
}
+#if ZEND_RC_DEBUG
+ /* PHP shouldn't create persistent strings during request, but at least
+ * dl() may do this. They belong to the temporary module and are not shared. */
+ if (!zend_rc_debug && !ZSTR_IS_INTERNED(str) && (GC_FLAGS(str) & GC_PERSISTENT)) {
+ GC_MAKE_PERSISTENT_LOCAL(str);
+ }
+#endif
return str;
}
@@ -602,19 +609,31 @@ static zend_always_inline zend_string *accel_find_interned_string_ex(zend_ulong
static zend_string* ZEND_FASTCALL accel_init_interned_string_for_php(const char *str, size_t size, bool permanent)
{
+ zend_string *ret;
+
if (ZCG(counted)) {
zend_ulong h = zend_inline_hash_func(str, size);
- zend_string *ret = accel_find_interned_string_ex(h, str, size);
+ ret = accel_find_interned_string_ex(h, str, size);
- if (!ret) {
- ret = zend_string_init(str, size, permanent);
- ZSTR_H(ret) = h;
+ if (ret) {
+ return ret;
}
- return ret;
+ ret = zend_string_init(str, size, permanent);
+ ZSTR_H(ret) = h;
+ } else {
+ ret = zend_string_init(str, size, permanent);
}
- return zend_string_init(str, size, permanent);
+#if ZEND_RC_DEBUG
+ /* PHP shouldn't create persistent strings during request, but at least
+ * dl() may do this. They belong to the temporary module and are not shared. */
+ if (!zend_rc_debug && permanent) {
+ GC_MAKE_PERSISTENT_LOCAL(ret);
+ }
+#endif
+
+ return ret;
}
static inline void accel_copy_permanent_list_types(