From: Willy Tarreau Date: Tue, 6 Oct 2026 20:41:23 +0000 (+0200) Subject: BUG/MEDIUM: mux-h2: don't return a freed tasklet from h2_deferred_shut() X-Git-Tag: v3.5-dev9~37 X-Git-Url: https://git.kaiwu.me/$%7BISSUES%7D/static/gitweb.js?a=commitdiff_plain;h=91d1152beff92d62a8e535b09bd17099f1f0fb9c;p=haproxy.git BUG/MEDIUM: mux-h2: don't return a freed tasklet from h2_deferred_shut() When h2_deferred_shut() completes the deferred shutdown of an orphaned stream (the stream connector already left), it destroys the stream with h2s_destroy(), which also frees the stream's shut_tl tasklet, which is precisely the one being run. However the function then returns , which is that freed tasklet, unless the connection was also released. The scheduler then clears TASK_RUNNING in the freed tasklet's state, which is a use-after-free write. In default builds the area is generally still in the thread's pool cache, so the bit being cleared is harmless, but it may corrupt memory if the area was reused, and it crashes with -dMuaf. The situation happens when an RST_STREAM (or an ES) could not be sent when the stream was shut because the mux buffer was full, and the stream detaches before the deferred shutdown is performed, e.g. an H2 client not reading its responses fast enough while a POST request is denied. It was reproduced with a debug patch simulating a full mux buffer for the first RST_STREAM, with a POST request blocked by "http-request deny" and -dMuaf: the worker dies with SIGSEGV in run_tasks_from_lists(), and works fine with the fix. The fix simply consists in always returning NULL once the stream was destroyed. This was introduced in 2.4 by commit 741631414 ("CLEANUP: task: make sure tasklet handlers always indicate their statuses"), so the fix must be backported to all supported versions. Note that mux-fcgi's fcgi_deferred_shut() has the same issue. --- diff --git a/src/mux_h2.c b/src/mux_h2.c index d181454d2..7455b3238 100644 --- a/src/mux_h2.c +++ b/src/mux_h2.c @@ -6076,11 +6076,11 @@ struct task *h2_deferred_shut(struct task *t, void *ctx, unsigned int state) h2_remove_from_list(h2s); if (!h2s_sc(h2s)) { + /* note: this frees the tasklet as well */ h2s_destroy(h2s); - if (h2c_is_dead(h2c)) { + t = NULL; + if (h2c_is_dead(h2c)) h2_release(h2c); - t = NULL; - } } } end: