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.8.10/lib/buffers.c
===================================================================
--- gnutls-3.8.10.orig/lib/buffers.c
+++ gnutls-3.8.10/lib/buffers.c
@@ -853,7 +853,7 @@ static int parse_handshake_header(gnutls
 {
 	uint8_t *dataptr = NULL; /* for realloc */
 	size_t handshake_header_size = HANDSHAKE_HEADER_SIZE(session),
-	       data_size, frag_size;
+	       data_size, frag_length;
 
 	/* Note: SSL2_HEADERS == 1 */
 	if (_mbuffer_get_udata_size(bufel) < handshake_header_size)
@@ -868,7 +868,7 @@ static int parse_handshake_header(gnutls
 		handshake_header_size =
 			SSL2_HEADERS; /* we've already read one byte */
 
-		frag_size =
+		frag_length =
 			_mbuffer_get_udata_size(bufel) -
 			handshake_header_size; /* we've read the first byte */
 
@@ -879,7 +879,7 @@ static int parse_handshake_header(gnutls
 
 		hsk->sequence = 0;
 		hsk->start_offset = 0;
-		hsk->length = frag_size;
+		hsk->length = frag_length;
 	} else
 #endif
 	{ /* TLS or DTLS handshake headers */
@@ -894,13 +894,13 @@ static int parse_handshake_header(gnutls
 		if (IS_DTLS(session)) {
 			hsk->sequence = _gnutls_read_uint16(&dataptr[4]);
 			hsk->start_offset = _gnutls_read_uint24(&dataptr[6]);
-			frag_size = _gnutls_read_uint24(&dataptr[9]);
+			frag_length = _gnutls_read_uint24(&dataptr[9]);
 		} else {
 			hsk->sequence = 0;
 			hsk->start_offset = 0;
-			frag_size = MIN((_mbuffer_get_udata_size(bufel) -
-					 handshake_header_size),
-					hsk->length);
+			frag_length = MIN((_mbuffer_get_udata_size(bufel) -
+					   handshake_header_size),
+					  hsk->length);
 		}
 
 		/* TLS1.3: distinguish server hello versus hello retry request.
@@ -919,27 +919,21 @@ static int parse_handshake_header(gnutls
 	}
 	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, (int)hsk->sequence);
+		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);
 
 	return handshake_header_size;
@@ -1002,11 +996,10 @@ static int merge_handshake_packet(gnutls
 			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 +1033,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);
 	}
@@ -1113,8 +1113,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,
@@ -1125,8 +1125,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);
@@ -1313,9 +1314,7 @@ int _gnutls_parse_record_buffered_msgs(g
 					&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 +1330,6 @@ int _gnutls_parse_record_buffered_msgs(g
 				ret = merge_handshake_packet(session, &tmp);
 				if (ret < 0)
 					return gnutls_assert_val(ret);
-
 			} while (_mbuffer_get_udata_size(bufel) > 0);
 
 			prev = bufel;
Index: gnutls-3.8.10/lib/gnutls_int.h
===================================================================
--- gnutls-3.8.10.orig/lib/gnutls_int.h
+++ gnutls-3.8.10/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;
Index: gnutls-3.8.10/tests/mini-dtls-fragments.c
===================================================================
--- gnutls-3.8.10.orig/tests/mini-dtls-fragments.c
+++ gnutls-3.8.10/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();
 }
 
