Commit b7c524a19fa for php.net
commit b7c524a19fa815799a858b98d39f176ca88648b1
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jul 17 11:38:13 2026 -0400
Free unbound arguments when partial application construction throws (#22788)
The ZEND_CALLABLE_CONVERT_PARTIAL handler relies on zp_bind() to move
each pre-bound argument into the partial and never frees positional
arguments itself. When construction throws before an argument is moved,
a NULL op_array from zp_get_op_array() or a bound-argument type error,
the unmoved arguments were owned by nobody and leaked. Free the
arguments that were not moved at each throwing path.
diff --git a/Zend/tests/partial_application/construct_throw_arg_leak.phpt b/Zend/tests/partial_application/construct_throw_arg_leak.phpt
new file mode 100644
index 00000000000..1291901f0cc
--- /dev/null
+++ b/Zend/tests/partial_application/construct_throw_arg_leak.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Partial application must not leak bound arguments when construction throws
+--FILE--
+<?php
+class Obj {}
+
+function typed(int $a, $b, $c) {}
+try {
+ typed([1, 2, 3], new Obj(), ?);
+} catch (\TypeError $e) {
+ echo $e::class, "\n";
+}
+
+function typed2($a, int $b, $c, $d) {}
+try {
+ typed2(new Obj(), [1, 2], new Obj(), ?);
+} catch (\TypeError $e) {
+ echo $e::class, "\n";
+}
+
+function one($a) {}
+try {
+ one(new Obj(), new Obj(), ?);
+} catch (\ArgumentCountError $e) {
+ echo $e::class, "\n";
+}
+
+echo "OK\n";
+?>
+--EXPECT--
+TypeError
+TypeError
+ArgumentCountError
+OK
diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c
index 4095daa1f1a..2b5b5f4d3b4 100644
--- a/Zend/zend_partial.c
+++ b/Zend/zend_partial.c
@@ -1069,6 +1069,13 @@ static const zend_op_array *zp_get_op_array(zval *this_ptr, zend_function *funct
return op_array;
}
+static void zp_free_unbound_args(uint32_t start, uint32_t argc, zval *argv)
+{
+ for (uint32_t offset = start; offset < argc; offset++) {
+ zval_ptr_dtor_nogc(&argv[offset]);
+ }
+}
+
/* Bind pre-bound arguments as lexical vars */
static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *argv,
zend_array *extra_named_params) {
@@ -1102,6 +1109,7 @@ static void zp_bind(zval *result, zend_function *function, uint32_t argc, zval *
zend_verify_arg_error(function, arg_info, offset+1, var);
zval_ptr_dtor(result);
ZVAL_NULL(result);
+ zp_free_unbound_args(offset, argc, argv);
return;
}
ZEND_ASSERT(zp_arg_must_be_sent_by_ref(function, offset+1) ? Z_ISREF_P(var) : !Z_ISREF_P(var));
@@ -1131,6 +1139,7 @@ void zend_partial_create(zval *result, zval *this_ptr, zend_function *function,
if (UNEXPECTED(!op_array)) {
ZEND_ASSERT(EG(exception));
+ zp_free_unbound_args(0, argc, argv);
ZVAL_NULL(result);
return;
}