From 932cd6f275e6ad3f381e025bf7e1e9f0d78366f3 Mon Sep 17 00:00:00 2001
From: Amaury Denoyelle <adenoyelle@haproxy.com>
Date: Tue, 17 Mar 2026 17:33:00 +0100
Subject: [PATCH] BUG/MEDIUM: h3: reject unaligned frames except DATA

HTTP/3 parser cannot deal with unaligned frames, except for DATA. As it
was expected that such case would not occur, a simple BUG_ON() was
written to protect HEADERS parsing.

First, this BUG_ON() was incorrectly written due an incorrect operator
'>=' vs '>' when checking if data wraps. Thus this patch correct it.

However this correction is not sufficient as it still possible to handle
a large unaligned HEADERS frame, which would trigger this BUG_ON(). This
is very unlikely as HEADERS is the first received frame on a request
stream, but not completely impossible. As HTTP/3 frame header (type +
length) is parsed first and removed, this leaves a small gap at the
buffer beginning. If this small gap is then filled with the remaining
frame payload, it would result in unaligned data. Also, trailers are
also sensitive here as in this case a HEADERS frame is handled after
other frames.

The objective of this patch is to ensure that an unaligned frame is now
handled in a safe way. This is extend to all HTTP/3 frames (except DATA)
and not only to HEADERS type. Parsing is interrupted if frame payload is
wrapping in the buffer. This should never happen except maybe with some
weird clients, so the connection is closed with H3_EXCESSIVE_LOAD error.

This approach is considered the safest one, in particular for backport
purpose. In the future, realign operation via copy may be implemented
instead if considered as useful.

This must be backported up to 2.6.

(cherry picked from commit 4e937e03919fe9fea6c84c47f85dcb5847e7a070)
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
(cherry picked from commit d2ecc4b1c6b3922aa77139ec0c923005340b0490)
[ad: remove function related to backend side]
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
(cherry picked from commit 6068d86318e87dd049e639e52fcef88521a5eba1)
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>
(cherry picked from commit a40912aaf2faba62ecf444f132b9c079c8f90e5f)
Signed-off-by: Amaury Denoyelle <adenoyelle@haproxy.com>

Conflict: NA
Reference: https://git.haproxy.org/?p=haproxy-3.0.git;a=patch;h=932cd6f275e6ad3f381e025bf7e1e9f0d78366f3
---
 src/h3.c |   26 ++++++++++++++++++++------
 1 file changed, 20 insertions(+), 6 deletions(-)

diff --git a/src/h3.c b/src/h3.c
index 821f422..bef2d9d 100644
--- a/src/h3.c
+++ b/src/h3.c
@@ -562,7 +562,7 @@ static ssize_t h3_headers_to_htx(struct qcs *qcs, const struct buffer *buf,
 	/* TODO support trailer parsing in this function */
 
 	/* TODO support buffer wrapping */
-	BUG_ON(b_head(buf) + len >= b_wrap(buf));
+	BUG_ON(b_head(buf) + len > b_wrap(buf));
 	ret = qpack_decode_fs((const unsigned char *)b_head(buf), len, tmp,
 	                    list, sizeof(list) / sizeof(list[0]));
 	if (ret < 0) {
@@ -1055,7 +1055,7 @@ static ssize_t h3_trailers_to_htx(struct qcs *qcs, const struct buffer *buf,
 	TRACE_ENTER(H3_EV_RX_FRAME|H3_EV_RX_HDR, qcs->qcc->conn, qcs);
 
 	/* TODO support buffer wrapping */
-	BUG_ON(b_head(buf) + len >= b_wrap(buf));
+	BUG_ON(b_head(buf) + len > b_wrap(buf));
 	ret = qpack_decode_fs((const unsigned char *)b_head(buf), len, tmp,
 	                    list, sizeof(list) / sizeof(list[0]));
 	if (ret < 0) {
@@ -1482,10 +1482,11 @@ static ssize_t h3_rcv_buf(struct qcs *qcs, struct buffer *b, int fin)
 		flen = h3s->demux_frame_len;
 		ftype = h3s->demux_frame_type;
 
-		/* Do not demux incomplete frames except H3 DATA which can be
-		 * fragmented in multiple HTX blocks.
+		/* Current HTTP/3 parser can currently only parse fully
+		 * received and aligned frames. The only exception is for DATA
+		 * frames as they can frequently be larger than bufsize.
 		 */
-		if (flen > b_data(b) && ftype != H3_FT_DATA) {
+		if (ftype != H3_FT_DATA) {
 			/* Reject frames bigger than bufsize.
 			 *
 			 * TODO HEADERS should in complement be limited with H3
@@ -1498,7 +1499,20 @@ static ssize_t h3_rcv_buf(struct qcs *qcs, struct buffer *b, int fin)
 				qcc_report_glitch(qcs->qcc, 1);
 				goto err;
 			}
-			break;
+
+			/* TODO extend parser to support the realignment of a frame. */
+			if (b_head(b) + b_data(b) > b_wrap(b)) {
+				TRACE_ERROR("cannot parse unaligned data frame", H3_EV_RX_FRAME, qcs->qcc->conn, qcs);
+				qcc_set_error(qcs->qcc, H3_ERR_EXCESSIVE_LOAD, 1);
+				qcc_report_glitch(qcs->qcc, 1);
+				goto err;
+			}
+
+			/* Only parse full HTTP/3 frames. */
+			if (flen > b_data(b)) {
+				TRACE_PROTO("pause parsing on incomplete payload", H3_EV_RX_FRAME, qcs->qcc->conn, qcs);
+				break;
+			}
 		}
 
 		last_stream_frame = (fin && flen == b_data(b));
-- 
1.7.10.4