From 3c400855fdfae8fea3f34c51edc84a9150a40279 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Thu, 20 Aug 2026 16:43:29 -0400 Subject: [PATCH] Ensure close_notify is sent after user_canceled during quiet shutdown --- src/ssl.c | 1 + src/ssl_api_rw.c | 16 ++++++++- tests/api/test_ssl_rw.c | 72 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_rw.h | 3 ++ wolfssl/internal.h | 2 ++ 5 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/ssl.c b/src/ssl.c index da91ff10ac..9f5e0ccc01 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5660,6 +5660,7 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->options.isClosed = 0; ssl->options.connReset = 0; ssl->options.sentNotify = 0; + ssl->options.sentUserCanceled = 0; ssl->options.closeNotify = 0; ssl->options.sendVerify = 0; ssl->options.serverState = NULL_STATE; diff --git a/src/ssl_api_rw.c b/src/ssl_api_rw.c index 5509947603..21af84136e 100644 --- a/src/ssl_api_rw.c +++ b/src/ssl_api_rw.c @@ -808,6 +808,10 @@ int wolfSSL_SendUserCanceled(WOLFSSL* ssl) if (ssl != NULL) { ssl->error = SendAlert(ssl, alert_warning, user_canceled); + if ((ssl->error == 0) || + (ssl->error == WC_NO_ERR_TRACE(WANT_WRITE))) { + ssl->options.sentUserCanceled = 1; + } if (ssl->error < 0) { WOLFSSL_ERROR(ssl->error); } @@ -1030,10 +1034,20 @@ int wolfSSL_shutdown(WOLFSSL* ssl) if (ssl == NULL) { ret = WOLFSSL_FATAL_ERROR; } - else if (ssl->options.quietShutdown) { + else if (ssl->options.quietShutdown && (!ssl->options.sentUserCanceled)) { WOLFSSL_MSG("quiet shutdown, no close notify sent"); ret = WOLFSSL_SUCCESS; } + else if (ssl->options.quietShutdown) { + /* A "user_canceled" alert has gone out so we need a "close_notify" to + * follow it per RFC 9846 Section 6.1. */ + if (!wolfssl_shutdown_flush_alert(ssl, &ret)) { + (void)wolfssl_shutdown_send_close_notify(ssl, &ret); + } + if (ret == WC_NO_ERR_TRACE(WOLFSSL_SHUTDOWN_NOT_DONE)) { + ret = WOLFSSL_SUCCESS; + } + } else { int done; diff --git a/tests/api/test_ssl_rw.c b/tests/api/test_ssl_rw.c index 2e461f9d50..49099f0bd3 100644 --- a/tests/api/test_ssl_rw.c +++ b/tests/api/test_ssl_rw.c @@ -1062,6 +1062,78 @@ int test_wolfSSL_SendUserCanceled_paths(void) return EXPECT_RESULT(); } +/* Test that quiet shutdown does not suppress the close_notify that the + * user_canceled alert obliges wolfSSL to send. + * + * RFC 9846 Section 6.1 has a "close_notify" following "user_canceled" and has + * the peer keep reading until it arrives. Quiet shutdown may drop the + * close_notify that stands alone - that is what the option is for - but not + * the one the peer has been told to wait for. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_SendUserCanceled_quiet_shutdown(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_TLS) && \ + !defined(WOLFSSL_NO_TLS12) && (defined(OPENSSL_EXTRA) || \ + defined(OPENSSL_EXTRA_X509_SMALL) || defined(WOLFSSL_EXTRA) || \ + defined(WOLFSSL_WPAS_SMALL)) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + char reply[16]; + + 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); + + wolfSSL_set_quiet_shutdown(ssl_c, 1); + /* Both alerts go out. Waiting for the peer's reply is what quiet shutdown + * skips, so the shutdown is done as far as this side is concerned. */ + ExpectIntEQ(wolfSSL_SendUserCanceled(ssl_c), WOLFSSL_SUCCESS); + + /* The server reads the user_canceled and then the close_notify, which is + * what it reports. Without the close_notify it would still be waiting. */ + ExpectIntEQ(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, 0), WOLFSSL_ERROR_ZERO_RETURN); + ExpectIntEQ(wolfSSL_get_shutdown(ssl_s), WOLFSSL_RECEIVED_SHUTDOWN); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + wolfSSL_CTX_free(ctx_c); + ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); + ctx_s = NULL; + + /* A quiet shutdown with no user_canceled behind it still sends nothing: + * that is the whole point of the option. */ + 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); + + wolfSSL_set_quiet_shutdown(ssl_c, 1); + ExpectIntEQ(wolfSSL_shutdown(ssl_c), WOLFSSL_SUCCESS); + + /* Nothing arrived, so the server is still waiting for a record. */ + ExpectIntLT(wolfSSL_read(ssl_s, reply, (int)sizeof(reply)), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(wolfSSL_get_shutdown(ssl_s), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + /* Test that an error the read side recorded is the one the write reports. * * With a write duplicate in use the read side hands errors over through diff --git a/tests/api/test_ssl_rw.h b/tests/api/test_ssl_rw.h index 169638b948..bca140fff3 100644 --- a/tests/api/test_ssl_rw.h +++ b/tests/api/test_ssl_rw.h @@ -38,6 +38,7 @@ int test_wolfSSL_shutdown_repeat_after_done(void); int test_wolfSSL_shutdown_flush_no_notify(void); int test_wolfSSL_shutdown_quic_alert_refused(void); int test_wolfSSL_SendUserCanceled_paths(void); +int test_wolfSSL_SendUserCanceled_quiet_shutdown(void); int test_wolfSSL_write_dup_err(void); #define TEST_SSL_RW_DECLS \ @@ -56,6 +57,8 @@ int test_wolfSSL_write_dup_err(void); TEST_DECL_GROUP("ssl_rw", test_wolfSSL_shutdown_flush_no_notify), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_shutdown_quic_alert_refused), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_SendUserCanceled_paths), \ + TEST_DECL_GROUP("ssl_rw", \ + test_wolfSSL_SendUserCanceled_quiet_shutdown), \ TEST_DECL_GROUP("ssl_rw", test_wolfSSL_write_dup_err) #endif /* TESTS_API_SSL_RW_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 7569a5cb2a..8a1c24acf7 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5441,6 +5441,8 @@ struct Options { word16 isClosed:1; /* if we consider conn closed */ word16 closeNotify:1; /* we've received a close notify */ word16 sentNotify:1; /* we've sent a close notify */ + word16 sentUserCanceled:1; /* we've sent a user_canceled and + * owe the peer a close notify */ word16 usingCompression:1; /* are we using compression */ word16 haveRSA:1; /* RSA available */ word16 haveECC:1; /* ECC available */