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(