From ec8ddd886f8c7a0b7407c0d72c77fd43bcdc4e3b Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 17:18:47 -0400 Subject: [PATCH] ext/curl: Free the pushed handle when the push callback denies or throws When the callback denies the push, or throws and leaves the return value undefined, libcurl destroys the easy handle itself. The wrapper kept a stale ch->cp after a throw, and in both cases never released the CurlHandle it had just created, so each denied push leaked one wrapper and permanently bumped the parent's clone counter. Clear ch->cp and drop the callback's reference instead. curl_free_obj() now keys its early return on ch->clone, which is unset only when the constructor failed, so a handle whose easy handle libcurl already freed still releases its handlers when its last reference goes, including when the callback kept it. --- NEWS | 4 + ext/curl/interface.c | 2 +- ext/curl/multi.c | 15 ++-- .../tests/curl_pushfunction_deny_leak.phpt | 80 +++++++++++++++++++ .../tests/curl_pushfunction_deny_retain.phpt | 57 +++++++++++++ 5 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 ext/curl/tests/curl_pushfunction_deny_leak.phpt create mode 100644 ext/curl/tests/curl_pushfunction_deny_retain.phpt diff --git a/NEWS b/NEWS index e7f45c810b23..7d3e5ea0ecba 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,10 @@ PHP NEWS . Fix GH-21999: GC inconsistency with lazy object, var_dump(), and object comparison. (Arnaud) +- Curl: + . Fixed a leaked CurlHandle and clone accounting when a CURLMOPT_PUSHFUNCTION + callback denies the push or throws. (Ilia Alshanetsky) + - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function registrations are freed while still reachable from the cycle collector. diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 336a89a214c4..d51099e92ecb 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -2834,7 +2834,7 @@ static void curl_free_obj(zend_object *object) fprintf(stderr, "DTOR CALLED, ch = %x\n", ch); #endif - if (!ch->cp) { + if (!ch->clone) { /* Can happen if constructor throws. */ zend_object_std_dtor(&ch->std); return; diff --git a/ext/curl/multi.c b/ext/curl/multi.c index a484373c107b..cdcfdc2418b1 100644 --- a/ext/curl/multi.c +++ b/ext/curl/multi.c @@ -409,16 +409,15 @@ static int _php_server_push_callback(CURL *parent_ch, CURL *easy, size_t num_hea zend_call_known_fcc(&mh->handlers.server_push, &retval, /* param_count */ 3, call_args, /* named_params */ NULL); zval_ptr_dtor_nogc(&headers); - if (!Z_ISUNDEF(retval)) { - if (CURL_PUSH_DENY != php_curl_get_long(&retval)) { - rval = CURL_PUSH_OK; - zend_llist_add_element(&mh->easyh, &pz_ch); - } else { - /* libcurl will free this easy handle, avoid double free */ - ch->cp = NULL; - } + if (!Z_ISUNDEF(retval) && CURL_PUSH_DENY != php_curl_get_long(&retval)) { + rval = CURL_PUSH_OK; + zend_llist_add_element(&mh->easyh, &pz_ch); + return rval; } + ch->cp = NULL; + zval_ptr_dtor(&pz_ch); + return rval; } /* }}} */ diff --git a/ext/curl/tests/curl_pushfunction_deny_leak.phpt b/ext/curl/tests/curl_pushfunction_deny_leak.phpt new file mode 100644 index 000000000000..f10d158c871d --- /dev/null +++ b/ext/curl/tests/curl_pushfunction_deny_leak.phpt @@ -0,0 +1,80 @@ +--TEST-- +CURLMOPT_PUSHFUNCTION deny must not leak the pushed handle +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + $iterations * $bytes_per_iteration_limit) { + printf("leaked %d bytes (%d bytes/iteration)\n", $growth, (int) ($growth / $iterations)); +} else { + echo "no leak detected\n"; +} +?> +--EXPECT-- +all pushes denied +no leak detected diff --git a/ext/curl/tests/curl_pushfunction_deny_retain.phpt b/ext/curl/tests/curl_pushfunction_deny_retain.phpt new file mode 100644 index 000000000000..4f33f2ca3911 --- /dev/null +++ b/ext/curl/tests/curl_pushfunction_deny_retain.phpt @@ -0,0 +1,57 @@ +--TEST-- +CURLMOPT_PUSHFUNCTION deny with the pushed handle retained by the callback +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + strlen($data))); +unset($kept); +gc_collect_cycles(); +echo "Done\n"; +?> +--EXPECT-- +int(1) +bool(true) +Done