You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent epic:#279 (E26: TLS Hardening) Raised by: CodeRabbit on PR #419 (S08.07). Narrowed to mbedTLS-only following design discussion 2026-05-24; OpenSSL parity moves to S26.03 once this story ships.
Problem
When MbedTlsStream_Open fails after the inner transport's Open() has already succeeded, the adapter returns false without unwinding. The inner transport stays open and the SSL state from ssl_setup / ssl_config_defaults is left allocated. Audit of the path from StreamSender.Send down to the socket found three resources that leak on this path:
Transport fd — MbedTlsStream_Open does not call SolidSyslogStream_Close(self->Config.Transport) on the failure tail; the next StreamSender reconnect tick then re-enters PosixTcpStream_Open, which clobbers self->Fd with a fresh socket(...) without closing the prior fd (SolidSyslogPosixTcpStream.c:102).
SslContext inner allocations from mbedtls_ssl_setup — only freed by mbedtls_ssl_free inside Close.
SslConfig inner allocations from mbedtls_ssl_config_defaults — only freed by mbedtls_ssl_config_free inside Close.
All three close if MbedTlsStream_Open calls its own Close on every failure path after the first allocating operation, since Close already unwinds the SSL state and delegates to the transport (SolidSyslogMbedTlsStream.c:77-84).
Scope
Audit every failure point in MbedTlsStream_Open and pin each one. The failure points are:
Close the inner transport (SolidSyslogStream_Close(self->Config.Transport))
Free the SslContext (mbedtls_ssl_free)
Free the SslConfig (mbedtls_ssl_config_free)
Emit a typed error via SolidSyslog_Error against MbedTlsStreamErrorSource (see error codes below), severity SOLIDSYSLOG_SEVERITY_ERROR
Return false
A subsequent Open on the same handle must succeed cleanly (recovery contract).
Error codes added to SolidSyslogMbedTlsStreamErrors.h
Messages are protocol-level (what the negotiation failed to achieve), not C-call-level (which mbedTLS function returned non-zero).
Code
Triggered by
Message
MBEDTLSSTREAM_ERROR_DEFAULTS_NOT_APPLIED
mbedtls_ssl_config_defaults ≠ 0
"TLS client could not be configured with its default protocol settings; connection not established"
MBEDTLSSTREAM_ERROR_SESSION_INIT_FAILED
mbedtls_ssl_setup ≠ 0
"TLS session could not be initialised against the configured context; connection not established"
MBEDTLSSTREAM_ERROR_SERVER_NAME_NOT_SET
mbedtls_ssl_set_hostname ≠ 0
"TLS server name (SNI / cert match) could not be configured; connection not established"
MBEDTLSSTREAM_ERROR_HANDSHAKE_REJECTED
Handshake returns a hard error (peer rejected, cert untrusted, protocol incompatible)
"TLS handshake rejected by peer or local verification; connection not established"
MBEDTLSSTREAM_ERROR_HANDSHAKE_TIMEOUT
Handshake retry budget exhausted
"TLS handshake did not complete within the bounded retry budget; connection not established"
Inner-transport Open failure is not a MbedTlsStream error — the transport owns its own diagnostics.
Acceptance criteria
MbedTlsStream_Open calls an internal unwind (either MbedTlsStream_Close or an equivalent helper) on every failure path after the first allocating operation.
Five new codes added to SolidSyslogMbedTlsStreamErrors.h (see table above), with messages in SolidSyslogMbedTlsStreamMessages.c. Each failure point emits the matching code via SolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, &MbedTlsStreamErrorSource, <code>) before returning.
MbedTlsFake gains return-value setters for mbedtls_ssl_config_defaults, mbedtls_ssl_setup, and mbedtls_ssl_set_hostname (only handshake / read / write are currently settable).
Six new / extended unit tests in Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp pin the contract. Each of tests 1–5 asserts: Open returned false, StreamFake_CloseCallCount(transport) == 1, MbedTlsFake_SslFreeCallCount() == 1, MbedTlsFake_SslConfigFreeCallCount() == 1, ErrorHandlerFake_Handle called once with MbedTlsStreamErrorSource, the matching code, and severity SOLIDSYSLOG_SEVERITY_ERROR.
SecondOpenAfterFailedFirstOpenSucceeds — recovery proof: first handshake fails, second succeeds; asserts Transport.OpenCount == 2, Transport.CloseCount == 1, second Open returns true.
All existing tests still pass; in particular, CloseAfterInternalCloseFromSendFailureDoesNotDoubleFree still pins the idempotent-Close invariant.
Verification (developer-side, not a CI gate)
Run the untrusted-CA integration test (HandshakeFailsWhenServerCertSignedByUntrustedCa) in a loop with ulimit -n 32 and confirm it does not hit EMFILE. Record the result in the PR description.
Out of scope
OpenSSL parity.TlsStream_Open has the same shape; ships as S26.03 after this story merges, using the test pattern proven here.
Defensive close-before-open in PosixTcpStream_Open. The contract today is "every Stream_Open is preceded by Stream_Close"; StreamSender honours it via Disconnect-before-reconnect, and the unwind added by this story restores that contract for the mbedTLS failure path. Making PosixTcpStream defensive would paper over future contract violations rather than surface them.
SolidSyslogWinsockTcpStream and SolidSyslogFreeRtosTcpStream hardening. Same clobber-on-reopen shape, same argument: not reachable from a valid caller once mbedTLS unwinds correctly.
Parent epic: #279 (E26: TLS Hardening)
Raised by: CodeRabbit on PR #419 (S08.07). Narrowed to mbedTLS-only following design discussion 2026-05-24; OpenSSL parity moves to S26.03 once this story ships.
Problem
When
MbedTlsStream_Openfails after the inner transport'sOpen()has already succeeded, the adapter returnsfalsewithout unwinding. The inner transport stays open and the SSL state fromssl_setup/ssl_config_defaultsis left allocated. Audit of the path fromStreamSender.Senddown to the socket found three resources that leak on this path:MbedTlsStream_Opendoes not callSolidSyslogStream_Close(self->Config.Transport)on the failure tail; the nextStreamSenderreconnect tick then re-entersPosixTcpStream_Open, which clobbersself->Fdwith a freshsocket(...)without closing the prior fd (SolidSyslogPosixTcpStream.c:102).SslContextinner allocations frommbedtls_ssl_setup— only freed bymbedtls_ssl_freeinsideClose.SslConfiginner allocations frommbedtls_ssl_config_defaults— only freed bymbedtls_ssl_config_freeinsideClose.All three close if
MbedTlsStream_Opencalls its ownCloseon every failure path after the first allocating operation, sinceClosealready unwinds the SSL state and delegates to the transport (SolidSyslogMbedTlsStream.c:77-84).Scope
Audit every failure point in
MbedTlsStream_Openand pin each one. The failure points are:MbedTlsStream_ApplySslConfigDefaultsreturns false (mbedtls_ssl_config_defaults≠ 0)MbedTlsStream_BindContextToConfigreturns false (mbedtls_ssl_setup≠ 0)MbedTlsStream_ConfigureExpectedHostnamereturns false (mbedtls_ssl_set_hostname≠ 0)MbedTlsStream_PerformHandshakereturns false — hard errorMbedTlsStream_PerformHandshakereturns false — budget exhaustedFor each, the adapter must:
SolidSyslogStream_Close(self->Config.Transport))SslContext(mbedtls_ssl_free)SslConfig(mbedtls_ssl_config_free)SolidSyslog_ErroragainstMbedTlsStreamErrorSource(see error codes below), severitySOLIDSYSLOG_SEVERITY_ERRORfalseA subsequent
Openon the same handle must succeed cleanly (recovery contract).Error codes added to
SolidSyslogMbedTlsStreamErrors.hMessages are protocol-level (what the negotiation failed to achieve), not C-call-level (which mbedTLS function returned non-zero).
MBEDTLSSTREAM_ERROR_DEFAULTS_NOT_APPLIEDmbedtls_ssl_config_defaults≠ 0MBEDTLSSTREAM_ERROR_SESSION_INIT_FAILEDmbedtls_ssl_setup≠ 0MBEDTLSSTREAM_ERROR_SERVER_NAME_NOT_SETmbedtls_ssl_set_hostname≠ 0MBEDTLSSTREAM_ERROR_HANDSHAKE_REJECTEDMBEDTLSSTREAM_ERROR_HANDSHAKE_TIMEOUTInner-transport
Openfailure is not aMbedTlsStreamerror — the transport owns its own diagnostics.Acceptance criteria
MbedTlsStream_Opencalls an internal unwind (eitherMbedTlsStream_Closeor an equivalent helper) on every failure path after the first allocating operation.SolidSyslogMbedTlsStreamErrors.h(see table above), with messages inSolidSyslogMbedTlsStreamMessages.c. Each failure point emits the matching code viaSolidSyslog_Error(SOLIDSYSLOG_SEVERITY_ERROR, &MbedTlsStreamErrorSource, <code>)before returning.MbedTlsFakegains return-value setters formbedtls_ssl_config_defaults,mbedtls_ssl_setup, andmbedtls_ssl_set_hostname(onlyhandshake/read/writeare currently settable).Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpppin the contract. Each of tests 1–5 asserts:Openreturned false,StreamFake_CloseCallCount(transport) == 1,MbedTlsFake_SslFreeCallCount() == 1,MbedTlsFake_SslConfigFreeCallCount() == 1,ErrorHandlerFake_Handlecalled once withMbedTlsStreamErrorSource, the matching code, and severitySOLIDSYSLOG_SEVERITY_ERROR.OpenClosesTransportAndFreesSslStateWhenSslConfigDefaultsFailsOpenClosesTransportAndFreesSslStateWhenSslSetupFailsOpenClosesTransportAndFreesSslStateWhenSetHostnameFailsOpenClosesTransportAndFreesSslStateWhenHandshakeFailsHard— extends existingOpenFailsImmediatelyOnHardSslErrorOpenClosesTransportAndFreesSslStateWhenHandshakeBudgetExhausts— extends existingOpenFailsWhenHandshakeNeverCompletesSecondOpenAfterFailedFirstOpenSucceeds— recovery proof: first handshake fails, second succeeds; assertsTransport.OpenCount == 2,Transport.CloseCount == 1, secondOpenreturns true.CloseAfterInternalCloseFromSendFailureDoesNotDoubleFreestill pins the idempotent-Close invariant.Verification (developer-side, not a CI gate)
Run the untrusted-CA integration test (
HandshakeFailsWhenServerCertSignedByUntrustedCa) in a loop withulimit -n 32and confirm it does not hitEMFILE. Record the result in the PR description.Out of scope
TlsStream_Openhas the same shape; ships as S26.03 after this story merges, using the test pattern proven here.PosixTcpStream_Open. The contract today is "everyStream_Openis preceded byStream_Close";StreamSenderhonours it viaDisconnect-before-reconnect, and the unwind added by this story restores that contract for the mbedTLS failure path. MakingPosixTcpStreamdefensive would paper over future contract violations rather than surface them.SolidSyslogWinsockTcpStreamandSolidSyslogFreeRtosTcpStreamhardening. Same clobber-on-reopen shape, same argument: not reachable from a valid caller once mbedTLS unwinds correctly.StreamSenderreconnect flow changes.