From 6595ade8856d5ba8455449ec5b104882dc7dbe05 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Thu, 5 Nov 2026 17:01:54 +0900 Subject: [PATCH 1/3] internal: count a queued write that another call flushed - SendData() counts the bytes of a record left by an earlier blocked write whenever outputBuffer is empty, whether this call or another call flushed it. - SendData() groups early data with the ClientHello only when no queued write credit is pending, so a retried write flushes its record and takes the credit. - wolfSSL_clear() resets prevSent and plainSz. - Add test_wolfSSL_write_retry_flushed_by_update_keys, test_wolfSSL_clear_write_pending and test_tls13_early_data_group_write_retry. --- src/internal.c | 27 ++++++---- src/ssl.c | 2 + tests/api/test_ssl_rw.c | 115 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_rw.h | 5 ++ tests/api/test_tls13.c | 86 ++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 2 + 6 files changed, 226 insertions(+), 11 deletions(-) diff --git a/src/internal.c b/src/internal.c index 57eaf3f9cfe..7c4d60a51a4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -31318,7 +31318,10 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) return WOLFSSL_FATAL_ERROR; } #ifdef WOLFSSL_EARLY_DATA_GROUP - groupMsgs = 1; + /* A retry of a blocked write flushes its record instead. */ + if (ssl->buffers.prevSent == 0 && ssl->buffers.plainSz == 0) { + groupMsgs = 1; + } #endif } else @@ -31374,17 +31377,19 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) } return (ssl->error = error); } - else { - /* advance sent to previous sent + plain size just sent */ - sent = ssl->buffers.prevSent + ssl->buffers.plainSz; - ssl->buffers.prevSent = 0; - ssl->buffers.plainSz = 0; - WOLFSSL_MSG("sent write buffered data"); + } + /* The record the last write queued has been sent, by the flush above or + * by another call such as wolfSSL_negotiate(). */ + if (ssl->buffers.outputBuffer.length == 0 && + (ssl->buffers.prevSent != 0 || ssl->buffers.plainSz != 0)) { + sent = ssl->buffers.prevSent + ssl->buffers.plainSz; + ssl->buffers.prevSent = 0; + ssl->buffers.plainSz = 0; + WOLFSSL_MSG("sent write buffered data"); - if (sent > (word32)sz) { - WOLFSSL_MSG("error: write() after WANT_WRITE with short size"); - return (ssl->error = BAD_FUNC_ARG); - } + if (sent > (word32)sz) { + WOLFSSL_MSG("error: write() after WANT_WRITE with short size"); + return (ssl->error = BAD_FUNC_ARG); } } diff --git a/src/ssl.c b/src/ssl.c index 3773badb1d9..2507812a69c 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -6040,6 +6040,8 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->buffers.clearOutputBuffer.length = 0; ssl->buffers.inputBuffer.idx = 0; ssl->buffers.inputBuffer.length = 0; + ssl->buffers.prevSent = 0; + ssl->buffers.plainSz = 0; /* Recycling the object for a new connection must not carry the * previous connection's key material along with it. A freshly * created object has all of this zeroed. */ diff --git a/tests/api/test_ssl_rw.c b/tests/api/test_ssl_rw.c index 4c992da4f74..60a79a640dd 100644 --- a/tests/api/test_ssl_rw.c +++ b/tests/api/test_ssl_rw.c @@ -875,6 +875,121 @@ int test_wolfSSL_clear_app_data_pending(void) return EXPECT_RESULT(); } +/* Test that a retried write does not send again the record that + * wolfSSL_update_keys() flushed ahead of its KeyUpdate. */ +int test_wolfSSL_write_retry_flushed_by_update_keys(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TLS13) \ + && !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + const char msg[] = "queued while blocked"; + char reply[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + test_memio_simulate_want_write(&test_ctx, 1, 1); + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + test_memio_simulate_want_write(&test_ctx, 1, 0); + + /* The KeyUpdate goes out with the queued record ahead of it. */ + ExpectIntEQ(wolfSSL_update_keys(ssl_c), WOLFSSL_SUCCESS); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->buffers.outputBuffer.length, 0); + } + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), (int)sizeof(msg)); + + /* One copy only. */ + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectBufEQ(reply, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Test that a write left blocked on one connection does not change the first + * write after wolfSSL_clear() reuses the object. */ +int test_wolfSSL_clear_write_pending(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_TLS) \ + && !defined(WOLFSSL_NO_TLS12) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + const char msg1[] = "blocked"; + const char msg2[] = "first write on the new connection"; + char reply[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The write blocks, then the shutdown flushes its record. */ + test_memio_simulate_want_write(&test_ctx, 1, 1); + ExpectIntEQ(wolfSSL_write(ssl_c, msg1, (int)sizeof(msg1)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + test_memio_simulate_want_write(&test_ctx, 1, 0); + ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SHUTDOWN_NOT_DONE); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->buffers.outputBuffer.length, 0); + } + + /* Reuse the client object for a new connection to a new server. */ + ExpectIntEQ(wolfSSL_clear(ssl_c), WOLFSSL_SUCCESS); + wolfSSL_free(ssl_s); + ssl_s = NULL; + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); +#ifndef NO_DH + SetDH(ssl_s); +#endif + test_memio_clear_buffer(&test_ctx, 0); + test_memio_clear_buffer(&test_ctx, 1); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The whole of the new write arrives. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg2, (int)sizeof(msg2)), + (int)sizeof(msg2)); + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + (int)sizeof(msg2)); + ExpectBufEQ(reply, msg2, sizeof(msg2)); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + /* Test that a renegotiation does not read input while decrypted application * data is still waiting to be read. * diff --git a/tests/api/test_ssl_rw.h b/tests/api/test_ssl_rw.h index 822863c4515..4619cbbf2d8 100644 --- a/tests/api/test_ssl_rw.h +++ b/tests/api/test_ssl_rw.h @@ -35,6 +35,8 @@ int test_wolfSSL_write_ex_partial(void); int test_wolfSSL_inject_app_data_ready(void); int test_wolfSSL_inject_app_data_partial_record(void); int test_wolfSSL_clear_app_data_pending(void); +int test_wolfSSL_write_retry_flushed_by_update_keys(void); +int test_wolfSSL_clear_write_pending(void); int test_wolfSSL_rehandshake_app_data_pending(void); int test_wolfSSL_rehandshake_app_data_partial_record(void); int test_wolfSSL_accept_early_data_pending(void); @@ -65,6 +67,9 @@ int test_wolfSSL_write_dup_max_fragment(void); TEST_DECL_GROUP("ssl_rw", \ test_wolfSSL_inject_app_data_partial_record), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_clear_app_data_pending), \ + TEST_DECL_GROUP("ssl_rw", \ + test_wolfSSL_write_retry_flushed_by_update_keys), \ + TEST_DECL_GROUP("ssl_rw", test_wolfSSL_clear_write_pending), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_rehandshake_app_data_pending), \ TEST_DECL_GROUP("ssl_rw", \ test_wolfSSL_rehandshake_app_data_partial_record), \ diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index ff904eedc4b..441a1fc4bef 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -6562,6 +6562,92 @@ int test_tls13_early_data(void) return EXPECT_RESULT(); } +/* Early data grouped with the ClientHello blocks. The retry sends the queued + * record once, and the next write sends all of its data. */ +int test_tls13_early_data_group_write_retry(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(WOLFSSL_EARLY_DATA_GROUP) && \ + defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + const char msg[] = "grouped with the ClientHello"; + const char msg2[] = "first write after the handshake"; + char buf[64]; + int rd = 0; + int written = 0; + + /* Full handshake for a ticket that allows early data. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_CTX_set_max_early_data(ctx_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + /* The ClientHello and the early data record block together. */ + test_memio_simulate_want_write(&test_ctx, 1, 1); + ExpectIntEQ(wolfSSL_write_early_data(ssl_c, msg, (int)sizeof(msg), + &written), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + test_memio_simulate_want_write(&test_ctx, 1, 0); + ExpectIntEQ(wolfSSL_write_early_data(ssl_c, msg, (int)sizeof(msg), + &written), (int)sizeof(msg)); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->buffers.prevSent, 0); + ExpectIntEQ(ssl_c->buffers.plainSz, 0); + } + + /* One copy of the early data only. */ + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(test_tls13_early_data_read_until_write_ok(ssl_s, buf, + sizeof(buf), &rd), (int)sizeof(msg)); + ExpectBufEQ(buf, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_tls13_early_data_read_until_write_ok(ssl_s, buf, + sizeof(buf), &rd), 0); + ExpectIntEQ(wolfSSL_get_early_data_status(ssl_c), + WOLFSSL_EARLY_DATA_ACCEPTED); + + /* The whole of the next write arrives. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg2, (int)sizeof(msg2)), + (int)sizeof(msg2)); + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), (int)sizeof(msg2)); + ExpectBufEQ(buf, msg2, sizeof(msg2)); + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + #if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 1cb5e79efa1..5d97739a398 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -49,6 +49,7 @@ int test_tls13_pq_groups(void); int test_tls13_multi_pqc_key_share(void); int test_tls13_mlkem_bad_key_share(void); int test_tls13_early_data(void); +int test_tls13_early_data_group_write_retry(void); int test_tls13_same_ch(void); int test_tls13_hrr_different_cs(void); int test_tls13_ch2_different_cs(void); @@ -239,6 +240,7 @@ int test_tls13_exporter_suite_decision_coverage(void); TEST_DECL_GROUP("tls13", test_tls13_multi_pqc_key_share), \ TEST_DECL_GROUP("tls13", test_tls13_mlkem_bad_key_share), \ TEST_DECL_GROUP("tls13", test_tls13_early_data), \ + TEST_DECL_GROUP("tls13", test_tls13_early_data_group_write_retry), \ TEST_DECL_GROUP("tls13", test_tls13_same_ch), \ TEST_DECL_GROUP("tls13", test_tls13_hrr_different_cs), \ TEST_DECL_GROUP("tls13", test_tls13_ch2_different_cs), \ From bbf41ce4c9e064a5ec6cdbf895d0e8613ce165ac Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Thu, 5 Nov 2026 21:21:04 +0900 Subject: [PATCH 2/3] tls13: drop a blocked write credit when early data is rejected - DoTls13EncryptedExtensions() and SendTls13ClientHello() clear prevSent and plainSz where the client sets earlyData to no_early_data on a rejection, so a retried wolfSSL_write() sends all of its data. - Add test_tls13_early_data_write_retry. --- src/tls13.c | 7 ++- tests/api/test_tls13.c | 136 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 2 + 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/tls13.c b/src/tls13.c index 57f09e0bac1..708dc044cbb 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -5196,8 +5196,11 @@ int SendTls13ClientHello(WOLFSSL* ssl) #ifdef WOLFSSL_EARLY_DATA if (!EarlyDataPossible(ssl)) ssl->earlyData = no_early_data; - if (ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE) + if (ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE) { ssl->earlyData = no_early_data; + ssl->buffers.prevSent = 0; + ssl->buffers.plainSz = 0; + } if (ssl->earlyData == no_early_data) TLSX_Remove(&ssl->extensions, TLSX_EARLY_DATA, ssl->heap); if (ssl->earlyData != no_early_data && @@ -6601,6 +6604,8 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input, WOLFSSL_MSG("Early data rejected by server (no early_data " "EncryptedExtensions response)"); ssl->earlyData = no_early_data; + ssl->buffers.prevSent = 0; + ssl->buffers.plainSz = 0; } } diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 441a1fc4bef..b7c316b41ff 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9955,6 +9955,142 @@ int test_tls13_middlebox_compat_client_ccs_retry_early_data(void) return EXPECT_RESULT(); } +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* Resume with sess and leave a wolfSSL_write() of msg, sent as early data, + * blocked with WANT_WRITE. A wolfSSL_connect() then flushes its record. */ +static int test_tls13_early_data_write_blocked(struct test_memio_ctx* test_ctx, + WOLFSSL_CTX** ctx_c, WOLFSSL_CTX** ctx_s, WOLFSSL** ssl_c, WOLFSSL** ssl_s, + WOLFSSL_SESSION* sess, int noKeyShares, const char* msg, int msgSz) +{ + EXPECT_DECLS; + int written = 0; + + XMEMSET(test_ctx, 0, sizeof(*test_ctx)); + ExpectIntEQ(test_memio_setup(test_ctx, ctx_c, ctx_s, ssl_c, ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_set_max_early_data(*ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(wolfSSL_set_session(*ssl_c, sess), WOLFSSL_SUCCESS); + if (noKeyShares) { + ExpectIntEQ(wolfSSL_NoKeyShares(*ssl_c), WOLFSSL_SUCCESS); + } + + ExpectIntEQ(test_tls13_early_data_write_until_write_ok(*ssl_c, "x", 1, + &written), 1); + test_memio_simulate_want_write(test_ctx, 1, 1); + ExpectIntEQ(wolfSSL_write(*ssl_c, msg, msgSz), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(*ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + test_memio_simulate_want_write(test_ctx, 1, 0); + + ExpectIntEQ(wolfSSL_connect(*ssl_c), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(*ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + return EXPECT_RESULT(); +} +#endif + +/* A wolfSSL_write() sent as early data blocks, and wolfSSL_connect() flushes + * its record. The retry sends nothing new when the server accepted early + * data, and sends all of msg again when the server rejected it. */ +int test_tls13_early_data_write_retry(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EARLY_DATA) && \ + defined(HAVE_SESSION_TICKET) && !defined(NO_SESSION_CACHE) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + const char msg[] = "blocked during early data"; + char buf[64]; + int rd = 0; + int hrr; + + /* Full handshake for a ticket that allows early data. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntGE(wolfSSL_CTX_set_max_early_data(ctx_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntGE(wolfSSL_set_max_early_data(ssl_s, MAX_EARLY_DATA_SZ), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* Accepted: the server got msg as early data, so the retry is done. */ + ExpectIntEQ(test_tls13_early_data_write_blocked(&test_ctx, &ctx_c, &ctx_s, + &ssl_c, &ssl_s, sess, 0, msg, (int)sizeof(msg)), TEST_SUCCESS); + ExpectIntEQ(test_tls13_early_data_read_until_write_ok(ssl_s, buf, + sizeof(buf), &rd), 1); + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(test_tls13_early_data_read_until_write_ok(ssl_s, buf, + sizeof(buf), &rd), (int)sizeof(msg)); + ExpectBufEQ(buf, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(test_tls13_early_data_read_until_write_ok(ssl_s, buf, + sizeof(buf), &rd), 0); + ExpectIntEQ(wolfSSL_get_early_data_status(ssl_c), + WOLFSSL_EARLY_DATA_ACCEPTED); + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), (int)sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* Rejected: the ticket's 0-RTT was used above, then by a + * HelloRetryRequest. The server dropped msg, so the retry resends it. */ + for (hrr = 0; hrr < 2 && EXPECT_SUCCESS(); hrr++) { + ExpectIntEQ(test_tls13_early_data_write_blocked(&test_ctx, &ctx_c, + &ctx_s, &ssl_c, &ssl_s, sess, hrr, msg, (int)sizeof(msg)), + TEST_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->options.hrrSentKeyShare, hrr); + } + ExpectIntEQ(wolfSSL_get_early_data_status(ssl_c), + WOLFSSL_EARLY_DATA_REJECTED); + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + (int)sizeof(msg)); + XMEMSET(buf, 0, sizeof(buf)); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), (int)sizeof(msg)); + ExpectBufEQ(buf, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + } + + wolfSSL_SESSION_free(sess); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + int test_tls13_plaintext_alert(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls13.h b/tests/api/test_tls13.h index 5d97739a398..29eef033cab 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -75,6 +75,7 @@ int test_tls13_middlebox_compat_server_ccs_retry(void); int test_tls13_middlebox_compat_hrr_ccs_retry(void); int test_tls13_middlebox_compat_client_ccs_retry(void); int test_tls13_middlebox_compat_client_ccs_retry_early_data(void); +int test_tls13_early_data_write_retry(void); int test_tls13_plaintext_alert(void); int test_tls13_warning_alert_is_fatal(void); int test_tls13_unknown_ext_rejected(void); @@ -267,6 +268,7 @@ int test_tls13_exporter_suite_decision_coverage(void); TEST_DECL_GROUP("tls13", test_tls13_middlebox_compat_client_ccs_retry), \ TEST_DECL_GROUP("tls13", \ test_tls13_middlebox_compat_client_ccs_retry_early_data), \ + TEST_DECL_GROUP("tls13", test_tls13_early_data_write_retry), \ TEST_DECL_GROUP("tls13", test_tls13_plaintext_alert), \ TEST_DECL_GROUP("tls13", test_tls13_warning_alert_is_fatal), \ TEST_DECL_GROUP("tls13", test_tls13_cert_req_sigalgs), \ From 677042d5eb82848fcf00d11af9ba41f3473b0366 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Fri, 6 Nov 2026 09:37:33 +0900 Subject: [PATCH 3/3] internal: apply a queued write credit after the KeyUpdate and alert sends - SendData() moves the queued write credit after the deferred KeyUpdate send, RetrySendAlert() and the IsWriteClosed() check, so a WANT_WRITE from any of them returns with prevSent and plainSz still set. - Add test_wolfSSL_write_retry_keyupdate_blocked and test_wolfSSL_write_retry_alert_blocked. --- src/internal.c | 35 ++++----- tests/api/test_ssl_rw.c | 155 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_rw.h | 4 ++ 3 files changed, 177 insertions(+), 17 deletions(-) diff --git a/src/internal.c b/src/internal.c index 7c4d60a51a4..2d141b2975c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -31378,20 +31378,6 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) return (ssl->error = error); } } - /* The record the last write queued has been sent, by the flush above or - * by another call such as wolfSSL_negotiate(). */ - if (ssl->buffers.outputBuffer.length == 0 && - (ssl->buffers.prevSent != 0 || ssl->buffers.plainSz != 0)) { - sent = ssl->buffers.prevSent + ssl->buffers.plainSz; - ssl->buffers.prevSent = 0; - ssl->buffers.plainSz = 0; - WOLFSSL_MSG("sent write buffered data"); - - if (sent > (word32)sz) { - WOLFSSL_MSG("error: write() after WANT_WRITE with short size"); - return (ssl->error = BAD_FUNC_ARG); - } - } ret = RetrySendAlert(ssl); if (ret != 0) { @@ -31421,9 +31407,8 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) #ifdef WOLFSSL_TLS13 /* A KeyUpdate response the read side left for the write side to send. It - * goes out ahead of the application data, and only once the output buffer - * is flushed, so that the record of an earlier call is accounted for above - * instead of being sent from here and then built again. */ + * goes out ahead of the application data, once the output buffer is + * flushed. */ if (ssl->options.sendKeyUpdate) { /* RFC 9846 Section 4.7.3: at the key update limit, ignore the * request rather than fail the write. */ @@ -31454,6 +31439,22 @@ int SendData(WOLFSSL* ssl, const void* data, size_t sz) } #endif + /* The record the last write queued has been sent, by this call's flush or + * by another call such as wolfSSL_negotiate(). Taken only after the early + * returns, so a WANT_WRITE from the KeyUpdate or alert keeps the credit. */ + if (ssl->buffers.outputBuffer.length == 0 && + (ssl->buffers.prevSent != 0 || ssl->buffers.plainSz != 0)) { + sent = ssl->buffers.prevSent + ssl->buffers.plainSz; + ssl->buffers.prevSent = 0; + ssl->buffers.plainSz = 0; + WOLFSSL_MSG("sent write buffered data"); + + if (sent > (word32)sz) { + WOLFSSL_MSG("error: write() after WANT_WRITE with short size"); + return (ssl->error = BAD_FUNC_ARG); + } + } + #ifdef WOLFSSL_THREADED_CRYPT ret = SendAsyncData(ssl); if (ret != 0) { diff --git a/tests/api/test_ssl_rw.c b/tests/api/test_ssl_rw.c index 60a79a640dd..a4edc2959f8 100644 --- a/tests/api/test_ssl_rw.c +++ b/tests/api/test_ssl_rw.c @@ -990,6 +990,161 @@ int test_wolfSSL_clear_write_pending(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +struct test_write_retry_block_ctx { + struct test_memio_ctx* test_ctx; + int calls; +}; + +/* Return WANT_WRITE on the first and third send, pass the rest to memio. */ +static int test_write_retry_block_send_cb(WOLFSSL* ssl, char* data, int sz, + void* ctx) +{ + struct test_write_retry_block_ctx* wctx = + (struct test_write_retry_block_ctx*)ctx; + + wctx->calls++; + if (wctx->calls == 1 || wctx->calls == 3) { + return WOLFSSL_CBIO_ERR_WANT_WRITE; + } + return test_memio_write_cb(ssl, data, sz, wctx->test_ctx); +} +#endif + +/* Test that a retried write does not send again a record that went out before + * the deferred KeyUpdate reply blocked. */ +int test_wolfSSL_write_retry_keyupdate_blocked(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + struct test_write_retry_block_ctx wctx; + const char msg[] = "sent before the KeyUpdate reply"; + char reply[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&wctx, 0, sizeof(wctx)); + wctx.test_ctx = &test_ctx; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + wolfSSL_SSLSetIOSend(ssl_c, test_write_retry_block_send_cb); + wolfSSL_SetIOWriteCtx(ssl_c, &wctx); + + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + + /* The read leaves the KeyUpdate reply to the next write. */ + ExpectIntEQ(wolfSSL_update_keys(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_read(ssl_c, reply, (int)sizeof(reply)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->options.sendKeyUpdate, 1); + } + + /* The record goes out, then the KeyUpdate reply blocks. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), (int)sizeof(msg)); + /* Record, record, KeyUpdate, KeyUpdate: no new record. */ + ExpectIntEQ(wctx.calls, 4); + + /* One copy only. */ + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectBufEQ(reply, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Test that a retried write does not send again a record that went out before + * a pending alert blocked. */ +int test_wolfSSL_write_retry_alert_blocked(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + struct test_write_retry_block_ctx wctx; + const char msg[] = "sent before the pending alert"; + char reply[64]; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&wctx, 0, sizeof(wctx)); + wctx.test_ctx = &test_ctx; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + wolfSSL_SSLSetIOSend(ssl_c, test_write_retry_block_send_cb); + wolfSSL_SetIOWriteCtx(ssl_c, &wctx); + + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + + /* A warning alert that leaves the connection open is waiting to go. */ + if (ssl_c != NULL) { + ssl_c->pendingAlert.code = user_canceled; + ssl_c->pendingAlert.level = alert_warning; + } + + /* The record goes out, then the alert blocks. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_WRITE); + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)sizeof(msg)), (int)sizeof(msg)); + /* Record, record, alert, alert: no new record. */ + ExpectIntEQ(wctx.calls, 4); + + /* One copy only. */ + XMEMSET(reply, 0, sizeof(reply)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + (int)sizeof(msg)); + ExpectBufEQ(reply, msg, sizeof(msg)); + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), + WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + /* Test that a renegotiation does not read input while decrypted application * data is still waiting to be read. * diff --git a/tests/api/test_ssl_rw.h b/tests/api/test_ssl_rw.h index 4619cbbf2d8..c9ccd712b31 100644 --- a/tests/api/test_ssl_rw.h +++ b/tests/api/test_ssl_rw.h @@ -37,6 +37,8 @@ int test_wolfSSL_inject_app_data_partial_record(void); int test_wolfSSL_clear_app_data_pending(void); int test_wolfSSL_write_retry_flushed_by_update_keys(void); int test_wolfSSL_clear_write_pending(void); +int test_wolfSSL_write_retry_keyupdate_blocked(void); +int test_wolfSSL_write_retry_alert_blocked(void); int test_wolfSSL_rehandshake_app_data_pending(void); int test_wolfSSL_rehandshake_app_data_partial_record(void); int test_wolfSSL_accept_early_data_pending(void); @@ -70,6 +72,8 @@ int test_wolfSSL_write_dup_max_fragment(void); TEST_DECL_GROUP("ssl_rw", \ test_wolfSSL_write_retry_flushed_by_update_keys), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_clear_write_pending), \ + TEST_DECL_GROUP("ssl_rw", test_wolfSSL_write_retry_keyupdate_blocked), \ + TEST_DECL_GROUP("ssl_rw", test_wolfSSL_write_retry_alert_blocked), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_rehandshake_app_data_pending), \ TEST_DECL_GROUP("ssl_rw", \ test_wolfSSL_rehandshake_app_data_partial_record), \