Commit 7c0ecd80530 for php.net

commit 7c0ecd80530507d9e28185be5b061aa1fbf853dc
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date:   Sun Sep 20 11:40:30 2026 +0200

    Fix OSS-Fuzz #545352966: default value AST of an SHM-persisted partial

    Ownership mismatch.
    zp_compile() generates a closure AST with parameter-default node here
    for parse_ini_string(), which is a zend_ast_ref that is shared.
    The persistence code assumes unique ownership.
    Change the code to be analogous to zend_persist_zval().

    Closes GH-23785.

diff --git a/NEWS b/NEWS
index 1a448d35591..0ddd4712ed3 100644
--- a/NEWS
+++ b/NEWS
@@ -61,6 +61,8 @@ PHP                                                                        NEWS
 - Opcache:
   . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a
     loop-invariant addition). (Ilia Alshanetsky)
+  . Fixed OSS-Fuzz #545352966 (default value AST of an SHM-persisted partial).
+    (ndossche)

 - PDO:
   . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
diff --git a/Zend/tests/partial_application/default_const_expr_shm.phpt b/Zend/tests/partial_application/default_const_expr_shm.phpt
new file mode 100644
index 00000000000..d3ac8900094
--- /dev/null
+++ b/Zend/tests/partial_application/default_const_expr_shm.phpt
@@ -0,0 +1,25 @@
+--TEST--
+PFA: default value AST of an SHM-persisted partial
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--FILE--
+<?php
+
+// parse_ini_string() has a $scanner_mode param that defaults to INI_SCANNER_NORMAL,
+// which is an IS_CONSTANT_AST.
+$p = parse_ini_string(?, ...);
+var_dump($p("a=1"));
+
+$param = (new ReflectionFunction($p))->getParameters()[2];
+var_dump($param->getDefaultValue());
+
+?>
+--EXPECT--
+array(1) {
+  ["a"]=>
+  string(1) "1"
+}
+int(0)
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index a4cc4a4ce57..497e9b80d57 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -1011,11 +1011,10 @@ static zend_op_array *zp_compile(zval *this_ptr, zend_function *function,
 	}
 #endif

+	/* 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);

-	zend_ast_destroy(closure_ast);
-
 clean:
 	zp_names_dtor(var_names, argc);
 	zend_arena_destroy(CG(ast_arena));
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 05fb6700ea7..bb4a1fbb5cf 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -2121,6 +2121,8 @@ zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
 		zend_bailout();
 	} zend_end_try();

+	zend_ast_destroy(ast);
+
 	ZEND_ASSERT(op_array->num_dynamic_func_defs == 1);

 	zend_string_release(op_array->dynamic_func_defs[0]->function_name);
diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h
index fe83800f3a0..79208893d12 100644
--- a/ext/opcache/ZendAccelerator.h
+++ b/ext/opcache/ZendAccelerator.h
@@ -340,6 +340,7 @@ uint32_t zend_accel_get_class_name_map_ptr(zend_string *type_name);
 const zend_op_array *zend_accel_pfa_cache_get(
 		const uint32_t *declaring_lineno_ptr, const zend_function *called_function, bool cacheable_in_shm);

+/* Compiles ast into an op_array, and caches it. Takes ownership of ast. */
 zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
 		zend_string *declaring_filename,
 		const uint32_t *declaring_lineno_ptr,