From 490bfb28002fc0253010243ac387c756014c06e5 Mon Sep 17 00:00:00 2001
From: Alexander Sosedkin <asosedkin@redhat.com>
Date: Mon, 23 Mar 2026 15:09:43 +0100
Subject: [PATCH] buffers: switch from end_offset over to frag_length

Instead of maintaining an inclusive [start_offset, end_offset] range
when reassembling DTLS handshake,
track start_offset and a relative frag_length instead.

You'd think it'd be a no-op, but it fixes:

* 0-length fragments triggering completion if message was 1 byte long
* a remotely triggerable underflow and an ensuing heap overrun

Reported-by: Joshua Rogers of AISLE Research Team <joshua@joshua.hu>
Fixes: #1811
Fixes: CVE-2026-33845
Fixes: GNUTLS-SA-2026-04-29-3
CVSS: 7.5 High CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Signed-off-by: Alexander Sosedkin <asosedkin@redhat.com>

CVE: CVE-2026-33845
Upstream-Status: Backport [https://gitlab.com/gnutls/gnutls/-/commit/e5b72c53c7d789d19d1d1cd10b275e87d0415413]
Signed-off-by: Adarsh Jagadish Kamini <adarsh.jagadish.kamini@est.tech>
---
 lib/buffers.c    | 50 +++++++++++++++++++++++++-----------------------
 lib/gnutls_int.h |  4 ++--
 2 files changed, 28 insertions(+), 26 deletions(-)

diff --git a/lib/buffers.c b/lib/buffers.c
index 09779a8f3..02cc222c4 100644
--- a/lib/buffers.c
+++ b/lib/buffers.c
@@ -919,10 +919,7 @@ static int parse_handshake_header(gnutls_session_t session, mbuffer_st *bufel,
 	}
 	data_size = _mbuffer_get_udata_size(bufel) - handshake_header_size;
 
-	if (frag_size > 0)
-		hsk->end_offset = hsk->start_offset + frag_size - 1;
-	else
-		hsk->end_offset = 0;
+	hsk->frag_length = frag_size;
 
 	_gnutls_handshake_log(
 		"HSK[%p]: %s (%u) was received. Length %d[%d], frag offset %d, frag length: %d, sequence: %d\n",
@@ -936,9 +933,10 @@ static int parse_handshake_header(gnutls_session_t session, mbuffer_st *bufel,
 
 	if (hsk->length > 0 &&
 	    (frag_size > data_size ||
-	     (frag_size > 0 && hsk->end_offset >= hsk->length))) {
+	     (frag_size > 0 && hsk->start_offset + frag_size > hsk->length))) {
 		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
-	} else if (hsk->length == 0 && hsk->end_offset != 0 &&
+	} else if (hsk->length == 0 &&
+		   hsk->start_offset + frag_size != hsk->start_offset &&
 		   hsk->start_offset != 0)
 		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
 
@@ -1002,11 +1000,10 @@ static int merge_handshake_packet(gnutls_session_t session,
 			hsk->data.length = hsk->length;
 		}
 
-		if (hsk->length > 0 && hsk->end_offset > 0 &&
-		    hsk->end_offset - hsk->start_offset + 1 != hsk->length) {
+		if (hsk->length > 0 && hsk->frag_length > 0 &&
+		    hsk->frag_length != hsk->length) {
 			memmove(&hsk->data.data[hsk->start_offset],
-				hsk->data.data,
-				hsk->end_offset - hsk->start_offset + 1);
+				hsk->data.data, hsk->frag_length);
 		}
 
 		session->internals.handshake_recv_buffer_size++;
@@ -1040,20 +1037,27 @@ static int merge_handshake_packet(gnutls_session_t session,
 		}
 
 		if (hsk->start_offset < recv_buf[pos].start_offset &&
-		    hsk->end_offset + 1 >= recv_buf[pos].start_offset) {
+		    hsk->start_offset + hsk->frag_length >=
+			    recv_buf[pos].start_offset) {
 			memcpy(&recv_buf[pos].data.data[hsk->start_offset],
 			       hsk->data.data, hsk->data.length);
 			recv_buf[pos].start_offset = hsk->start_offset;
-			recv_buf[pos].end_offset =
-				MIN(hsk->end_offset, recv_buf[pos].end_offset);
-		} else if (hsk->end_offset > recv_buf[pos].end_offset &&
-			   hsk->start_offset <= recv_buf[pos].end_offset + 1) {
+			recv_buf[pos].frag_length = MIN(
+				hsk->frag_length, recv_buf[pos].frag_length);
+		} else if (hsk->start_offset + hsk->frag_length >
+				   recv_buf[pos].start_offset +
+					   recv_buf[pos].frag_length &&
+			   hsk->start_offset <=
+				   recv_buf[pos].start_offset +
+					   recv_buf[pos].frag_length) {
 			memcpy(&recv_buf[pos].data.data[hsk->start_offset],
 			       hsk->data.data, hsk->data.length);
 
-			recv_buf[pos].end_offset = hsk->end_offset;
 			recv_buf[pos].start_offset = MIN(
 				hsk->start_offset, recv_buf[pos].start_offset);
+			recv_buf[pos].frag_length = hsk->start_offset +
+						    hsk->frag_length -
+						    recv_buf[pos].start_offset;
 		}
 		_gnutls_handshake_buffer_clear(hsk);
 	}
@@ -1113,8 +1117,8 @@ static int get_last_packet(gnutls_session_t session,
 		}
 
 		else if ((recv_buf[LAST_ELEMENT].start_offset == 0 &&
-			  recv_buf[LAST_ELEMENT].end_offset ==
-				  recv_buf[LAST_ELEMENT].length - 1) ||
+			  recv_buf[LAST_ELEMENT].frag_length ==
+				  recv_buf[LAST_ELEMENT].length) ||
 			 recv_buf[LAST_ELEMENT].length == 0) {
 			session->internals.dtls.hsk_read_seq++;
 			_gnutls_handshake_buffer_move(hsk,
@@ -1125,8 +1129,9 @@ static int get_last_packet(gnutls_session_t session,
 			/* if we don't have a complete handshake message, but we
 			 * have queued data waiting, try again to reconstruct the
 			 * handshake packet, using the queued */
-			if (recv_buf[LAST_ELEMENT].end_offset !=
-				    recv_buf[LAST_ELEMENT].length - 1 &&
+			if ((recv_buf[LAST_ELEMENT].start_offset +
+			     recv_buf[LAST_ELEMENT].frag_length) !=
+				    recv_buf[LAST_ELEMENT].length &&
 			    record_check_unprocessed(session) > 0)
 				return gnutls_assert_val(
 					GNUTLS_E_INT_CHECK_AGAIN);
@@ -1313,9 +1318,7 @@ int _gnutls_parse_record_buffered_msgs(gnutls_session_t session)
 					&session->internals.record_buffer,
 					bufel, ret);
 
-				data_size = MIN(tmp.length,
-						tmp.end_offset -
-							tmp.start_offset + 1);
+				data_size = MIN(tmp.length, tmp.frag_length);
 
 				ret = _gnutls_buffer_append_data(
 					&tmp.data,
@@ -1331,7 +1334,6 @@ int _gnutls_parse_record_buffered_msgs(gnutls_session_t session)
 				ret = merge_handshake_packet(session, &tmp);
 				if (ret < 0)
 					return gnutls_assert_val(ret);
-
 			} while (_mbuffer_get_udata_size(bufel) > 0);
 
 			prev = bufel;
diff --git a/lib/gnutls_int.h b/lib/gnutls_int.h
index 54d3c9f67..283f25c07 100644
--- a/lib/gnutls_int.h
+++ b/lib/gnutls_int.h
@@ -479,10 +479,10 @@ typedef struct {
 	uint16_t sequence;
 
 	/* indicate whether that message is complete.
-	 * complete means start_offset == 0 and end_offset == length
+	 * complete means start_offset == 0 and frag_length == length
 	 */
 	uint32_t start_offset;
-	uint32_t end_offset;
+	uint32_t frag_length; /* used exclusively in DTLS reassembly */
 
 	uint8_t header[MAX_HANDSHAKE_HEADER_SIZE];
 	int header_size;
