From 1fcfaa0c1ed1ae4a11be7fab29a1b52d3934ba18 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 12:31:55 +0000 Subject: [PATCH 1/9] test(openssl): add S26.03 error codes and protocol-level messages Slice 1 of S26.03 (#441). Five new codes in TlsStreamErrors.h mirroring the S26.02 mbedTLS pattern (CONTEXT_INIT_FAILED, SESSION_INIT_FAILED, SERVER_NAME_NOT_SET, HANDSHAKE_REJECTED, HANDSHAKE_TIMEOUT) and matching protocol-level messages in TlsStreamMessages.c. No emit sites yet; subsequent slices add the per-helper emissions plus the Open failure-tail unwind. --- .../OpenSsl/Interface/SolidSyslogTlsStreamErrors.h | 5 +++++ Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h b/Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h index 3ffcdb0d..4d0577d0 100644 --- a/Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h +++ b/Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h @@ -11,6 +11,11 @@ EXTERN_C_BEGIN { TLSSTREAM_ERROR_POOL_EXHAUSTED, TLSSTREAM_ERROR_UNKNOWN_DESTROY, + TLSSTREAM_ERROR_CONTEXT_INIT_FAILED, + TLSSTREAM_ERROR_SESSION_INIT_FAILED, + TLSSTREAM_ERROR_SERVER_NAME_NOT_SET, + TLSSTREAM_ERROR_HANDSHAKE_REJECTED, + TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT, TLSSTREAM_ERROR_MAX }; diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c b/Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c index 80f3d202..953c6d88 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c @@ -8,6 +8,16 @@ static const char* TlsStreamError_AsString(uint8_t code) static const char* const messages[TLSSTREAM_ERROR_MAX] = { [TLSSTREAM_ERROR_POOL_EXHAUSTED] = "SolidSyslogTlsStream_Create pool exhausted; returning fallback stream", [TLSSTREAM_ERROR_UNKNOWN_DESTROY] = "SolidSyslogTlsStream_Destroy called with a handle not issued by this pool", + [TLSSTREAM_ERROR_CONTEXT_INIT_FAILED] = + "TLS client context could not be configured; connection not established", + [TLSSTREAM_ERROR_SESSION_INIT_FAILED] = + "TLS session could not be initialised against the configured context; connection not established", + [TLSSTREAM_ERROR_SERVER_NAME_NOT_SET] = + "TLS server name (SNI / cert match) could not be configured; connection not established", + [TLSSTREAM_ERROR_HANDSHAKE_REJECTED] = + "TLS handshake rejected by peer or local verification; connection not established", + [TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT] = + "TLS handshake did not complete within the bounded retry budget; connection not established", }; const char* result = "unknown"; if (code < (uint8_t) TLSSTREAM_ERROR_MAX) From be7227aba92685078e5a87639081de5bb85d86f1 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:22:38 +0000 Subject: [PATCH 2/9] fix(openssl): emit CONTEXT_INIT_FAILED and unwind when InitSslContext fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 2 of S26.03 (#441). Mirrors S26.02's per-helper emit + tail unwind pattern. When TlsStream_InitSslContext returns false (covers SSL_CTX_new allocation failure plus every sub-step inside ConfigureSslContext — trust anchors, client identity, protocol floor, cipher list), InitSslContext emits TLSSTREAM_ERROR_CONTEXT_INIT_FAILED at severity ERROR. TlsStream_Open's failure tail now calls TlsStream_Close + TlsStream_ReleaseSslContext, releasing the inner transport plus any Ssl / BioMethod / Ctx allocated before the failure. Both helpers are NULL-guarded so the unwind is safe regardless of which step tripped (matches the S26.02 idempotent-Close pattern). Test side: introduces CHECK_OPEN_UNWOUND_WITH_ERROR(transport, code) macro covering the 5 universal post-failure assertions (transport closed once, ErrorHandler called once, source, code, severity). Per- mode SSL-resource-free assertions stay in the existing XxxFailureFreesCtx tests because their counts vary by failure depth. ErrorHandlerFake_Install added to setup() so every test starts with a fresh handler — unaffected tests don't observe it. Subsequent slices (3-7) extend the remaining per-failure tests with the macro and add the per-helper emissions for the other 4 codes. --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 27 ++++++++++++++++--- Tests/SolidSyslogTlsStreamTest.cpp | 17 ++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index f5362e64..64e1f73d 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -6,10 +6,14 @@ #include #include #include +#include +#include "SolidSyslogError.h" #include "SolidSyslogNullStream.h" +#include "SolidSyslogPrival.h" #include "SolidSyslogStream.h" #include "SolidSyslogStreamDefinition.h" +#include "SolidSyslogTlsStreamErrors.h" #include "SolidSyslogTlsStreamPrivate.h" enum @@ -135,15 +139,30 @@ static inline void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* self static inline bool TlsStream_Open(struct SolidSyslogStream* base, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* self = TlsStream_SelfFromBase(base); - return SolidSyslogStream_Open(self->Config.Transport, addr) && TlsStream_InitSslContext(self) && - TlsStream_InitSslSession(self) && TlsStream_AttachTransportBio(self) && - TlsStream_ConfigureExpectedHostname(self) && TlsStream_PerformHandshake(self); + bool ok = SolidSyslogStream_Open(self->Config.Transport, addr) && TlsStream_InitSslContext(self) && + TlsStream_InitSslSession(self) && TlsStream_AttachTransportBio(self) && + TlsStream_ConfigureExpectedHostname(self) && TlsStream_PerformHandshake(self); + if (!ok) + { + TlsStream_Close(base); + TlsStream_ReleaseSslContext(self); + } + return ok; } static inline bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* self) { self->Ctx = TlsStream_CreateSslContext(&self->Config); - return self->Ctx != NULL; + bool ok = self->Ctx != NULL; + if (!ok) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_CONTEXT_INIT_FAILED + ); + } + return ok; } static inline SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index a9b0326d..8794eaa2 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -4,10 +4,13 @@ #include #include +#include "ErrorHandlerFake.h" #include "OpenSslFake.h" #include "AddressFake.h" +#include "SolidSyslogPrival.h" #include "SolidSyslogStream.h" #include "SolidSyslogTlsStream.h" +#include "SolidSyslogTlsStreamErrors.h" #include "SolidSyslogTransport.h" #include "StreamFake.h" #include "TestUtils.h" @@ -16,6 +19,18 @@ using namespace CososoTesting; // NOLINT(google-build-using-namespace) -- test-file scope only; brings NEVER/ONCE/TWICE/THRICE into scope for the CALLED_* // macros +// NOLINTBEGIN(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) -- macro preserves __FILE__/__LINE__ in failure output; do-while wraps the multi-statement body for safe single-statement use +#define CHECK_OPEN_UNWOUND_WITH_ERROR(transport, expectedCode) \ + do \ + { \ + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); \ + CALLED_FAKE(ErrorHandlerFake_Handle, ONCE); \ + POINTERS_EQUAL(&TlsStreamErrorSource, ErrorHandlerFake_LastSource()); \ + UNSIGNED_LONGS_EQUAL((expectedCode), ErrorHandlerFake_LastCode()); \ + LONGS_EQUAL(SOLIDSYSLOG_SEVERITY_ERROR, ErrorHandlerFake_LastSeverity()); \ + } while (0) +// NOLINTEND(cppcoreguidelines-macro-usage,cppcoreguidelines-avoid-do-while) + class TEST_SolidSyslogTlsStream_ReadReturnsNegativeOneOnHardErrorAndClosesSsl_Test; class TEST_SolidSyslogTlsStream_ReadReturnsNegativeOneOnZeroReturnAndClosesSsl_Test; class TEST_SolidSyslogTlsStream_SendClosesTransportOnWriteFailure_Test; @@ -40,6 +55,7 @@ TEST_GROUP(SolidSyslogTlsStream) void setup() override { OpenSslFake_Reset(); + ErrorHandlerFake_Install(nullptr); NoOpSleepCallCount = 0; g_lastSleepMs = 0; transport = StreamFake_Create(); @@ -633,6 +649,7 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCtxNewFails) { OpenSslFake_SetCtxNewFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSslNewFails) From f6b66c9bdb37d23199edbb0d1e29df0aece0e760 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:30:31 +0000 Subject: [PATCH 3/9] test(openssl): extend remaining CONTEXT_INIT_FAILED tests with unwind macro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3 of S26.03 (#441). Each of the 8 existing per-failure-mode tests inside InitSslContext's scope (LoadVerifyLocations, MinProto, CipherListRejected, OnlyClientCert, OnlyClientKey, UseCertChainFile, UsePrivateKeyFile, CheckPrivateKey) now asserts the full unwind + error contract via CHECK_OPEN_UNWOUND_WITH_ERROR. All pass on arrival — slice 2's per-helper emit in InitSslContext + tail unwind in Open covers every CONTEXT_INIT sub-failure uniformly. Also added ReCreateStreamWithUpdatedConfig() helper to TEST_GROUP, mirroring S26.02's MbedTls equivalent. Replaces inlined Destroy/recreate-stream sequences in the six tests that need a config tweak (CipherList, ClientCertChainPath, ClientKeyPath). The helper also destroys and recreates the transport + resets OpenSslFake + reinstalls ErrorHandlerFake, so post-recreate count assertions (== 1) observe only the new Open. --- Tests/SolidSyslogTlsStreamTest.cpp | 42 +++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 8794eaa2..f56a1eef 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -73,6 +73,22 @@ TEST_GROUP(SolidSyslogTlsStream) StreamFake_Destroy(transport); } + /* Tests needing config tweaks (CipherList, ClientCertChainPath, ServerName, …) + * call this to release setup()'s pool slot, mutate `config`, then re-Create. + * Fully resets the fixture (transport, OpenSslFake counters, error handler) + * so the test body observes counts from this Open onwards only — matters + * for assertions like CHECK_OPEN_UNWOUND_WITH_ERROR that pin counts at == 1. */ + void ReCreateStreamWithUpdatedConfig() + { + SolidSyslogTlsStream_Destroy(stream); + StreamFake_Destroy(transport); + OpenSslFake_Reset(); + ErrorHandlerFake_Install(nullptr); + transport = StreamFake_Create(); + config.Transport = transport; + stream = SolidSyslogTlsStream_Create(&config); + } + /* Drive the registered BIO read callback with the given transport return — collapses the open + set-return + grab-callback + invoke boilerplate. */ [[nodiscard]] int InvokeBioReadWithTransportReturn(SolidSyslogSsize transportReturn) const @@ -234,11 +250,11 @@ TEST(SolidSyslogTlsStream, OpenSkipsCipherListSetupWhenNotConfigured) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCipherListRejected) { - SolidSyslogTlsStream_Destroy(stream); config.CipherList = "not-a-real-cipher"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetCipherListFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, CipherListFailureFreesCtx) @@ -662,6 +678,7 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenLoadVerifyLocationsFails) { OpenSslFake_SetLoadVerifyLocationsFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, LoadVerifyLocationsFailureFreesCtx) @@ -675,6 +692,7 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenMinProtoVersionFails) { OpenSslFake_SetMinProtoVersionFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, MinProtoVersionFailureFreesCtx) @@ -822,11 +840,11 @@ TEST(SolidSyslogTlsStream, OpenChecksClientKeyMatchesCert) TEST(SolidSyslogTlsStream, OpenFailsWhenOnlyClientCertIsSet) { - SolidSyslogTlsStream_Destroy(stream); config.ClientCertChainPath = "/some/path/client.pem"; config.ClientKeyPath = nullptr; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, OpenMakesNoClientIdentityCallsWhenOnlyClientCertIsSet) @@ -843,11 +861,11 @@ TEST(SolidSyslogTlsStream, OpenMakesNoClientIdentityCallsWhenOnlyClientCertIsSet TEST(SolidSyslogTlsStream, OpenFailsWhenOnlyClientKeyIsSet) { - SolidSyslogTlsStream_Destroy(stream); config.ClientCertChainPath = nullptr; config.ClientKeyPath = "/some/path/client.key"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, OpenMakesNoClientIdentityCallsWhenOnlyClientKeyIsSet) @@ -874,12 +892,12 @@ TEST(SolidSyslogTlsStream, PartialClientIdentityConfigFreesCtx) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenUseCertChainFileFails) { - SolidSyslogTlsStream_Destroy(stream); config.ClientCertChainPath = "/some/path/client.pem"; config.ClientKeyPath = "/some/path/client.key"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetUseCertChainFileFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, UseCertChainFileFailureFreesCtx) @@ -895,12 +913,12 @@ TEST(SolidSyslogTlsStream, UseCertChainFileFailureFreesCtx) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenUsePrivateKeyFileFails) { - SolidSyslogTlsStream_Destroy(stream); config.ClientCertChainPath = "/some/path/client.pem"; config.ClientKeyPath = "/some/path/client.key"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetUsePrivateKeyFileFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, UsePrivateKeyFileFailureFreesCtx) @@ -916,12 +934,12 @@ TEST(SolidSyslogTlsStream, UsePrivateKeyFileFailureFreesCtx) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCheckPrivateKeyFails) { - SolidSyslogTlsStream_Destroy(stream); config.ClientCertChainPath = "/some/path/client.pem"; config.ClientKeyPath = "/some/path/client.key"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetCheckPrivateKeyFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_CONTEXT_INIT_FAILED); } TEST(SolidSyslogTlsStream, CheckPrivateKeyFailureFreesCtx) From 08bbc786076e4c831377137c1feda2d63709b888 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:33:41 +0000 Subject: [PATCH 4/9] fix(openssl): emit SESSION_INIT_FAILED when SSL_new or BIO setup fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 4 of S26.03 (#441). Both per-helper emit sites (InitSslSession's SSL_new and AttachTransportBio's BIO_meth_new / BIO_new) now emit the shared TLSSTREAM_ERROR_SESSION_INIT_FAILED code at severity ERROR. The shared code reflects the protocol-level meaning ("TLS session could not be initialised against the configured context") — the integrator does not need to distinguish SSL allocation from BIO allocation at this level. Slice 2's tail unwind already covers the resource release on both paths. Three existing tests extended with the macro to pin the contract: OpenReturnsFalseWhenSslNewFails, WhenBioMethNewFails, WhenBioNewFails. --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 19 ++++++++++++++++++- Tests/SolidSyslogTlsStreamTest.cpp | 3 +++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 64e1f73d..b717def5 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -233,7 +233,16 @@ static inline bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* ciphe static inline bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* self) { self->Ssl = SSL_new(self->Ctx); - return self->Ssl != NULL; + bool ok = self->Ssl != NULL; + if (!ok) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_SESSION_INIT_FAILED + ); + } + return ok; } static inline bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* self) @@ -245,6 +254,14 @@ static inline bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* sel BIO_set_data(bio, self->Config.Transport); SSL_set_bio(self->Ssl, bio, bio); } + else + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_SESSION_INIT_FAILED + ); + } return ok; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index f56a1eef..966224a4 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -672,6 +672,7 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSslNewFails) { OpenSslFake_SetSslNewFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_SESSION_INIT_FAILED); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenLoadVerifyLocationsFails) @@ -706,12 +707,14 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenBioMethNewFails) { OpenSslFake_SetBioMethNewFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_SESSION_INIT_FAILED); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenBioNewFails) { OpenSslFake_SetBioNewFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_SESSION_INIT_FAILED); } TEST(SolidSyslogTlsStream, BioNewFailureFreesBioMethodInline) From be736e38e2a01754c6e76386297f82dd56f4c95a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:35:15 +0000 Subject: [PATCH 5/9] fix(openssl): emit SERVER_NAME_NOT_SET when SNI / set1_host fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 5 of S26.03 (#441). ConfigureExpectedHostname emits TLSSTREAM_ERROR_SERVER_NAME_NOT_SET at severity ERROR when either SSL_set_tlsext_host_name or SSL_set1_host fails. Emission stays inside the ServerName != NULL guard — a caller who never sets ServerName never reaches these calls, so no spurious error reported. Slice 2's tail unwind already covers the resource release. Two existing tests (WhenSet1HostFails, WhenSniHostnameSetupFails) extended with the macro + switched to ReCreateStreamWithUpdatedConfig for the ServerName injection. --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 8 ++++++++ Tests/SolidSyslogTlsStreamTest.cpp | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index b717def5..024c5d86 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -377,6 +377,14 @@ static inline bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStre { ok = (SSL_set_tlsext_host_name(self->Ssl, self->Config.ServerName) == 1) && (SSL_set1_host(self->Ssl, self->Config.ServerName) == 1); + if (!ok) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_SERVER_NAME_NOT_SET + ); + } } return ok; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 966224a4..67740741 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -645,20 +645,20 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenHandshakeFails) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSet1HostFails) { - SolidSyslogTlsStream_Destroy(stream); config.ServerName = "logs.example"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetSet1HostFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_SERVER_NAME_NOT_SET); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSniHostnameSetupFails) { - SolidSyslogTlsStream_Destroy(stream); config.ServerName = "logs.example"; - stream = SolidSyslogTlsStream_Create(&config); + ReCreateStreamWithUpdatedConfig(); OpenSslFake_SetSniHostnameFails(true); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_SERVER_NAME_NOT_SET); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCtxNewFails) From 4d3b58d620eba4f1aab47299e54a4ff1477b76cd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:40:18 +0000 Subject: [PATCH 6/9] fix(openssl): emit HANDSHAKE_REJECTED and HANDSHAKE_TIMEOUT per exit branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 6+7 of S26.03 (#441). Combined into one commit because the production change is a single PerformHandshake refactor — splitting the combined "not-retryable OR budget-exhausted" exit branch into two distinct arms so each exit emits its own protocol-level code: - Hard SSL error (non-WANT) → TLSSTREAM_ERROR_HANDSHAKE_REJECTED. Covers peer rejection, cert chain untrusted, server name mismatch, protocol-version incompatibility — every fatal handshake outcome the client sees as a single non-retryable rc. - Budget exhausted (persistent WANT_READ/WRITE for HANDSHAKE_TIMEOUT_ MILLISECONDS) → TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT. Distinct from REJECTED so the integrator can tell a wedged peer from one that actively rejected. Three tests extended with the macro: - OpenReturnsFalseWhenHandshakeFails — pinned to HANDSHAKE_REJECTED (forced via SSL_get_error returning SSL_ERROR_SSL). - OpenFailsWhenHandshakeNeverCompletes — pinned to HANDSHAKE_TIMEOUT (persistent WANT_READ). - OpenFailsImmediatelyOnHardSslError — pinned to HANDSHAKE_REJECTED; keeps its existing once-only Connect and never-Sleep assertions. Mirrors S26.02 slices 7+8. --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 16 +++++++++++++++- Tests/SolidSyslogTlsStreamTest.cpp | 7 +++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 024c5d86..7b95b6f7 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -421,8 +421,22 @@ static inline bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* self) else { int err = SSL_get_error(self->Ssl, rc); - if (!TlsStream_IsRetryableSslError(err) || TlsStream_IsHandshakeBudgetExhausted(totalSleptMs)) + if (!TlsStream_IsRetryableSslError(err)) { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_HANDSHAKE_REJECTED + ); + done = true; + } + else if (TlsStream_IsHandshakeBudgetExhausted(totalSleptMs)) + { + SolidSyslog_Error( + SOLIDSYSLOG_SEVERITY_ERROR, + &TlsStreamErrorSource, + (uint8_t) TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT + ); done = true; } else diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 67740741..e82b609d 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -639,8 +639,13 @@ TEST(SolidSyslogTlsStream, OpenReturnsTrueOnHappyPath) TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenHandshakeFails) { + /* Default OpenSslFake_SetConnectFails(true) returns -1 from SSL_connect + * and SSL_get_error reports SSL_ERROR_SSL (the default for SetGetErrorReturn) + * — a non-retryable hard error, which is the HANDSHAKE_REJECTED branch. */ OpenSslFake_SetConnectFails(true); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_HANDSHAKE_REJECTED); } TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSet1HostFails) @@ -1061,6 +1066,7 @@ TEST(SolidSyslogTlsStream, OpenFailsWhenHandshakeNeverCompletes) progress, so the bounded budget should expire and Open returns false. */ ArrangePersistentHandshakeError(SSL_ERROR_WANT_READ); CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT); } TEST(SolidSyslogTlsStream, OpenFailsImmediatelyOnHardSslError) @@ -1070,6 +1076,7 @@ TEST(SolidSyslogTlsStream, OpenFailsImmediatelyOnHardSslError) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); CALLED_FAKE(OpenSslFake_Connect, ONCE); CALLED_FUNCTION(NoOpSleep, NEVER); + CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_HANDSHAKE_REJECTED); } /* ------------------------------------------------------------------------- From f6051bec3049ebe5c069fced0256a6c1dd68b55f Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:43:28 +0000 Subject: [PATCH 7/9] test(openssl): pin Open-Close-Open recovery after failed Open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 8 of S26.03 (#441). Mirrors S26.02 slice 9. Pins the recovery contract that the per-failure-point unwinds enable: after a failed Open, the next Open is a clean Open-Close-Open cycle on the inner transport. Without the unwind tail added in slice 2, the inner transport would stay open and the next StreamSender reconnect tick would re-enter PosixTcpStream_Open, clobbering the prior fd. Arrangement uses OpenSslFake_SetConnectReturnSequence([fail, ok]) plus a SetGetErrorReturn flip to drive the first handshake to a hard error (fail-fast) and the second to success. Asserts Transport.OpenCount == 2, Transport.CloseCount == 1, second Open returns true. Passed on arrival — no production change. --- Tests/SolidSyslogTlsStreamTest.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index e82b609d..56322bb5 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -1079,6 +1079,24 @@ TEST(SolidSyslogTlsStream, OpenFailsImmediatelyOnHardSslError) CHECK_OPEN_UNWOUND_WITH_ERROR(transport, TLSSTREAM_ERROR_HANDSHAKE_REJECTED); } +TEST(SolidSyslogTlsStream, SecondOpenAfterFailedFirstOpenSucceeds) +{ + /* The recovery contract that the per-failure-point unwinds enable: once + * Open's failure tail Closes the transport and releases the SSL state, the + * next Open is a clean Open-Close-Open cycle on the transport. Without the + * unwind, the inner transport would stay open and PosixTcpStream_Open would + * clobber its fd on the next StreamSender reconnect tick. */ + int handshakeSequence[] = {-1, 1}; + OpenSslFake_SetConnectReturnSequence(handshakeSequence, 2); + OpenSslFake_SetGetErrorReturn(SSL_ERROR_SSL); /* first call: hard error, fail-fast */ + + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); + OpenSslFake_SetGetErrorReturn(0); /* second call: handshake succeeds, no error lookup */ + CHECK_TRUE(SolidSyslogStream_Open(stream, addr)); + LONGS_EQUAL(2, StreamFake_OpenCallCount(transport)); + LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); +} + /* ------------------------------------------------------------------------- * Send fail-fast: any non-success closes the SSL session and the underlying * transport so the StreamSender's reconnect path runs on the next tick. From 51811246626a2e71a23a8d069deb9dc52aff2b7e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:52:31 +0000 Subject: [PATCH 8/9] chore(misra): shift TlsStream suppression line numbers for S26.03 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 9 of S26.03 (#441). TlsStream.c grew by 24 lines in slices 2-7 (per-helper emits in InitSslContext, InitSslSession, AttachTransportBio, ConfigureExpectedHostname, two PerformHandshake exit branches; unwind tail in Open). Two existing suppressions for the file moved to new line numbers: 70 -> 74 (11.3, SelfFromBase cast) 16 -> 20 (5.7, anonymous-enum opener) No new suppressions; no new MISRA violations introduced by the unwind or the emission helpers. Verified with cppcheck-misra against the full CI include set — clean exit, matches main baseline. --- misra_suppressions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/misra_suppressions.txt b/misra_suppressions.txt index fdd0bb9d..09225d8d 100644 --- a/misra_suppressions.txt +++ b/misra_suppressions.txt @@ -45,7 +45,7 @@ misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosMutex.c:40 misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosResolver.c:44 misra-c2012-11.3:Platform/FreeRtos/Source/SolidSyslogFreeRtosTcpStream.c:85 misra-c2012-11.3:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:64 -misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:70 +misra-c2012-11.3:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:74 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddress.c:10 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressPrivate.h:20 misra-c2012-11.3:Platform/Posix/Source/SolidSyslogPosixAddressPrivate.h:26 @@ -119,7 +119,7 @@ misra-c2012-5.7:Core/Source/SolidSyslogUdpPayload.c:5 misra-c2012-5.7:Platform/FreeRtos/Source/SolidSyslogFreeRtosResolver.c:21 misra-c2012-5.7:Platform/FreeRtos/Source/SolidSyslogFreeRtosTcpStream.c:27 misra-c2012-5.7:Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c:18 -misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:16 +misra-c2012-5.7:Platform/OpenSsl/Source/SolidSyslogTlsStream.c:20 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogGetAddrInfoResolver.c:20 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixDatagram.c:21 misra-c2012-5.7:Platform/Posix/Source/SolidSyslogPosixSleep.c:6 From 9b2437d320824b53645d28c26940115a65b7a3ea Mon Sep 17 00:00:00 2001 From: David Cozens Date: Sun, 24 May 2026 14:57:13 +0000 Subject: [PATCH 9/9] docs: update DEVLOG for S26.03 OpenSSL Open unwind --- DEVLOG.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index 422552e3..a97fc6df 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -11811,3 +11811,75 @@ MISRA rule — different category, doesn't set precedent. ### Open questions - None. + +## 2026-05-24 — S26.03 TlsStream Open unwinds + per-failure-point error reporting (OpenSSL parity of S26.02) + +### Decisions + +- **Five codes, matching S26.02's coarse grain.** OpenSSL has more + configurable surfaces in the context (trust anchors, client identity, + protocol floor, cipher list — each independently failing with an + int return) than mbedTLS has, so a literal one-code-per-failing-API + expansion would have yielded 8-9 codes. Consolidated under + `TLSSTREAM_ERROR_CONTEXT_INIT_FAILED` so the integrator gets the + same diagnostic granularity as the MbedTls adapter. Finer codes + (TRUST_ANCHORS_NOT_LOADED, CLIENT_IDENTITY_INVALID, + CIPHER_LIST_REJECTED) are a future enhancement story if any + integrator surfaces a need. +- **One emit site per bool-returning helper, shared code for the + SSL/BIO alloc pair.** `InitSslContext`, `InitSslSession`, + `AttachTransportBio`, `ConfigureExpectedHostname`, and the two + `PerformHandshake` exit branches each emit at the point of failure + detection. `InitSslSession` and `AttachTransportBio` share + `SESSION_INIT_FAILED` — both are alloc-class failures the + integrator cannot triage finer than "TLS session resources couldn't + be allocated". +- **Per-mode resource-free assertions stay in the existing + XxxFailureFreesCtx tests.** The new `CHECK_OPEN_UNWOUND_WITH_ERROR` + macro pins the universal 5-assertion shape (transport closed, error + source, code, severity, count) without trying to pin SSL_CTX_free / + SSL_free counts — those vary by how far Open got before failing, so + they live in the existing per-mode tests that already pin them + precisely. +- **Existing test names kept.** S26.02 renamed `OpenFailsImmediately…` + to `OpenClosesTransportAndFreesSslState…` because the new tests had + to be added from scratch and the rename established the convention. + Here, ~15 existing `OpenReturnsFalseWhen…` tests already exist — + renaming all of them would be a big mechanical diff and the macro's + name (`CHECK_OPEN_UNWOUND_WITH_ERROR`) carries the assertion meaning. + Kept the existing names; extended the bodies. +- **`ReCreateStreamWithUpdatedConfig` helper added to the + TEST_GROUP.** Mirrors S26.02's `ReCreateHandleWithUpdatedConfig`. + Replaced inlined destroy/recreate-stream sequences in the eight + tests that needed a config tweak (CipherList, ClientCertChain/Key, + ServerName) — same six-line helper, same fixture-reset semantics + so the macro's `== 1` count assertions work. + +### Deferred + +- **Finer-grained CONTEXT_INIT codes** (TRUST_ANCHORS_NOT_LOADED, + CLIENT_IDENTITY_INVALID, CIPHER_LIST_REJECTED). Out of scope for + S26.03 by deliberate choice to maintain parity with S26.02. + Re-evaluate when an integrator asks for the granularity. +- **CodeRabbit cppcheck-misra include-set false positives.** Per the + feedback memory captured after S26.02 PR #440, expect any + "stale suppression" finding on the line shifts (`70 -> 74`, + `16 -> 20`) to be a false positive from the script running cppcheck + without `-IPlatform/OpenSsl/Interface`. Reproduce with the full + CI include set before agreeing. + +### Open questions + +- None. + +### Process notes (collateral) + +- **Lost David's local `.devcontainer/devcontainer.json` clang-service + switch.** A misguided `xargs clang-format -i` over + `git diff --name-only main...HEAD` mangled non-C/C++ files + (DEVLOG.md, misra_suppressions.txt, devcontainer.json). Reverted via + `git checkout --`, which also reverted David's uncommitted switch. + The switch has no functional impact in-session (we're already in the + clang container) but needs re-applying via the IDE before the next + Rebuild Container. Format pipeline going forward: filter + `git diff --name-only` for `.c|.cpp|.h|.hpp$` extensions.