Event pipe: do not free buffers still used by the downstream

When a body filter returned an error, ngx_event_pipe_drain_chains()
returned all buffers to p->free_raw_bufs, including those in p->busy,
which are still owned by the downstream. If upstream EOF was detected
in the same ngx_event_pipe() pass, ngx_event_pipe_read_upstream() then
freed them while still referenced. With HTTP/2 only the stream's fake
connection is marked on error, so ngx_http_v2_send_output_queue() kept
sending DATA frames pointing at the freed buffers, a use-after-free.

Fixed by keeping the p->busy chains intact, the buffers are freed with
the request pool.  This also stops the pipe from reusing them for new
upstream reads, which previously could overwrite response body data
still queued for the client.  Additionally, the pipe no longer treats
a downstream it has given up on as merely blocked, so the loop is
still guaranteed to terminate.

Co-authored-by: Roman Arutyunyan <arut@nginx.com>
This commit is contained in:
Ivan Ovchinnikov 2026-08-21 20:48:15 +00:00 • committed by Ivan Ovchinnikov
parent 028c0fab84
commit 8fa349cef4

View file

@ -253,6 +253,7 @@ ngx_event_pipe_read_upstream(ngx_event_pipe_t *p)
chain->next = NULL;
} else if (!p->cacheable
&& !p->downstream_error
&& p->downstream->data == p->output_ctx
&& p->downstream->write->ready
&& !p->downstream->write->delayed)
@ -1111,11 +1112,7 @@ ngx_event_pipe_drain_chains(ngx_event_pipe_t *p)
ngx_chain_t *cl, *tl;
for ( ;; ) {
if (p->busy) {
cl = p->busy;
p->busy = NULL;
} else if (p->out) {
if (p->out) {
cl = p->out;
p->out = NULL;