Commit d94c6a253a for openssl.org
commit d94c6a253a12674972c4620ce6ec34cdb12bafa1
Author: Ryan Hooper <ryanh@openssl.foundation>
Date: Mon Oct 19 13:30:02 2026 -0400
DTLS 1.3: Fix WANT_READ while awaiting ServerHello
After HelloRetryRequest, buffering records from a later epoch can
request a retry without reading the BIO. Supply missing read-retry
flags so SSL_get_error() reports SSL_ERROR_WANT_READ rather than
SSL_ERROR_SYSCALL. Preserve existing retry conditions and handle
listener connections without a read BIO.
Add regression tests for delayed ServerHello delivery and permanent
loss followed by timeout-driven retransmission. Check WANT_READ and
an empty error queue after every retry, then verify handshake
completion and bidirectional application data.
Fixes: #33241
Assisted-by: Claude Code:claude-sonnet-5-5
Assisted-by: OpenCode:github-copilot/gpt-6-astra
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Merge-date: Thu Oct 22 06:08:35 2026
Merged-from: https://github.com/openssl/openssl/pull/33268
diff --git a/ssl/record/rec_layer_d1.c b/ssl/record/rec_layer_d1.c
index 1c9ae2e138..c99269c702 100644
--- a/ssl/record/rec_layer_d1.c
+++ b/ssl/record/rec_layer_d1.c
@@ -323,10 +323,31 @@ start:
* Anything other than a timeout is an error. SSLfatal() already
* called if appropriate.
*/
- if (ret <= 0)
- return ret;
- else
+ if (ret > 0)
goto start;
+
+ /*
+ * Once the record layer holds buffered records from a later
+ * epoch it asks for a retry without reading the BIO again (see
+ * dtls_get_more_records()), so nothing has set the BIO's retry
+ * flags. Without them SSL_get_error() reports
+ * SSL_ERROR_SYSCALL for what is an ordinary wait for more
+ * data.
+ *
+ * A connection accepted by a DTLS listener has no read BIO of
+ * its own (see dtls_listener_create_conn_ssl());
+ * SSL_get_error() already reports SSL_ERROR_WANT_READ for
+ * SSL_READING on such a connection without needing one.
+ */
+ if (sc->rwstate == SSL_READING) {
+ BIO *rbio = SSL_get_rbio(s);
+
+ if (rbio != NULL && !BIO_should_retry(rbio)) {
+ BIO_clear_retry_flags(rbio);
+ BIO_set_retry_read(rbio);
+ }
+ }
+ return ret;
}
rr->off = 0;
sc->rlayer.num_recs++;
diff --git a/test/dtls13_internal_test.c b/test/dtls13_internal_test.c
index d7e787750a..bfb88fd44b 100644
--- a/test/dtls13_internal_test.c
+++ b/test/dtls13_internal_test.c
@@ -2318,6 +2318,188 @@ end:
return testresult;
}
+static const unsigned char dummy_cookie[] = "cookie";
+
+static int generate_cookie_cb(SSL *ssl, unsigned char *cookie,
+ unsigned int *cookie_len)
+{
+ memcpy(cookie, dummy_cookie, sizeof(dummy_cookie));
+ *cookie_len = sizeof(dummy_cookie);
+ return 1;
+}
+
+static int verify_cookie_cb(SSL *ssl, const unsigned char *cookie,
+ unsigned int cookie_len)
+{
+ return TEST_mem_eq(cookie, cookie_len, dummy_cookie, sizeof(dummy_cookie));
+}
+
+/*
+ * Drain the available datagrams with SSL_connect(), requiring WANT_READ and
+ * an empty error queue after every call. Return 0 on an unexpected result or
+ * if the bounded loop cannot drain the read BIO, and 1 otherwise.
+ */
+static int connect_while_readable(SSL *ssl, BIO *rbio)
+{
+ int i, ret;
+
+ for (i = 0; i < 32; i++) {
+ ERR_clear_error();
+ ret = SSL_connect(ssl);
+ if (!TEST_int_eq(SSL_get_error(ssl, ret), SSL_ERROR_WANT_READ)
+ || !TEST_ulong_eq(ERR_peek_error(), 0))
+ return 0;
+ if (BIO_ctrl_pending(rbio) == 0)
+ return 1;
+ }
+ return TEST_size_t_eq(BIO_ctrl_pending(rbio), 0);
+}
+
+/*
+ * After a HelloRetryRequest the client's record layer already expects DTLS
+ * 1.3 but is still in epoch 0. If the datagrams with the ServerHello are lost,
+ * or arrive after ones holding only epoch 2 records, the client has to buffer
+ * those records until the ServerHello turns up. That is an ordinary wait for
+ * more data and must be reported as SSL_ERROR_WANT_READ, not as
+ * SSL_ERROR_SYSCALL, which applications must treat as fatal.
+ *
+ * The server requires a cookie, so it answers the first ClientHello with a
+ * HelloRetryRequest. Its small MTU spreads the ServerHello flight over several
+ * datagrams. The ServerHello datagrams are either delivered after the
+ * encrypted ones (idx == 0), or discarded permanently so that recovery
+ * requires a retransmitted flight (idx == 1).
+ */
+static int test_dtls13_hrr_lost_server_hello(int idx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ BIO *bio;
+ unsigned char dg[16][1500], buf[2048];
+ int dglen[16];
+ int ndg = 0, first_enc, i, ret, testresult = 0;
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+ DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey))
+ || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0)))
+ goto end;
+ SSL_CTX_set_options(sctx, SSL_OP_COOKIE_EXCHANGE);
+ SSL_CTX_set_cookie_generate_cb(sctx, generate_cookie_cb);
+ SSL_CTX_set_cookie_verify_cb(sctx, verify_cookie_cb);
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+ NULL, NULL)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ bio = SSL_get_rbio(client);
+
+ SSL_set_options(server, SSL_OP_NO_QUERY_MTU);
+ if (!TEST_long_gt(SSL_set_mtu(server, 400), 0))
+ goto end;
+
+ /* ClientHello, then HelloRetryRequest, then the second ClientHello. */
+ ret = SSL_connect(client);
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ ret = SSL_accept(server);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ ret = SSL_connect(client);
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ /* The server answers with its ServerHello flight. */
+ ret = SSL_accept(server);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_int_eq(sc->hello_retry_request, SSL_HRR_COMPLETE))
+ goto end;
+
+ /* Capture the flight, delivering none of it yet. */
+ while ((ret = BIO_read(bio, dg[ndg], sizeof(dg[0]))) > 0) {
+ dglen[ndg++] = ret;
+ if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg)))
+ goto end;
+ }
+ if (!TEST_int_ge(ndg, 2))
+ goto end;
+
+ /*
+ * The ServerHello is sent in the clear, in epoch 0 (a legacy record header
+ * starting 0x16), and can itself span several datagrams. Everything after
+ * it holds only encrypted epoch 2 records.
+ */
+ for (first_enc = 0; first_enc < ndg && dg[first_enc][0] == 0x16; first_enc++)
+ continue;
+ if (!TEST_int_gt(first_enc, 0) || !TEST_int_lt(first_enc, ndg))
+ goto end;
+ for (i = first_enc; i < ndg; i++)
+ if (!TEST_int_eq(dg[i][0] & 0xe0, 0x20))
+ goto end;
+
+ /* Deliver only the encrypted datagrams from the original flight. */
+ for (i = first_enc; i < ndg; i++)
+ if (!TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+ dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
+ dglen[i]))
+ goto end;
+
+ /*
+ * The client can only buffer them: it has to wait for more data. Each
+ * call handles one datagram, so keep calling while some are waiting, as an
+ * application does while its socket is readable.
+ */
+ if (!connect_while_readable(client, bio))
+ goto end;
+
+ if (idx == 0) {
+ /* Deliver the original ServerHello datagrams late. */
+ for (i = 0; i < first_enc; i++)
+ if (!TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+ dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
+ dglen[i]))
+ goto end;
+ } else {
+ /*
+ * The original ServerHello datagrams remain lost. Expire the server's
+ * timer and deliver its entire retransmitted flight, including any
+ * duplicates of the encrypted handshake messages buffered above.
+ */
+ if (!TEST_size_t_gt(pqueue_size(&sc->d1->sent_messages), 0)
+ || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+ sc->d1->next_timeout = ossl_time_subtract(ossl_time_now(),
+ ossl_seconds2time(1));
+ if (!TEST_int_gt(DTLSv1_handle_timeout(server), 0)
+ || !TEST_size_t_gt(BIO_ctrl_pending(bio), 0)
+ || !TEST_ulong_eq(ERR_peek_error(), 0))
+ goto end;
+ }
+ if (!connect_while_readable(client, bio)
+ || !TEST_int_eq(SSL_accept(server), 1))
+ goto end;
+ ret = SSL_read(client, buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+ || !TEST_int_eq(SSL_get_state(client), TLS_ST_OK))
+ goto end;
+
+ /* Application data flows both ways. */
+ if (!TEST_int_eq(SSL_write(server, "s", 1), 1)
+ || !TEST_int_eq(SSL_read(client, buf, 1), 1)
+ || !TEST_uchar_eq(buf[0], 's')
+ || !TEST_int_eq(SSL_write(client, "c", 1), 1)
+ || !TEST_int_eq(SSL_read(server, buf, 1), 1)
+ || !TEST_uchar_eq(buf[0], 'c'))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
#endif /* OPENSSL_NO_DTLS1_3 */
int setup_tests(void)
@@ -2350,6 +2532,7 @@ int setup_tests(void)
ADD_TEST(test_dtls13_retained_epoch_seq_match);
ADD_TEST(test_dtls13_retained_epoch_app_data_rejected);
ADD_TEST(test_dtls13_retained_epoch_ack_authority);
+ ADD_ALL_TESTS(test_dtls13_hrr_lost_server_hello, 2);
#endif
return 1;
}