Commit 1ea089895d for openssl.org
commit 1ea089895df7820edc3e569ba62d95d8c92c068f
Author: Ryan Hooper <ryanh@openssl.foundation>
Date: Tue Sep 8 12:57:27 2026 -0400
test: derive dtls_listen_write_seq_test's expected sequence from the client
create_cookie_clienthello() patched only the first captured record's
epoch/sequence bytes to a hardcoded value, assuming the retried (with
cookie) ClientHello always fits in a single record. That assumption
silently breaks whenever it doesn't (e.g. a no-dtls1_3 build produces a
smaller ClientHello that still fragments into two records), letting a
forged sequence number collide with a later fragment's real one and
causing the reassembly to hang.
Instead, drive a real retransmission of the initial ClientHello (via
DTLSv1_handle_timeout()) to organically land the cookie retry on record
sequence 2, and report back whatever sequence number the client's record
layer actually used instead of dictating one. The test now checks the
server's response against that captured value, so it no longer depends
on how many records the ClientHello happens to fragment into.
Assisted-by: Claude:claude-sonnet-5
Reviewed-by: Richard Levitte <levitte@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Merge-date: Wed Sep 9 07:14:50 2026
Merged-from: https://github.com/openssl/openssl/pull/32746
diff --git a/test/dtlsv1listentest.c b/test/dtlsv1listentest.c
index 36106ef541..2f103ecfa3 100644
--- a/test/dtlsv1listentest.c
+++ b/test/dtlsv1listentest.c
@@ -15,6 +15,7 @@
#include <openssl/conf.h>
#include "internal/nelem.h"
#include "internal/ssl_unwrap.h"
+#include "internal/time.h"
#include "../ssl/ssl_local.h"
#include "helpers/ssltestlib.h"
#include "testutil.h"
@@ -621,10 +622,11 @@ err:
#ifndef OPENSSL_NO_DTLS1_2
static unsigned char *create_cookie_clienthello(int *outlen,
- const unsigned char *seq)
+ unsigned char *out_seq)
{
SSL_CTX *ctx = NULL;
SSL *ssl = NULL;
+ SSL_CONNECTION *sc = NULL;
BIO *rbio = NULL, *wbio = NULL;
BIO *ssl_rbio = NULL, *ssl_wbio = NULL;
char *data = NULL;
@@ -645,9 +647,28 @@ static unsigned char *create_cookie_clienthello(int *outlen,
rbio = wbio = NULL;
SSL_set_connect_state(ssl);
+ if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(ssl)))
+ goto err;
+
+ /* Send the initial, cookie-less ClientHello: record sequence 0. */
if (!TEST_int_le(sslret = SSL_connect(ssl), 0)
- || !TEST_int_eq(SSL_get_error(ssl, sslret), SSL_ERROR_WANT_READ)
- || !TEST_int_gt(BIO_reset(ssl_wbio), 0)
+ || !TEST_int_eq(SSL_get_error(ssl, sslret), SSL_ERROR_WANT_READ))
+ goto err;
+
+ /*
+ * Simulate the initial ClientHello (or the server's HelloVerifyRequest)
+ * being lost in transit: force the retransmit timer to look expired and
+ * drive a genuine retransmission of the buffered ClientHello through
+ * the normal write path. This consumes record sequence 1 for real,
+ * so the retried (with-cookie) ClientHello below naturally lands on
+ * sequence 2 -- rather than the test asserting that value by fiat.
+ */
+ sc->d1->next_timeout = ossl_ms2time(1);
+ if (!TEST_long_eq(DTLSv1_handle_timeout(ssl), 1))
+ goto err;
+
+ /* Neither copy of the cookie-less ClientHello is needed -- discard both. */
+ if (!TEST_int_gt(BIO_reset(ssl_wbio), 0)
|| !TEST_int_eq(BIO_write(ssl_rbio, verify, sizeof(verify)),
sizeof(verify))
|| !TEST_int_le(sslret = SSL_connect(ssl), 0)
@@ -662,8 +683,14 @@ static unsigned char *create_cookie_clienthello(int *outlen,
*outlen = (int)datalen;
- /* DTLS record header bytes 3..10 are epoch and sequence number. */
- memcpy(ret + 3, seq, DTLS_RECORD_EPOCH_AND_SEQ_LEN);
+ /*
+ * Report back the epoch+sequence number (DTLS record header bytes
+ * 3..10) that the client's own record layer actually assigned to the
+ * first record of the retried ClientHello. The caller uses this to
+ * work out what it should expect back from the server, instead of the
+ * test dictating a fixed sequence number.
+ */
+ memcpy(out_seq, ret + 3, DTLS_RECORD_EPOCH_AND_SEQ_LEN);
err:
SSL_free(ssl);
@@ -675,13 +702,7 @@ err:
static int dtls_listen_write_seq_test(int tst)
{
- static const unsigned char initial_seq[DTLS_RECORD_EPOCH_AND_SEQ_LEN] = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02
- };
- static const unsigned char expected_record_seq[2 + DTLS_RECORD_EPOCH_AND_SEQ_LEN] = {
- 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x02
- };
+ unsigned char actual_seq[DTLS_RECORD_EPOCH_AND_SEQ_LEN];
unsigned char *inbuf = NULL;
int inbuflen = 0;
SSL_CONNECTION *s = NULL;
@@ -694,7 +715,7 @@ static int dtls_listen_write_seq_test(int tst)
long datalen;
int ret, success = 0;
- if (!TEST_ptr(inbuf = create_cookie_clienthello(&inbuflen, initial_seq)))
+ if (!TEST_ptr(inbuf = create_cookie_clienthello(&inbuflen, actual_seq)))
goto err;
if (!TEST_ptr(ctx = SSL_CTX_new(DTLS_server_method()))
@@ -753,8 +774,18 @@ static int dtls_listen_write_seq_test(int tst)
datalen = BIO_get_mem_data(outbio, &data);
if (!TEST_long_ge(datalen, DTLS1_RT_HEADER_LENGTH)
- || !TEST_mem_eq(data + 1, sizeof(expected_record_seq),
- expected_record_seq, sizeof(expected_record_seq)))
+ /* The server's response record is always DTLS1.2 once a cookie has
+ * been validated -- that's what DTLSv1_listen() negotiates down to. */
+ || !TEST_uint_eq(((unsigned char)data[1] << 8) | (unsigned char)data[2],
+ DTLS1_2_VERSION)
+ /*
+ * This is the actual regression check: the server's write sequence
+ * must continue from the epoch/sequence number of the ClientHello
+ * record DTLSv1_listen() validated the cookie against -- whatever
+ * that value turned out to be, not one the test dictates.
+ */
+ || !TEST_mem_eq(data + 3, DTLS_RECORD_EPOCH_AND_SEQ_LEN,
+ actual_seq, DTLS_RECORD_EPOCH_AND_SEQ_LEN))
goto err;
SSL_set0_rbio(ssl, NULL);