From e5b72c53c7d789d19d1d1cd10b275e87d0415413 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>
---
 lib/buffers.c    | 51 +++++++++++++++++++++++++-----------------------
 lib/gnutls_int.h |  4 ++--
 2 files changed, 29 insertions(+), 26 deletions(-)

From bd70e112d4d1f063223f0f0886aaaf33699390d0 Mon Sep 17 00:00:00 2001
From: Alexander Sosedkin <asosedkin@redhat.com>
Date: Wed, 22 Apr 2026 14:19:57 +0200
Subject: buffers: rename a variable in parse_handshake_header

Signed-off-by: Alexander Sosedkin <asosedkin@redhat.com>
---
 lib/buffers.c | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

From 1057846cd5c611037113327f5ae38af2c5cd7c87 Mon Sep 17 00:00:00 2001
From: Alexander Sosedkin <asosedkin@redhat.com>
Date: Fri, 20 Mar 2026 16:55:10 +0100
Subject: [PATCH] tests/mini-dtls-fragments: test injecting 0-length ones

Signed-off-by: Alexander Sosedkin <asosedkin@redhat.com>
---
 tests/mini-dtls-fragments.c | 47 +++++++++++++++++++++++++++++++++++++
 1 file changed, 47 insertions(+)

Index: gnutls-3.7.3/lib/buffers.c
===================================================================
--- gnutls-3.7.3.orig/lib/buffers.c
+++ gnutls-3.7.3/lib/buffers.c
@@ -893,7 +893,7 @@ parse_handshake_header(gnutls_session_t
 {
 	uint8_t *dataptr = NULL;	/* for realloc */
 	size_t handshake_header_size =
-	    HANDSHAKE_HEADER_SIZE(session), data_size, frag_size;
+	    HANDSHAKE_HEADER_SIZE(session), data_size, frag_length;
 
 	/* Note: SSL2_HEADERS == 1 */
 	if (_mbuffer_get_udata_size(bufel) < handshake_header_size)
@@ -909,7 +909,7 @@ parse_handshake_header(gnutls_session_t
 	     && bufel->htype == GNUTLS_HANDSHAKE_CLIENT_HELLO_V2)) {
 		handshake_header_size = SSL2_HEADERS;	/* we've already read one byte */
 
-		frag_size = _mbuffer_get_udata_size(bufel) - handshake_header_size;	/* we've read the first byte */
+		frag_length = _mbuffer_get_udata_size(bufel) - handshake_header_size;	/* we've read the first byte */
 
 		if (dataptr[0] != GNUTLS_HANDSHAKE_CLIENT_HELLO)
 			return
@@ -919,7 +919,7 @@ parse_handshake_header(gnutls_session_t
 
 		hsk->sequence = 0;
 		hsk->start_offset = 0;
-		hsk->length = frag_size;
+		hsk->length = frag_length;
 	} else
 #endif
 	{	/* TLS or DTLS handshake headers */
@@ -936,12 +936,12 @@ parse_handshake_header(gnutls_session_t
 			hsk->sequence = _gnutls_read_uint16(&dataptr[4]);
 			hsk->start_offset =
 			    _gnutls_read_uint24(&dataptr[6]);
-			frag_size =
+			frag_length =
 			    _gnutls_read_uint24(&dataptr[9]);
 		} else {
 			hsk->sequence = 0;
 			hsk->start_offset = 0;
-			frag_size =
+			frag_length =
 			    MIN((_mbuffer_get_udata_size(bufel) -
 				 handshake_header_size), hsk->length);
 		}
@@ -957,30 +957,23 @@ parse_handshake_header(gnutls_session_t
 	}
 	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_length;
 
 	_gnutls_handshake_log
 	    ("HSK[%p]: %s (%u) was received. Length %d[%d], frag offset %d, frag length: %d, sequence: %d\n",
 	     session, _gnutls_handshake2str(hsk->htype),
 	     (unsigned) hsk->htype, (int) hsk->length, (int) data_size,
-	     hsk->start_offset, (int) frag_size,
+	     hsk->start_offset, (int) frag_length,
 	     (int) hsk->sequence);
 
 	hsk->header_size = handshake_header_size;
 	memcpy(hsk->header, _mbuffer_get_udata_ptr(bufel),
 	       handshake_header_size);
 
-	if (hsk->length > 0 && (frag_size > data_size ||
-				(frag_size > 0 &&
-				 hsk->end_offset >= hsk->length))) {
+	if (frag_length > data_size) /* fragment straight up lying to us */
 		return
 		    gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
-	}
-	else if (hsk->length == 0 && hsk->end_offset != 0
-		 && hsk->start_offset != 0)
+	if (frag_length + hsk->start_offset > hsk->length) /* reassembly OOB */
 		return
 		    gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
 
@@ -1037,19 +1030,20 @@ static int merge_handshake_packet(gnutls
 		    gnutls_assert_val(GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS);
 
 	if (!exists) {
-		if (hsk->length > 0 && hsk->end_offset > 0
-		    && hsk->end_offset - hsk->start_offset + 1 !=
-		    hsk->length) {
+		if (hsk->length > 0) {
 			ret =
 			    _gnutls_buffer_resize(&hsk->data, hsk->length);
 			if (ret < 0)
 				return gnutls_assert_val(ret);
 
 			hsk->data.length = 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++;
@@ -1083,20 +1077,27 @@ static int merge_handshake_packet(gnutls
 		}
 
 		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);
 	}
@@ -1157,8 +1158,8 @@ static int get_last_packet(gnutls_sessio
 		}
 
 		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,
@@ -1170,7 +1171,9 @@ static int get_last_packet(gnutls_sessio
 			/* 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);
 			else
@@ -1358,9 +1361,7 @@ int _gnutls_parse_record_buffered_msgs(g
 						 ret);
 
 				data_size =
-				    MIN(tmp.length,
-					tmp.end_offset - tmp.start_offset +
-					1);
+				    MIN(tmp.length, tmp.frag_length);
 
 				ret =
 				    _gnutls_buffer_append_data(&tmp.data,
@@ -1378,9 +1379,7 @@ int _gnutls_parse_record_buffered_msgs(g
 				    merge_handshake_packet(session, &tmp);
 				if (ret < 0)
 					return gnutls_assert_val(ret);
-
-			}
-			while (_mbuffer_get_udata_size(bufel) > 0);
+			} while (_mbuffer_get_udata_size(bufel) > 0);
 
 			prev = bufel;
 			bufel =
Index: gnutls-3.7.3/lib/gnutls_int.h
===================================================================
--- gnutls-3.7.3.orig/lib/gnutls_int.h
+++ gnutls-3.7.3/lib/gnutls_int.h
@@ -400,10 +400,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;
Index: gnutls-3.7.3/tests/mini-dtls-fragments.c
===================================================================
--- gnutls-3.7.3.orig/tests/mini-dtls-fragments.c
+++ gnutls-3.7.3/tests/mini-dtls-fragments.c
@@ -165,6 +165,50 @@ static uint64_t read_u48(const uint8_t *
 	return seq;
 }
 
+static void make_0frag(uint8_t *dst, const uint8_t *src)
+{
+	memcpy(dst, src, 13 + 12);
+	dst[13 + 6] = dst[13 + 7] = dst[13 + 8] = 0; /* frag offset = 0 */
+	dst[13 + 9] = dst[13 + 10] = dst[13 + 11] = 0; /* frag length = 0 */
+	/* record payload length: just the 12-byte handshake header, no data */
+	dst[11] = 0;
+	dst[12] = 12;
+}
+
+ATTRIBUTE_NONNULL((2))
+static ssize_t client_push_inj0(gnutls_transport_ptr_t tr, const void *d_,
+				size_t l)
+{
+	static uint32_t seq = 0;
+	const uint8_t *d = (const uint8_t *)d_;
+	uint8_t frag[13 + 12];
+	uint8_t *b;
+
+	if (l < 13) /* too short for a DTLS record header */
+		return queue_put(&c2s, d, l);
+	if (!(d[3] == 0 && d[4] == 0)) /* not epoch 0: encrypted, don't touch */
+		return queue_put(&c2s, d, l);
+
+	b = malloc(l);
+	assert(b);
+	memcpy(b, d, l);
+
+	if (l >= 13 + 12 && d[0] == 22) { /* handshake record: inject 0-frag */
+		make_0frag(frag, d);
+		write_u48(frag + 5, seq++); /* 0-frag first */
+		queue_put(&c2s, frag, sizeof(frag));
+
+		write_u48(b + 5, seq++); /* real second */
+		queue_put(&c2s, b, l);
+	} else { /* other (e.g. CCS): just renumber */
+		write_u48(b + 5, seq++);
+		queue_put(&c2s, b, l);
+	}
+
+	free(b);
+	return l;
+}
+
 static void test(gnutls_push_func client_push, bool expect_success)
 {
 	gnutls_session_t client, server;
@@ -459,16 +503,78 @@ static ssize_t client_push_split_hello_b
 	return l;
 }
 
+static void test_malicious1811(void)
+{
+	static const uint8_t dgram[] = {
+		22, /* type = handshake */
+		0xfe, 0xfd, /* version = DTLS 1.2 */
+		0x00, 0x00, /* epoch = 0 */
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* seq = 0 */
+		0x00, 0x0c, /* record length = 12 */
+
+		0x01, /* type = ClientHello */
+		0xff, 0xff, 0xff, /* length = 0xffffff (!) */
+		0x00, 0x00, /* msg seq = 0 */
+		0x00, 0x00, 0x02, /* frag_offset = 2 (!) */
+		0x00, 0x00, 0x00, /* frag_length = 0 (!) */
+	};
+	gnutls_session_t server;
+	gnutls_certificate_credentials_t scred;
+	int sr;
+
+	if (debug)
+		gnutls_global_set_log_level(4711);
+
+	gnutls_certificate_allocate_credentials(&scred);
+	gnutls_certificate_set_x509_key_mem(scred, &server_cert, &server_key,
+					    GNUTLS_X509_FMT_PEM);
+
+	gnutls_init(&server, GNUTLS_SERVER | GNUTLS_DATAGRAM);
+	gnutls_priority_set_direct(server, "NORMAL:+VERS-DTLS1.2", NULL);
+	gnutls_credentials_set(server, GNUTLS_CRD_CERTIFICATE, scred);
+
+	gnutls_dtls_set_timeouts(server, get_dtls_retransmit_timeout(),
+				 get_timeout());
+
+	gnutls_transport_set_ptr(server, server);
+	gnutls_transport_set_push_function(server, server_push);
+	gnutls_transport_set_pull_function(server, server_pull);
+	gnutls_transport_set_pull_timeout_function(server,
+						   c2s_pull_timeout_once);
+
+	queue_put(&c2s, dgram, sizeof(dgram));
+
+	gnutls_global_set_log_function(server_log_func);
+	do {
+		sr = gnutls_handshake(server); /* crashes if vulnerable */
+	} while (c2s.head != c2s.tail && !gnutls_error_is_fatal(sr));
+	if (gnutls_error_is_fatal(sr))
+		fail("server: %s\n", gnutls_strerror(sr));
+
+	success("OK\n");
+
+	queue_reset(&c2s);
+	queue_reset(&s2c);
+
+	gnutls_deinit(server);
+	gnutls_certificate_free_credentials(scred);
+}
+
 void doit(void)
 {
 	global_init();
+	success("normal:\n");
 	test(client_push_normal, true);
+	success("valid 0-len fragments injected every 2nd push in epoch0:\n");
+	test(client_push_inj0, true);
 	success("malicious reassembly bug exploitation (#1816):\n");
 	test_malicious1816();
 	success("split client hello smoke-test\n");
 	test(client_push_split_hello, true);
 	success("split client hello smoke-test and mangle sequence number\n");
 	test(client_push_split_hello_bad_seq, false);
+	success("malicious injection aiming for an underflow (#1811):\n");
+	test_malicious1811();
 	gnutls_global_deinit();
 }
 
