From 9f56efe3100874719883e327500d294a4c3bb87b Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 17 Jul 2026 09:23:03 -0400 Subject: [PATCH] Free unbound arguments when partial application construction throws 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. --- .../construct_throw_arg_leak.phpt | 34 +++++++++++++++++++ Zend/zend_partial.c | 9 +++++ 2 files changed, 43 insertions(+) create mode 100644 Zend/tests/partial_application/construct_throw_arg_leak.phpt 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 000000000000..1291901f0cc8 --- /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-- + +--EXPECT-- +TypeError +TypeError +ArgumentCountError +OK diff --git a/Zend/zend_partial.c b/Zend/zend_partial.c index 4095daa1f1a3..2b5b5f4d3b44 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; }