From 539594eb58f250f09eb92e05cc1d6bc40c57afce Mon Sep 17 00:00:00 2001 From: BarshonClintonSarkar Date: Fri, 2 Oct 2026 09:53:05 +0800 Subject: [PATCH 1/3] Add regression test for process-global PSA state surviving a socket close Closing one SocketMbedTLS must not invalidate PSA crypto state (RNG, key slots) used by other live sockets in the same process. Co-Authored-By: Claude Fable 5.1 --- test/CMakeLists.txt | 6 +++ test/IXSocketMbedTLSPSATest.cpp | 87 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 test/IXSocketMbedTLSPSATest.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e482f249..6bdf79bd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,6 +47,12 @@ if (USE_ZLIB) ) endif() +if (USE_TLS AND USE_MBED_TLS) + list(APPEND TEST_TARGET_NAMES + IXSocketMbedTLSPSATest + ) +endif() + # Ping test fails intermittently, disabling them for now # IXWebSocketPingTest.cpp # IXWebSocketPingTimeoutTest.cpp diff --git a/test/IXSocketMbedTLSPSATest.cpp b/test/IXSocketMbedTLSPSATest.cpp new file mode 100644 index 00000000..b9a2f6bc --- /dev/null +++ b/test/IXSocketMbedTLSPSATest.cpp @@ -0,0 +1,87 @@ +/* + * IXSocketMbedTLSPSATest.cpp + * + * PSA crypto state is process-global: closing one SocketMbedTLS must not + * tear it down under other live sockets. + */ + +#ifdef IXWEBSOCKET_USE_MBED_TLS + +#include + +#if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) + +#include "IXTest.h" +#include +#include +#include +#include +#include + +using namespace ix; + +namespace +{ + // Hashing works without PSA init, so probe the RNG and a key slot instead. + bool psaStateWorks() + { + uint8_t bytes[16]; + psa_status_t status = psa_generate_random(bytes, sizeof(bytes)); + if (status != PSA_SUCCESS) + { + std::cerr << "psa_generate_random failed: " << (int) status << std::endl; + return false; + } + + psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attributes, PSA_KEY_TYPE_HMAC); + psa_set_key_bits(&attributes, 128); + psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_algorithm(&attributes, PSA_ALG_HMAC(PSA_ALG_SHA_256)); + + psa_key_id_t key = 0; + status = psa_import_key(&attributes, bytes, sizeof(bytes), &key); + psa_reset_key_attributes(&attributes); + if (status != PSA_SUCCESS) + { + std::cerr << "psa_import_key failed: " << (int) status << std::endl; + return false; + } + + return psa_destroy_key(key) == PSA_SUCCESS; + } +} // namespace + +TEST_CASE("SocketMbedTLS close does not tear down process-global PSA state", "[socket_mbedtls]") +{ + SocketMbedTLS a(SocketTLSOptions{}); + REQUIRE(psaStateWorks()); + + { + SocketMbedTLS b(SocketTLSOptions{}); + b.close(); + } + REQUIRE(psaStateWorks()); + + a.close(); + + SocketMbedTLS c(SocketTLSOptions{}); + REQUIRE(psaStateWorks()); +} + +TEST_CASE("uninitNetSystem releases PSA state and allows re-init", "[socket_mbedtls]") +{ + REQUIRE(initNetSystem()); + { + SocketMbedTLS a(SocketTLSOptions{}); + a.close(); + } + REQUIRE(uninitNetSystem()); + + REQUIRE(psa_crypto_init() == PSA_SUCCESS); + REQUIRE(psaStateWorks()); +} + +#endif // MBEDTLS_VERSION_MAJOR >= 3.6 + +#endif // IXWEBSOCKET_USE_MBED_TLS From f76caf02444d7c226a7766f17bae3b42afb5f09e Mon Sep 17 00:00:00 2001 From: BarshonClintonSarkar Date: Fri, 2 Oct 2026 09:55:19 +0800 Subject: [PATCH 2/3] Release PSA crypto state in uninitNetSystem(), not per socket (fix #612) SocketMbedTLS::close() called mbedtls_psa_crypto_free() for every socket. That function is process-global: it wipes all PSA key slots and the shared entropy/RNG state, so closing one TLS socket broke every other live TLS socket in the process. With several concurrent TLS WebSocket clients reconnecting on their own threads this showed up as PSA_ERROR_BAD_STATE failures and heap-corruption aborts. - close() no longer calls mbedtls_psa_crypto_free(). - uninitNetSystem() now calls mbedtls_psa_crypto_free() on all platforms when built with mbedTLS >= 3.6, mirroring the existing WSAStartup/WSACleanup pairing. It must be called only after all sockets are closed. - initMBedTLS() is guarded by a per-socket flag so the ctor + init() double call no longer re-initialises live mbedTLS contexts (which re-inits mutexes under MBEDTLS_THREADING_C). close() clears the flag so a socket can be re-initialised. - init() checks psa_crypto_init() and fails the connect with an error message instead of running TLS on uninitialised PSA state. Affected releases: v11.4.6, v12.0.0, v12.0.1 and master (introduced in #527, widened to mbedTLS 4.x in #579), only when built against mbedTLS >= 3.6. v11.4.5 and earlier are not affected. Co-Authored-By: Claude Fable 5.1 --- ixwebsocket/IXNetSystem.cpp | 7 +++++++ ixwebsocket/IXNetSystem.h | 1 + ixwebsocket/IXSocketMbedTLS.cpp | 33 +++++++++++++++++++++++---------- ixwebsocket/IXSocketMbedTLS.h | 1 + 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/ixwebsocket/IXNetSystem.cpp b/ixwebsocket/IXNetSystem.cpp index 9763da09..67b85dbc 100644 --- a/ixwebsocket/IXNetSystem.cpp +++ b/ixwebsocket/IXNetSystem.cpp @@ -7,6 +7,10 @@ #include "IXNetSystem.h" #include #include +#ifdef IXWEBSOCKET_USE_MBED_TLS +#include +#include +#endif #ifdef _WIN32 #ifndef EAFNOSUPPORT #define EAFNOSUPPORT 102 @@ -38,6 +42,9 @@ namespace ix bool uninitNetSystem() { +#if defined(IXWEBSOCKET_USE_MBED_TLS) && (MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6)) + mbedtls_psa_crypto_free(); +#endif #ifdef _WIN32 int err = WSACleanup(); return err == 0; diff --git a/ixwebsocket/IXNetSystem.h b/ixwebsocket/IXNetSystem.h index 061b3622..535a5069 100644 --- a/ixwebsocket/IXNetSystem.h +++ b/ixwebsocket/IXNetSystem.h @@ -88,6 +88,7 @@ namespace ix #endif bool initNetSystem(); + // Also releases process-global TLS (PSA) state; call only after all sockets are closed. bool uninitNetSystem(); int poll(struct pollfd* fds, nfds_t nfds, int timeout, void** event); diff --git a/ixwebsocket/IXSocketMbedTLS.cpp b/ixwebsocket/IXSocketMbedTLS.cpp index d236f611..8bef603f 100644 --- a/ixwebsocket/IXSocketMbedTLS.cpp +++ b/ixwebsocket/IXSocketMbedTLS.cpp @@ -40,15 +40,19 @@ namespace ix { std::lock_guard lock(_mutex); - mbedtls_ssl_init(&_ssl); - mbedtls_ssl_config_init(&_conf); + if (!_mbedtlsInitialized) + { + _mbedtlsInitialized = true; + mbedtls_ssl_init(&_ssl); + mbedtls_ssl_config_init(&_conf); #if MBEDTLS_VERSION_MAJOR < 4 - mbedtls_ctr_drbg_init(&_ctr_drbg); - mbedtls_entropy_init(&_entropy); + mbedtls_ctr_drbg_init(&_ctr_drbg); + mbedtls_entropy_init(&_entropy); #endif - mbedtls_x509_crt_init(&_cacert); - mbedtls_x509_crt_init(&_cert); - mbedtls_pk_init(&_pkey); + mbedtls_x509_crt_init(&_cacert); + mbedtls_x509_crt_init(&_cert); + mbedtls_pk_init(&_pkey); + } // Initialize the PSA Crypto API for mbedTLS 3.6+ and all 4.x releases. // See: https://github.com/Mbed-TLS/mbedtls/blob/development/docs/use-psa-crypto.md #if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) @@ -111,6 +115,15 @@ namespace ix initMBedTLS(); std::lock_guard lock(_mutex); +#if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) + psa_status_t status = psa_crypto_init(); + if (status != PSA_SUCCESS) + { + errMsg = "PSA crypto init failed: " + std::to_string((int) status); + return false; + } +#endif + #if MBEDTLS_VERSION_MAJOR < 4 const char* pers = "IXSocketMbedTLS"; if (mbedtls_ctr_drbg_seed(&_ctr_drbg, @@ -328,9 +341,9 @@ namespace ix mbedtls_x509_crt_free(&_cacert); mbedtls_x509_crt_free(&_cert); mbedtls_pk_free(&_pkey); -#if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) - mbedtls_psa_crypto_free(); -#endif + _mbedtlsInitialized = false; + // PSA crypto state is process-global; it is released in ix::uninitNetSystem(), + // not per socket. Socket::close(); } diff --git a/ixwebsocket/IXSocketMbedTLS.h b/ixwebsocket/IXSocketMbedTLS.h index 05ab15d8..5a18e66e 100644 --- a/ixwebsocket/IXSocketMbedTLS.h +++ b/ixwebsocket/IXSocketMbedTLS.h @@ -54,6 +54,7 @@ namespace ix std::mutex _mutex; SocketTLSOptions _tlsOptions; + bool _mbedtlsInitialized = false; bool init(const std::string& host, bool isClient, std::string& errMsg); void initMBedTLS(); From 65528a1952f845cf841c767f360579f5404f9cfe Mon Sep 17 00:00:00 2001 From: BarshonClintonSarkar Date: Fri, 2 Oct 2026 10:11:56 +0800 Subject: [PATCH 3/3] Guard the PSA include for pre-3.6 mbedTLS and keep the PSA test binary non-empty - IXNetSystem.cpp: include only for mbedTLS >= 3.6, so builds against mbedTLS 2.x (no psa/ headers) still compile. - The PSA lifetime test compiles to a single passing placeholder case below mbedTLS 3.6 instead of an empty binary, which Catch2 reports as "no tests ran" (exit 2) and would fail ctest for a supported config. - Document in usage.md that uninitNetSystem() now also releases process-global TLS (PSA) state and must follow the last socket close. Co-Authored-By: Claude Fable 5.1 --- docs/usage.md | 2 +- ixwebsocket/IXNetSystem.cpp | 2 ++ test/IXSocketMbedTLSPSATest.cpp | 18 ++++++++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 43664615..c35c7f35 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -4,7 +4,7 @@ The [*ws*](https://github.com/machinezone/IXWebSocket/tree/master/ws) folder cou ## Windows note -To use the network system on Windows, you need to initialize it once with *WSAStartup()* and clean it up with *WSACleanup()*. We have helpers for that which you can use, see below. This init would typically take place in your main function. +To use the network system on Windows, you need to initialize it once with *WSAStartup()* and clean it up with *WSACleanup()*. We have helpers for that which you can use, see below. This init would typically take place in your main function. On every platform, *ix::uninitNetSystem()* also releases process-global TLS state (mbedTLS PSA crypto, with mbedTLS >= 3.6), so call it only after all sockets are closed. ```cpp #include diff --git a/ixwebsocket/IXNetSystem.cpp b/ixwebsocket/IXNetSystem.cpp index 67b85dbc..42bf1df5 100644 --- a/ixwebsocket/IXNetSystem.cpp +++ b/ixwebsocket/IXNetSystem.cpp @@ -9,8 +9,10 @@ #include #ifdef IXWEBSOCKET_USE_MBED_TLS #include +#if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) #include #endif +#endif #ifdef _WIN32 #ifndef EAFNOSUPPORT #define EAFNOSUPPORT 102 diff --git a/test/IXSocketMbedTLSPSATest.cpp b/test/IXSocketMbedTLSPSATest.cpp index b9a2f6bc..e5bc1d4a 100644 --- a/test/IXSocketMbedTLSPSATest.cpp +++ b/test/IXSocketMbedTLSPSATest.cpp @@ -7,19 +7,18 @@ #ifdef IXWEBSOCKET_USE_MBED_TLS +#include +#include #include +using namespace ix; + #if MBEDTLS_VERSION_MAJOR >= 4 || (MBEDTLS_VERSION_MAJOR == 3 && MBEDTLS_VERSION_MINOR >= 6) -#include "IXTest.h" -#include #include -#include #include #include -using namespace ix; - namespace { // Hashing works without PSA init, so probe the RNG and a key slot instead. @@ -82,6 +81,13 @@ TEST_CASE("uninitNetSystem releases PSA state and allows re-init", "[socket_mbed REQUIRE(psaStateWorks()); } -#endif // MBEDTLS_VERSION_MAJOR >= 3.6 +#else // mbedTLS < 3.6 + +TEST_CASE("SocketMbedTLS PSA lifetime (not applicable below mbedTLS 3.6)", "[socket_mbedtls]") +{ + SUCCEED("mbedTLS < 3.6 has no PSA state to release"); +} + +#endif // mbedTLS >= 3.6 #endif // IXWEBSOCKET_USE_MBED_TLS