Commit 956c177e725 for php

commit 956c177e7257d992721e6ffbfc8feae18c13d38c
Author: Jorg Adam Sowa <jorg.sowa@gmail.com>
Date:   Wed Nov 11 16:24:39 2026 +0100

    Fix stale PFA cache entries after the declaring site is freed (#25374)

    The request PFA cache is keyed on the address of the declaring site. When
    eval()'d code or a class constant AST was freed, a new PFA could reuse
    that address and get the cached function for a different call.

diff --git a/NEWS b/NEWS
index 101e48c11fa..f702d1bb3d0 100644
--- a/NEWS
+++ b/NEWS
@@ -26,6 +26,9 @@ PHP                                                                        NEWS
   . Fixed use-after-free when the error handler releases a string while an
     "Illegal string offset" warning is raised for its offset in a read, ?? or
     nested isset()/empty(). (Jorg Sowa)
+  . Fixed partial function applications in eval'd or included code, or in
+    class constants, reusing a stale cached function with wrong bound
+    arguments. (Jorg Sowa)
   . Fixed anonymous class names being truncated at the NUL byte in error
     messages, the error log, and HTML and XML-RPC error output. (Jorg Sowa)
   . Fixed AST printing dropping readonly on promoted constructor parameters.
@@ -34,6 +37,9 @@ PHP                                                                        NEWS
 - Curl:
   . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)

+- Date:
+  . Update timelib to 2026.03 (Derick)
+
 - DBA:
   . Fixed dba_close() not closing a database opened with dba_popen().
     (Ilia Alshanetsky)
@@ -127,19 +133,6 @@ PHP                                                                        NEWS
   . Fix potential ABI incompatibility when adding fields in patches by adding
     padding to globals. (ilutov)

-- Date:
-  . Update timelib to 2026.03 (Derick)
-
-- Fileinfo:
-  . Fixed bug GH-25051 (TrueType fonts detected as application/octet-stream
-    since libmagic 5.48). (Weilin Du)
-  . Fixed incorrect MIME type detection for HWPX files. (Weilin Du)
-
-- Streams:
-  . Fixed use-after-free of the previous request's default stream context when
-    a stream error is raised during request startup, such as when a POST body
-    cannot be buffered to a temporary file. (Go Kudo)
-
 05 Nov 2026, PHP 8.6.0RC5

 - Core:
diff --git a/Zend/tests/partial_application/cache_freed_declaring_site_001.phpt b/Zend/tests/partial_application/cache_freed_declaring_site_001.phpt
new file mode 100644
index 00000000000..56b4edfc932
--- /dev/null
+++ b/Zend/tests/partial_application/cache_freed_declaring_site_001.phpt
@@ -0,0 +1,37 @@
+--TEST--
+PFA in class constant: request cache must not outlive the freed declaring AST
+--FILE--
+<?php
+function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }
+
+$n = 0;
+for ($round = 0; $round < 2; $round++) {
+    for ($argc = 1; $argc <= 7; $argc++) {
+        for ($mask = 1; $mask < (1 << $argc); $mask++) {
+            $args = [];
+            $expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
+            $placeholders = 0;
+            for ($i = 0; $i < $argc; $i++) {
+                if ($mask & (1 << $i)) {
+                    $args[] = '?';
+                    $expected[$i] = ++$placeholders;
+                } else {
+                    $args[] = "'v$i'";
+                    $expected[$i] = "v$i";
+                }
+            }
+            $shape = 'f(' . implode(', ', $args) . ')';
+            $n++;
+            eval("class C$n { const X = $shape; }");
+            $pfa = constant("C$n::X");
+            $result = $pfa(...range(1, $placeholders));
+            if ($result !== implode('|', $expected)) {
+                echo "$shape: got $result\n";
+            }
+        }
+    }
+}
+echo "Done\n";
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/partial_application/cache_freed_declaring_site_002.phpt b/Zend/tests/partial_application/cache_freed_declaring_site_002.phpt
new file mode 100644
index 00000000000..56959d460c3
--- /dev/null
+++ b/Zend/tests/partial_application/cache_freed_declaring_site_002.phpt
@@ -0,0 +1,36 @@
+--TEST--
+PFA in eval(): request cache must not outlive the freed declaring op_array
+--FILE--
+<?php
+function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }
+
+$n = 0;
+for ($round = 0; $round < 2; $round++) {
+    for ($argc = 1; $argc <= 7; $argc++) {
+        for ($mask = 1; $mask < (1 << $argc); $mask++) {
+            $args = [];
+            $expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
+            $placeholders = 0;
+            for ($i = 0; $i < $argc; $i++) {
+                if ($mask & (1 << $i)) {
+                    $args[] = '?';
+                    $expected[$i] = ++$placeholders;
+                } else {
+                    $args[] = "'v$i'";
+                    $expected[$i] = "v$i";
+                }
+            }
+            $shape = 'f(' . implode(', ', $args) . ')';
+            $n++;
+            $pfa = eval("return $shape;");
+            $result = $pfa(...range(1, $placeholders));
+            if ($result !== implode('|', $expected)) {
+                echo "$shape: got $result\n";
+            }
+        }
+    }
+}
+echo "Done\n";
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/partial_application/cache_freed_declaring_site_003.phpt b/Zend/tests/partial_application/cache_freed_declaring_site_003.phpt
new file mode 100644
index 00000000000..aa85ad6c948
--- /dev/null
+++ b/Zend/tests/partial_application/cache_freed_declaring_site_003.phpt
@@ -0,0 +1,37 @@
+--TEST--
+PFA in constant declared in eval(): request cache must not outlive the freed declaring AST
+--FILE--
+<?php
+function f($a = 'A', $b = 'B', $c = 'C', $d = 'D', $e = 'E', $f = 'F', $g = 'G') { return "$a|$b|$c|$d|$e|$f|$g"; }
+
+$n = 0;
+for ($round = 0; $round < 2; $round++) {
+    for ($argc = 1; $argc <= 7; $argc++) {
+        for ($mask = 1; $mask < (1 << $argc); $mask++) {
+            $args = [];
+            $expected = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
+            $placeholders = 0;
+            for ($i = 0; $i < $argc; $i++) {
+                if ($mask & (1 << $i)) {
+                    $args[] = '?';
+                    $expected[$i] = ++$placeholders;
+                } else {
+                    $args[] = "'v$i'";
+                    $expected[$i] = "v$i";
+                }
+            }
+            $shape = 'f(' . implode(', ', $args) . ')';
+            $n++;
+            eval("const K$n = $shape;");
+            $pfa = constant("K$n");
+            $result = $pfa(...range(1, $placeholders));
+            if ($result !== implode('|', $expected)) {
+                echo "$shape: got $result\n";
+            }
+        }
+    }
+}
+echo "Done\n";
+?>
+--EXPECT--
+Done
diff --git a/Zend/tests/partial_application/cache_freed_declaring_site_004.phpt b/Zend/tests/partial_application/cache_freed_declaring_site_004.phpt
new file mode 100644
index 00000000000..aaa63866aca
--- /dev/null
+++ b/Zend/tests/partial_application/cache_freed_declaring_site_004.phpt
@@ -0,0 +1,16 @@
+--TEST--
+PFAs in constants declared on the same line must not share a cache entry
+--FILE--
+<?php
+function f($a, $b) { return "$a|$b"; }
+
+class C { const X = f(?, 'x'), Y = f('y', ?); }
+const K1 = f(?, 'k1'), K2 = f('k2', ?);
+
+var_dump((C::X)(1), (C::Y)(2), (K1)(3), (K2)(4));
+?>
+--EXPECT--
+string(3) "1|x"
+string(3) "y|2"
+string(4) "3|k1"
+string(4) "k2|4"
diff --git a/Zend/tests/partial_application/cache_freed_declaring_site_005.phpt b/Zend/tests/partial_application/cache_freed_declaring_site_005.phpt
new file mode 100644
index 00000000000..37212bfd1f0
--- /dev/null
+++ b/Zend/tests/partial_application/cache_freed_declaring_site_005.phpt
@@ -0,0 +1,42 @@
+--TEST--
+PFA in included file: request cache must not outlive the freed declaring op_array
+--INI--
+opcache.enable_cli=0
+--FILE--
+<?php
+function f($a = 'A', $b = 'B', $c = 'C', $d = 'D') { return "$a|$b|$c|$d"; }
+
+$file = __DIR__ . '/cache_freed_declaring_site_005.inc';
+for ($round = 0; $round < 2; $round++) {
+    for ($argc = 1; $argc <= 4; $argc++) {
+        for ($mask = 1; $mask < (1 << $argc); $mask++) {
+            $args = [];
+            $expected = ['A', 'B', 'C', 'D'];
+            $placeholders = 0;
+            for ($i = 0; $i < $argc; $i++) {
+                if ($mask & (1 << $i)) {
+                    $args[] = '?';
+                    $expected[$i] = ++$placeholders;
+                } else {
+                    $args[] = "'v$i'";
+                    $expected[$i] = "v$i";
+                }
+            }
+            $shape = 'f(' . implode(', ', $args) . ')';
+            file_put_contents($file, "<?php return $shape;");
+            $pfa = include $file;
+            $result = $pfa(...range(1, $placeholders));
+            if ($result !== implode('|', $expected)) {
+                echo "$shape: got $result\n";
+            }
+        }
+    }
+}
+echo "Done\n";
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/cache_freed_declaring_site_005.inc');
+?>
+--EXPECT--
+Done
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index d7b1373b551..eea1ae07888 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1361,10 +1361,16 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
 			if (uses_variadic_placeholder) {
 				flags |= ZEND_PARTIAL_USES_VARIADIC_PLACEHOLDER;
 			}
+			/* SHM names are shared by same-line PFAs, so they can't be a cache key */
+			ZEND_ASSERT((flags & ZEND_PARTIAL_CACHEABLE_IN_SHM)
+				|| !(GC_FLAGS(fcc_ast->name) & IS_STR_PERMANENT));
+			/* A non-SHM AST may be freed, but the cached PFA keeps its name alive */
+			const uint32_t *cache_key = (flags & ZEND_PARTIAL_CACHEABLE_IN_SHM)
+				? &ast->lineno : (const uint32_t *) fcc_ast->name;
 			zend_partial_create(result, scope, &frame->This, fptr,
 					ZEND_CALL_NUM_ARGS(frame), ZEND_CALL_ARG(frame, 1),
 					extra_named_params, named_positions,
-					fcc_ast->filename, &ast->lineno,
+					fcc_ast->filename, cache_key, /* declaring_op_array */ NULL,
 					(void**)cache_slot, fcc_ast->name, flags, /* const_args */ 0);

 			if (named_positions) {
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 98732f06017..abed7636622 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -12292,6 +12292,7 @@ static void zend_compile_const_expr_fcc(zend_ast **ast_ptr)

 	if (!is_fcc) {
 		fcc->filename = zend_string_copy(CG(active_op_array)->filename);
+		/* Not unique by content, but its address is a cache key until persisted */
 		fcc->name = zend_compile_partial_name(CG(active_op_array), fcc->lineno);
 	}

diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index 1108cff2301..b227c1f9e25 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -701,7 +701,8 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 		uint32_t argc, zval *argv, zend_array *extra_named_params,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
-		const uint32_t *declaring_lineno_ptr, void **cache_slot,
+		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array, void **cache_slot,
 		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	zend_op_array *op_array = NULL;
@@ -1016,7 +1017,8 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,

 	/* Takes ownership of closure_ast */
 	op_array = zend_accel_compile_pfa(closure_ast, declaring_filename,
-			declaring_lineno_ptr, function, pfa_name, flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);
+			declaring_lineno_ptr, declaring_op_array, function, pfa_name,
+			flags & ZEND_PARTIAL_CACHEABLE_IN_SHM);

 clean:
 	zp_names_dtor(var_names, argc);
@@ -1040,7 +1042,8 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
 		uint32_t argc, zval *argv, zend_array *extra_named_params,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
-		const uint32_t *declaring_lineno_ptr, void **cache_slot,
+		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array, void **cache_slot,
 		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	if (EXPECTED(function->type == ZEND_INTERNAL_FUNCTION
@@ -1056,7 +1059,7 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
 	if (UNEXPECTED(!op_array)) {
 		op_array = zp_compile(this_ptr, function, argc, argv,
 			extra_named_params, named_positions, declaring_filename, declaring_lineno_ptr,
-			cache_slot, pfa_name, flags, const_args);
+			declaring_op_array, cache_slot, pfa_name, flags, const_args);
 	}

 	if (EXPECTED(op_array) && !(function->common.fn_flags & ZEND_ACC_NEVER_CACHE)) {
@@ -1135,7 +1138,8 @@ void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr,
 		uint32_t argc, zval *argv, zend_array *extra_named_params,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
-		const uint32_t *declaring_lineno_ptr, void **cache_slot,
+		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array, void **cache_slot,
 		zend_string *pfa_name, uint32_t flags, uint32_t const_args) {

 	ZEND_ASSERT(pfa_name);
@@ -1143,7 +1147,7 @@ void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr,
 	const zend_op_array *op_array = zp_get_op_array(this_ptr, function, argc, argv,
 			extra_named_params, named_positions,
 			declaring_filename, declaring_lineno_ptr,
-			cache_slot, pfa_name, flags, const_args);
+			declaring_op_array, cache_slot, pfa_name, flags, const_args);

 	if (UNEXPECTED(!op_array)) {
 		ZEND_ASSERT(EG(exception));
diff --git a/Zend/zend_partial.h b/Zend/zend_partial.h
index 285db1161e7..43b4c8ab561 100644
--- a/Zend/zend_partial.h
+++ b/Zend/zend_partial.h
@@ -30,12 +30,14 @@ BEGIN_EXTERN_C()
 /* Create a partial application of 'function'
  * 'declaring_lineno_ptr' should be a pointer the zend_op.lineno or
  * zend_ast.lineno that declares the PFA. The address is used to build a cache
- * key. */
+ * key, so it must not be reused while cached. 'declaring_op_array' is retained
+ * for that purpose. */
 void zend_partial_create(zval *result, zend_class_entry *scope, zval *this_ptr, zend_function *function,
 		uint32_t argc, zval *argv, zend_array *extra_named_params,
 		const zend_array *named_positions,
 		zend_string *declaring_filename,
-		const uint32_t *declaring_lineno_ptr, void **cache_slot,
+		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array, void **cache_slot,
 		zend_string *pfa_name, uint32_t flags, uint32_t const_args);

 void zend_partial_op_array_dtor(zval *pDest);
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 1596838195b..d10c891a730 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -9923,7 +9923,7 @@ ZEND_VM_HANDLER(212, ZEND_CALLABLE_CONVERT_PARTIAL, CONST, CONST|UNUSED, NUM)
 		(ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) ?
 			call->extra_named_params : NULL,
 		OP2_TYPE == IS_CONST ? Z_ARRVAL_P(named_positions) : NULL,
-		EX(func)->op_array.filename, &opline->lineno, cache_slot,
+		EX(func)->op_array.filename, &opline->lineno, &EX(func)->op_array, cache_slot,
 		pfa_name, opline->extended_value & ZEND_PARTIAL_FLAGS, const_args);

 	if (ZEND_CALL_INFO(call) & ZEND_CALL_HAS_EXTRA_NAMED_PARAMS) {
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index b196dcc01f0..8679330b2d0 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 9330a85a6ea..a5fa14a5fc7 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -2113,6 +2113,7 @@ const zend_op_array *zend_accel_pfa_cache_get(
 zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array,
 		const zend_function *called_function,
 		zend_string *pfa_func_name, bool cacheable_in_shm)
 {
@@ -2169,21 +2170,33 @@ zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
 		destroy_op_array(script_op_array);
 		efree(script_op_array);

+		const zend_op_array *retained[2];
+		uint32_t num_retained = 0;
 		if ((called_function->common.fn_flags & ZEND_ACC_CLOSURE)
 				&& called_function->type == ZEND_USER_FUNCTION
 				&& called_function->op_array.refcount) {
 			/* Extend the lifetime of the called opcodes if
 			 * the called function is a closure.
 			 * See comment in zend_accel_pfa_key(). */
-			zend_op_array *copy = zend_arena_alloc(&CG(arena), sizeof(*copy));
-			memcpy(copy, called_function, sizeof(*copy));
-			function_add_ref((zend_function *) copy);
-			/* Reference the copy in op_array->dynamic_func_defs so that it's
-			 * destroyed when op_array is destroyed. */
+			retained[num_retained++] = &called_function->op_array;
+		}
+		if (declaring_op_array && declaring_op_array->refcount) {
+			/* The key contains a declaring opline address, which eval() code may free and reuse. */
+			retained[num_retained++] = declaring_op_array;
+		}
+		if (num_retained) {
+			/* Copies in dynamic_func_defs are released with op_array. */
 			ZEND_ASSERT(!op_array->dynamic_func_defs && !op_array->num_dynamic_func_defs);
-			op_array->dynamic_func_defs = safe_emalloc(1, sizeof(*op_array->dynamic_func_defs), 0);
-			op_array->dynamic_func_defs[0] = copy;
-			op_array->num_dynamic_func_defs = 1;
+			op_array->dynamic_func_defs = safe_emalloc(num_retained, sizeof(*op_array->dynamic_func_defs), 0);
+			for (uint32_t i = 0; i < num_retained; i++) {
+				zend_op_array *copy = zend_arena_alloc(&CG(arena), sizeof(*copy));
+				memcpy(copy, retained[i], sizeof(*copy));
+				/* function_add_ref() ignores ZEND_EVAL_CODE */
+				copy->type = ZEND_USER_FUNCTION;
+				function_add_ref((zend_function *) copy);
+				op_array->dynamic_func_defs[i] = copy;
+			}
+			op_array->num_dynamic_func_defs = num_retained;
 		}

 		zend_hash_add_new_ptr(&EG(partial_function_application_cache), key, op_array);
diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h
index 5fd34441c40..5f8ebdd6f27 100644
--- a/ext/opcache/ZendAccelerator.h
+++ b/ext/opcache/ZendAccelerator.h
@@ -353,6 +353,7 @@ const zend_op_array *zend_accel_pfa_cache_get(
 zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr,
+		const zend_op_array *declaring_op_array,
 		const zend_function *called_function,
 		zend_string *pfa_func_name, bool cacheable_in_shm);