Skip to content

Release PSA crypto state in uninitNetSystem(), not per socket (fixes #612) - #613

Open
ClintonSarkar wants to merge 3 commits into
machinezone:masterfrom
idnerdidx:fix/psa-crypto-process-lifetime
Open

ClintonSarkar wants to merge 3 commits into
machinezone:masterfrom
idnerdidx:fix/psa-crypto-process-lifetime

Conversation

@ClintonSarkar

@ClintonSarkar ClintonSarkar commented Oct 2, 2026 •

Copy link
Copy Markdown

Summary

SocketMbedTLS::close() called mbedtls_psa_crypto_free() for every socket. That call is process-global: it tears down all PSA key slots and the shared entropy/RNG state, so closing one TLS socket breaks every other live TLS socket in the process. A process with ~6 concurrent TLS WebSocket clients reconnecting on their own threads hit PSA_ERROR_BAD_STATE failures and heap-corruption aborts. This PR moves the teardown into ix::uninitNetSystem(), as suggested in #612.

What changed

  • close(): no longer calls mbedtls_psa_crypto_free().
  • uninitNetSystem(): calls mbedtls_psa_crypto_free() on all platforms when built with mbedTLS >= 3.6, mirroring the existing WSAStartup/WSACleanup pair. Documented in IXNetSystem.h.
  • initMBedTLS() guard: a per-socket _mbedtlsInitialized flag stops the ctor + init() double call from re-initialising live mbedTLS contexts (with MBEDTLS_THREADING_C that re-inits live mutexes). close() clears it so a socket can be re-initialised. psa_crypto_init() stays outside the guard (it is idempotent).
  • Checked psa_crypto_init() in init(): a failed PSA init now fails the connect with PSA crypto init failed: <status> instead of running TLS on uninitialised PSA state.
  • Test: new test/IXSocketMbedTLSPSATest.cpp ([socket_mbedtls], built only with USE_TLS + USE_MBED_TLS and mbedTLS >= 3.6). It probes PSA state via psa_generate_random and an HMAC key import (hashing works without PSA init, so it would not detect the bug), across another socket's close, and checks that uninitNetSystem() followed by psa_crypto_init() recovers.
  • PSA include guard: IXNetSystem.cpp includes <psa/crypto.h> only for mbedTLS >= 3.6, so builds against mbedTLS 2.x (no psa/ headers) are unaffected.
  • Placeholder below 3.6: the PSA test binary carries a single passing placeholder case below mbedTLS 3.6, so it never reports "no tests ran" and ctest stays green there.

Affected versions

Version Affected (built with mbedTLS >= 3.6)
v11.4.5 and earlier No
v11.4.6 Yes (introduced by #527)
v12.0.0 Yes (guard widened to mbedTLS 4.x by #579)
v12.0.1 Yes
master Yes

Builds against mbedTLS < 3.6 never compiled the call and are not affected.

Behaviour change

Applications that never call ix::uninitNetSystem() now keep PSA state until process exit, which the OS reclaims anyway. Applications that do call it must do so after closing all sockets — the same contract as WSACleanup on Windows.

Note on threading

Concurrent TLS from multiple threads also requires mbedTLS built with MBEDTLS_THREADING_C, which the stock mbedTLS config leaves off. This is a documentation note only; this PR does not change it.

Testing

Linux (g++ 13, Ninja), mbedTLS 3.6.5 built from source, configured with -DUSE_TLS=ON -DUSE_MBED_TLS=ON -DUSE_ZLIB=OFF -DUSE_TEST=ON.

Red — test commit only, on unfixed master (IXSocketMbedTLSPSATest "[socket_mbedtls]"):

psa_generate_random failed: -137
...
SocketMbedTLS close does not tear down process-global PSA state
...
test/IXSocketMbedTLSPSATest.cpp:64: FAILED:
  REQUIRE( psaStateWorks() )
with expansion:
  false
...
test cases: 2 | 1 passed | 1 failed
assertions: 6 | 5 passed | 1 failed

(-137 is PSA_ERROR_BAD_STATE; line 64 is the check right after another socket's close().)

Green — with the fix:

All tests passed (7 assertions in 2 test cases)

Full ctest: 17/18 passed. IXHttpServerTest fails on Accept-Encoding == "gzip" both with and without this change (my build had USE_ZLIB=OFF), so it is unrelated.

The pre-3.6 branches (include guard and test placeholder) were syntax-checked by forcing the version condition off with g++ -fsyntax-only, not built against a real older mbedTLS.

🤖 Generated with Claude Code

ClintonSarkar and others added 3 commits October 2, 2026 09:53
…lose

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 <noreply@anthropic.com>
…chinezone#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
machinezone#527, widened to mbedTLS 4.x in machinezone#579), only when built against
mbedTLS >= 3.6. v11.4.5 and earlier are not affected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y non-empty

- IXNetSystem.cpp: include <psa/crypto.h> 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 <noreply@anthropic.com>
@ClintonSarkar
ClintonSarkar force-pushed the fix/psa-crypto-process-lifetime branch from 0f296d8 to 65528a1 Compare October 2, 2026 02:16

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant