I did this
Small leak in cf-h2-proxy.c from the recent bc40e09f63 (2026-05-05, "lib: introduce Curl_peer") — the H2 proxy filter's ctx_clear memsets the context before Curl_peer_unlink(&ctx->dest) gets a chance to drop the refcount, so the peer leaks per failed CONNECT.
lib/cf-h2-proxy.c:198-205:
static void cf_h2_proxy_ctx_free(struct cf_h2_proxy_ctx *ctx)
{
if(ctx) {
cf_h2_proxy_ctx_clear(ctx); /* memsets ctx, including ctx->dest */
Curl_peer_unlink(&ctx->dest); /* now NULL — no-op */
curlx_free(ctx);
}
}
Curl_peer_unlink (lib/peer.c:298) returns immediately when *ppeer is NULL. The H1 sibling has the order right — tunnel_free (lib/cf-h1-proxy.c:182-192) calls Curl_peer_unlink(&ts->dest) first, before any field clears. Same commit fixed H1 correctly and broke H2.
Same shape in cf_h2_proxy_close (lib/cf-h2-proxy.c:1017-1030) — the ctx_clear runs but ctx->dest is never unlinked first. While in there, the H1 sibling at lib/cf-h1-proxy.c:755-767 also explicitly resets cf->connected = FALSE on close; the H2 filter omits that, which is harmless in current call paths (Curl_conn_free immediately follows) but worth aligning for sanity.
Two-line fix:
static void cf_h2_proxy_ctx_free(struct cf_h2_proxy_ctx *ctx)
{
if(ctx) {
- cf_h2_proxy_ctx_clear(ctx);
Curl_peer_unlink(&ctx->dest);
+ cf_h2_proxy_ctx_clear(ctx);
curlx_free(ctx);
}
}
static void cf_h2_proxy_close(...)
{
...
if(ctx) {
struct cf_call_data save;
+ cf->connected = FALSE;
CF_DATA_SAVE(save, cf, data);
+ Curl_peer_unlink(&ctx->dest);
cf_h2_proxy_ctx_clear(ctx);
CF_DATA_RESTORE(cf, save);
}
}
PoC: a Python H2 proxy that accepts CONNECT, returns :status 200, then closes the inner stream (reports/F21_h2proxy.py). A small client (reports/F21_poc.c) runs three iterations with CURLPROXY_HTTPS2. ASan reports three leaks for three iterations:
==1461494==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 153 byte(s) in 3 object(s) allocated from:
#0 calloc
#1 peer_create lib/peer.c:121
#2 Curl_peer_from_url lib/peer.c:445
...
SUMMARY: AddressSanitizer: 153 byte(s) leaked in 3 allocation(s).
Patched libcurl: zero leaks across the same run.
I expected the following
no leak
curl/libcurl version
master branch
operating system
all
I did this
Small leak in
cf-h2-proxy.cfrom the recentbc40e09f63(2026-05-05, "lib: introduce Curl_peer") — the H2 proxy filter'sctx_clearmemsets the context beforeCurl_peer_unlink(&ctx->dest)gets a chance to drop the refcount, so the peer leaks per failed CONNECT.lib/cf-h2-proxy.c:198-205:Curl_peer_unlink(lib/peer.c:298) returns immediately when*ppeeris NULL. The H1 sibling has the order right —tunnel_free(lib/cf-h1-proxy.c:182-192) callsCurl_peer_unlink(&ts->dest)first, before any field clears. Same commit fixed H1 correctly and broke H2.Same shape in
cf_h2_proxy_close(lib/cf-h2-proxy.c:1017-1030) — thectx_clearruns butctx->destis never unlinked first. While in there, the H1 sibling atlib/cf-h1-proxy.c:755-767also explicitly resetscf->connected = FALSEon close; the H2 filter omits that, which is harmless in current call paths (Curl_conn_freeimmediately follows) but worth aligning for sanity.Two-line fix:
static void cf_h2_proxy_ctx_free(struct cf_h2_proxy_ctx *ctx) { if(ctx) { - cf_h2_proxy_ctx_clear(ctx); Curl_peer_unlink(&ctx->dest); + cf_h2_proxy_ctx_clear(ctx); curlx_free(ctx); } } static void cf_h2_proxy_close(...) { ... if(ctx) { struct cf_call_data save; + cf->connected = FALSE; CF_DATA_SAVE(save, cf, data); + Curl_peer_unlink(&ctx->dest); cf_h2_proxy_ctx_clear(ctx); CF_DATA_RESTORE(cf, save); } }PoC: a Python H2 proxy that accepts CONNECT, returns
:status 200, then closes the inner stream (reports/F21_h2proxy.py). A small client (reports/F21_poc.c) runs three iterations withCURLPROXY_HTTPS2. ASan reports three leaks for three iterations:Patched libcurl: zero leaks across the same run.
I expected the following
no leak
curl/libcurl version
master branch
operating system
all