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;
 	}