Commit c191020fdaf for php.net
commit c191020fdaf020e63b9e9afcb225ce3481b48abb
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jul 17 13:22:39 2026 -0400
Fix double-free of closure run_time_cache in partial application (#22790)
A partial over a closure whose op_array is not persistent shallow-copies
the closure's op_array into the per-request PFA cache as a lifetime
anchor. The copy inherited ZEND_ACC_HEAP_RT_CACHE and the run_time_cache
pointer, so destroy_op_array() freed the closure's cache twice: once when
the closure was released and again at PFA cache teardown. Use
function_add_ref() for the copy, which resets the per-request
run_time_cache and static_variables_ptr map pointers, so only the live
closure owns and frees the cache.
diff --git a/Zend/tests/partial_application/closure_rt_cache_lifetime.phpt b/Zend/tests/partial_application/closure_rt_cache_lifetime.phpt
new file mode 100644
index 00000000000..7eb4810cf00
--- /dev/null
+++ b/Zend/tests/partial_application/closure_rt_cache_lifetime.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Partial application over a Closure::fromCallable() must not double-free the run_time_cache
+--FILE--
+<?php
+class C {
+ public function m($x, $y) { return $x + $y; }
+}
+
+$cl = Closure::fromCallable([new C, 'm']);
+$partial = $cl(10, ?);
+var_dump($partial(5));
+unset($partial);
+unset($cl);
+gc_collect_cycles();
+echo "OK\n";
+?>
+--EXPECT--
+int(15)
+OK
diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c
index 2c467dc1fc8..0a7a62eae9a 100644
--- a/ext/opcache/ZendAccelerator.c
+++ b/ext/opcache/ZendAccelerator.c
@@ -2146,8 +2146,7 @@ zend_op_array *zend_accel_compile_pfa(zend_ast *ast,
* See comment in zend_accel_pfa_key(). */
zend_op_array *copy = zend_arena_alloc(&CG(arena), sizeof(*copy));
memcpy(copy, called_function, sizeof(*copy));
- zend_string_addref(copy->function_name);
- (*copy->refcount)++;
+ 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. */
ZEND_ASSERT(!op_array->dynamic_func_defs && !op_array->num_dynamic_func_defs);