From 4939a63dcbd6abd156389c4290a21341ac8dc0b7 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 1 Jun 2026 08:41:33 +0000 Subject: [PATCH 1/6] Proxy: fix HTTP/2 INITIAL_WINDOW_SIZE saved state The window size change was previously kept in a local variable while processing SETTINGS parameters. If parsing was suspended after SETTINGS_INITIAL_WINDOW_SIZE, the value was lost on resume and was acknowledged without being applied. Store the parsed initial window size in the HTTP/2 state until the SETTINGS frame is fully processed. --- src/http/v2/ngx_http_v2.c | 20 ++++++++++++++------ src/http/v2/ngx_http_v2.h | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c index 69cb0ae09..661761ba4 100644 --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -2181,6 +2181,8 @@ ngx_http_v2_state_settings(ngx_http_v2_connection_t *h2c, u_char *pos, return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_SIZE_ERROR); } + h2c->state.init_window_set = 0; + return ngx_http_v2_state_settings_params(h2c, pos, end); } @@ -2193,8 +2195,6 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, ngx_uint_t id, value; ngx_http_v2_out_frame_t *frame; - window_delta = 0; - while (h2c->state.length) { if (end - pos < NGX_HTTP_V2_SETTINGS_PARAM_SIZE) { return ngx_http_v2_state_save(h2c, pos, end, @@ -2222,7 +2222,8 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, NGX_HTTP_V2_FLOW_CTRL_ERROR); } - window_delta = value - h2c->init_window; + h2c->state.init_window_size = value; + h2c->state.init_window_set = 1; break; case NGX_HTTP_V2_MAX_FRAME_SIZE_SETTING: @@ -2275,13 +2276,20 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, ngx_http_v2_queue_ordered_frame(h2c, frame); - if (window_delta) { - h2c->init_window += window_delta; + if (h2c->state.init_window_set) { + window_delta = (ssize_t) h2c->state.init_window_size + - (ssize_t) h2c->init_window; - if (ngx_http_v2_adjust_windows(h2c, window_delta) != NGX_OK) { + h2c->init_window = h2c->state.init_window_size; + + if (window_delta + && ngx_http_v2_adjust_windows(h2c, window_delta) != NGX_OK) + { return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); } + + h2c->state.init_window_set = 0; } return ngx_http_v2_state_complete(h2c, pos, end); diff --git a/src/http/v2/ngx_http_v2.h b/src/http/v2/ngx_http_v2.h index 9605c8a23..3d444af0b 100644 --- a/src/http/v2/ngx_http_v2.h +++ b/src/http/v2/ngx_http_v2.h @@ -80,10 +80,12 @@ typedef struct { ngx_uint_t sid; size_t length; size_t padding; + size_t init_window_size; unsigned flags:8; unsigned incomplete:1; unsigned keep_pool:1; + unsigned init_window_set:1; /* HPACK */ unsigned parse_name:1; From 076137c966ea979ff1f2b519858c38a66eb3131c Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 8 Jun 2026 15:13:31 +0000 Subject: [PATCH 2/6] Proxy: fix HTTP/2 MAX_FRAME_SIZE setting state MAX_FRAME_SIZE was applied immediately while parsing SETTINGS parameters, unlike INITIAL_WINDOW_SIZE which is applied after the SETTINGS frame is fully processed. Store the parsed value in the HTTP/2 state and apply it after all parameters have been parsed, before INITIAL_WINDOW_SIZE adjustment can resume streams and emit DATA frames. --- src/http/v2/ngx_http_v2.c | 9 ++++++++- src/http/v2/ngx_http_v2.h | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c index 661761ba4..d9952ae0d 100644 --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -2182,6 +2182,7 @@ ngx_http_v2_state_settings(ngx_http_v2_connection_t *h2c, u_char *pos, } h2c->state.init_window_set = 0; + h2c->state.max_frame_set = 0; return ngx_http_v2_state_settings_params(h2c, pos, end); } @@ -2239,7 +2240,8 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, NGX_HTTP_V2_PROTOCOL_ERROR); } - h2c->frame_size = value; + h2c->state.max_frame_size = value; + h2c->state.max_frame_set = 1; break; case NGX_HTTP_V2_ENABLE_PUSH_SETTING: @@ -2267,6 +2269,11 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, pos += NGX_HTTP_V2_SETTINGS_PARAM_SIZE; } + if (h2c->state.max_frame_set) { + h2c->frame_size = h2c->state.max_frame_size; + h2c->state.max_frame_set = 0; + } + frame = ngx_http_v2_get_frame(h2c, NGX_HTTP_V2_SETTINGS_ACK_SIZE, NGX_HTTP_V2_SETTINGS_FRAME, NGX_HTTP_V2_ACK_FLAG, 0); diff --git a/src/http/v2/ngx_http_v2.h b/src/http/v2/ngx_http_v2.h index 3d444af0b..2f9b25df9 100644 --- a/src/http/v2/ngx_http_v2.h +++ b/src/http/v2/ngx_http_v2.h @@ -81,11 +81,13 @@ typedef struct { size_t length; size_t padding; size_t init_window_size; + size_t max_frame_size; unsigned flags:8; unsigned incomplete:1; unsigned keep_pool:1; unsigned init_window_set:1; + unsigned max_frame_set:1; /* HPACK */ unsigned parse_name:1; From aeb2795fd9442db3f5305a0b282ef8c9d1eaf084 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 1 Jun 2026 09:15:29 +0000 Subject: [PATCH 3/6] Proxy: fix HTTP/2 SETTINGS ACK ordering A SETTINGS acknowledgement should only be queued after settings changes have been applied to the connection state. This keeps the acknowledgement path aligned with the state updates performed while processing SETTINGS_INITIAL_WINDOW_SIZE. --- src/http/v2/ngx_http_v2.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c index d9952ae0d..8543dd350 100644 --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -2274,15 +2274,6 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, h2c->state.max_frame_set = 0; } - frame = ngx_http_v2_get_frame(h2c, NGX_HTTP_V2_SETTINGS_ACK_SIZE, - NGX_HTTP_V2_SETTINGS_FRAME, - NGX_HTTP_V2_ACK_FLAG, 0); - if (frame == NULL) { - return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); - } - - ngx_http_v2_queue_ordered_frame(h2c, frame); - if (h2c->state.init_window_set) { window_delta = (ssize_t) h2c->state.init_window_size - (ssize_t) h2c->init_window; @@ -2299,6 +2290,15 @@ ngx_http_v2_state_settings_params(ngx_http_v2_connection_t *h2c, u_char *pos, h2c->state.init_window_set = 0; } + frame = ngx_http_v2_get_frame(h2c, NGX_HTTP_V2_SETTINGS_ACK_SIZE, + NGX_HTTP_V2_SETTINGS_FRAME, + NGX_HTTP_V2_ACK_FLAG, 0); + if (frame == NULL) { + return ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); + } + + ngx_http_v2_queue_ordered_frame(h2c, frame); + return ngx_http_v2_state_complete(h2c, pos, end); } From 34ef0d43f7ffb4fa2735f41866b6c93786577022 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 8 Jun 2026 03:52:24 +0000 Subject: [PATCH 4/6] Proxy: limit HTTP/2 upstream header field sizes HTTP/2 upstream response header parsing used the HPACK string length to allocate header name and value buffers. The length was not checked against the configured upstream buffer size before allocation, so a malicious upstream could force nginx to allocate excessive request-pool memory before the header was rejected. Reject oversized HTTP/2 header name and value lengths before allocation. Also keep a per-header-block limit based on the upstream buffer size so a header block cannot consume unbounded memory across fields. --- src/http/modules/ngx_http_proxy_v2_module.c | 30 ++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/http/modules/ngx_http_proxy_v2_module.c b/src/http/modules/ngx_http_proxy_v2_module.c index 0be5691aa..c25e0d239 100644 --- a/src/http/modules/ngx_http_proxy_v2_module.c +++ b/src/http/modules/ngx_http_proxy_v2_module.c @@ -77,6 +77,7 @@ typedef struct { ngx_str_t value; u_char *field_end; + size_t header_limit; size_t field_length; size_t field_rest; u_char field_state; @@ -2491,6 +2492,7 @@ ngx_http_proxy_v2_parse_header(ngx_http_request_t *r, if (ctx->type == NGX_HTTP_V2_HEADERS_FRAME) { ctx->parsing_headers = 1; ctx->fragment_state = 0; + ctx->header_limit = r->upstream->conf->buffer_size; min = (ctx->flags & NGX_HTTP_V2_PADDED_FLAG ? 1 : 0) + (ctx->flags & NGX_HTTP_V2_PRIORITY_FLAG ? 5 : 0); @@ -2686,7 +2688,7 @@ ngx_http_proxy_v2_parse_fragment(ngx_http_request_t *r, ngx_http_proxy_v2_ctx_t *ctx, ngx_buf_t *b) { u_char ch, *p, *last; - size_t size; + size_t len, size; ngx_uint_t index, size_update; enum { sw_start = 0, @@ -3023,6 +3025,14 @@ ngx_http_proxy_v2_parse_fragment(ngx_http_request_t *r, break; case sw_name: + if (ctx->field_length > r->upstream->conf->buffer_size) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent too large http2 " + "header name length: %uz", + ctx->field_length); + return NGX_ERROR; + } + ctx->name.len = ctx->field_huffman ? ctx->field_length * 8 / 5 : ctx->field_length; @@ -3132,6 +3142,14 @@ ngx_http_proxy_v2_parse_fragment(ngx_http_request_t *r, break; case sw_value: + if (ctx->field_length > r->upstream->conf->buffer_size) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent too large http2 " + "header value length: %uz", + ctx->field_length); + return NGX_ERROR; + } + ctx->value.len = ctx->field_huffman ? ctx->field_length * 8 / 5 : ctx->field_length; @@ -3228,6 +3246,16 @@ ngx_http_proxy_v2_parse_fragment(ngx_http_request_t *r, } } + len = ctx->name.len + ctx->value.len; + + if (len > ctx->header_limit) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent too large http2 header"); + return NGX_ERROR; + } + + ctx->header_limit -= len; + return NGX_OK; } From a52c1cd6e4b88b34374fd96bd8d9f4e7605299e3 Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 8 Jun 2026 04:00:05 +0000 Subject: [PATCH 5/6] Proxy: fix HTTP/2 upstream control frame flood checks HTTP/2 upstream PING and SETTINGS flood checks were guarded by ctx->free == NULL. Since the counters were incremented only after that condition was true, an upstream connection with a free output buffer could receive unlimited control frames without reaching the limit. Count all non-ACK PING and SETTINGS frames from the upstream. Keep the flood limit independent of the output buffer free list state. --- src/http/modules/ngx_http_proxy_v2_module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/http/modules/ngx_http_proxy_v2_module.c b/src/http/modules/ngx_http_proxy_v2_module.c index c25e0d239..9f4d8ae2b 100644 --- a/src/http/modules/ngx_http_proxy_v2_module.c +++ b/src/http/modules/ngx_http_proxy_v2_module.c @@ -3679,7 +3679,7 @@ ngx_http_proxy_v2_parse_settings(ngx_http_request_t *r, return NGX_ERROR; } - if (ctx->free == NULL && ctx->settings++ > 1000) { + if (ctx->settings++ > 1000) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent too many settings frames"); return NGX_ERROR; @@ -3837,7 +3837,7 @@ ngx_http_proxy_v2_parse_ping(ngx_http_request_t *r, return NGX_ERROR; } - if (ctx->free == NULL && ctx->pings++ > 1000) { + if (ctx->pings++ > 1000) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent too many ping frames"); return NGX_ERROR; From 254f56139c62abce1b5f11309a4078784671186d Mon Sep 17 00:00:00 2001 From: Zhidao HONG Date: Mon, 8 Jun 2026 13:44:38 +0000 Subject: [PATCH 6/6] Proxy: fix HTTP/2 upstream GOAWAY with NO_ERROR HTTP/2 upstream GOAWAY frames with the NO_ERROR code can indicate a graceful connection shutdown. When the last stream id is lower than the stream used by the current request, the request was not processed by the upstream. Previously this was treated as an upstream error, producing a 502 response and marking the upstream peer as failed. Return a retry status for this case and let the upstream core retry the request without marking the peer failed. Keep non-zero GOAWAY errors on the existing error path, and reset HTTP/2 frame parser state when the proxy request is reinitialized for the retry. --- src/http/modules/ngx_http_proxy_v2_module.c | 20 +++++++++++++++++++- src/http/ngx_http_upstream.c | 19 +++++++++++++++---- src/http/ngx_http_upstream.h | 2 ++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/http/modules/ngx_http_proxy_v2_module.c b/src/http/modules/ngx_http_proxy_v2_module.c index 9f4d8ae2b..49e5729df 100644 --- a/src/http/modules/ngx_http_proxy_v2_module.c +++ b/src/http/modules/ngx_http_proxy_v2_module.c @@ -935,6 +935,14 @@ ngx_http_proxy_v2_reinit_request(ngx_http_request_t *r) } ctx->state = 0; + ctx->frame_state = 0; + ctx->fragment_state = 0; + ctx->rest = 0; + ctx->stream_id = 0; + ctx->type = 0; + ctx->flags = 0; + ctx->padding = 0; + ctx->error = 0; ctx->header_sent = 0; ctx->output_closed = 0; ctx->output_blocked = 0; @@ -1437,6 +1445,10 @@ ngx_http_proxy_v2_process_header(ngx_http_request_t *r) return NGX_HTTP_UPSTREAM_INVALID_HEADER; } + if (rc == NGX_HTTP_UPSTREAM_RETRY) { + return NGX_HTTP_UPSTREAM_RETRY; + } + if (rc == NGX_OK) { continue; } @@ -1918,7 +1930,9 @@ ngx_http_proxy_v2_process_control_frame(ngx_http_request_t *r, if (ctx->stream_id < ctx->id) { - /* TODO: we can retry non-idempotent requests */ + if (ctx->error == 0) { + return NGX_HTTP_UPSTREAM_RETRY; + } ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent goaway with error %ui", @@ -2207,6 +2221,10 @@ ngx_http_proxy_v2_process_frames(ngx_http_request_t *r, return NGX_ERROR; } + if (rc == NGX_HTTP_UPSTREAM_RETRY) { + return NGX_ERROR; + } + if (rc == NGX_OK) { continue; } diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c index bb8964add..9c2948f9f 100644 --- a/src/http/ngx_http_upstream.c +++ b/src/http/ngx_http_upstream.c @@ -2597,6 +2597,11 @@ done: return; } + if (rc == NGX_HTTP_UPSTREAM_RETRY) { + ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_RETRY); + return; + } + if (rc == NGX_ERROR) { ngx_http_upstream_finalize_request(r, u, NGX_HTTP_INTERNAL_SERVER_ERROR); @@ -4598,7 +4603,8 @@ ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, } if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_403 - || ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) + || ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404 + || ft_type == NGX_HTTP_UPSTREAM_FT_RETRY) { state = NGX_PEER_NEXT; @@ -4619,7 +4625,10 @@ ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, "upstream timed out"); } - if (u->peer.cached && ft_type == NGX_HTTP_UPSTREAM_FT_ERROR) { + if (u->peer.cached + && (ft_type == NGX_HTTP_UPSTREAM_FT_ERROR + || ft_type == NGX_HTTP_UPSTREAM_FT_RETRY)) + { /* TODO: inform balancer instead */ u->peer.tries++; } @@ -4670,14 +4679,16 @@ ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, timeout = u->conf->next_upstream_timeout; - if (u->request_sent + if (ft_type != NGX_HTTP_UPSTREAM_FT_RETRY + && u->request_sent && (r->method & (NGX_HTTP_POST|NGX_HTTP_LOCK|NGX_HTTP_PATCH))) { ft_type |= NGX_HTTP_UPSTREAM_FT_NON_IDEMPOTENT; } if (u->peer.tries == 0 - || ((u->conf->next_upstream & ft_type) != ft_type) + || (ft_type != NGX_HTTP_UPSTREAM_FT_RETRY + && ((u->conf->next_upstream & ft_type) != ft_type)) || (u->request_sent && r->request_body_no_buffering) || (timeout && ngx_current_msec - u->peer.start_time >= timeout)) { diff --git a/src/http/ngx_http_upstream.h b/src/http/ngx_http_upstream.h index 4560bbe9a..9603b89eb 100644 --- a/src/http/ngx_http_upstream.h +++ b/src/http/ngx_http_upstream.h @@ -31,6 +31,7 @@ #define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK 0x00001000 #define NGX_HTTP_UPSTREAM_FT_MAX_WAITING 0x00002000 #define NGX_HTTP_UPSTREAM_FT_NON_IDEMPOTENT 0x00004000 +#define NGX_HTTP_UPSTREAM_FT_RETRY 0x00008000 #define NGX_HTTP_UPSTREAM_FT_NOLIVE 0x40000000 #define NGX_HTTP_UPSTREAM_FT_OFF 0x80000000 @@ -44,6 +45,7 @@ #define NGX_HTTP_UPSTREAM_INVALID_HEADER 40 #define NGX_HTTP_UPSTREAM_EARLY_HINTS 41 +#define NGX_HTTP_UPSTREAM_RETRY 42 #define NGX_HTTP_UPSTREAM_IGN_XA_REDIRECT 0x00000002