From 346edabf8490effa8632349d9582789976940202 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 21 Apr 2026 23:31:08 +0100 Subject: [PATCH 01/33] test: S03.08 in-process TLS integration harness with real libssl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new OpenSslIntegrationTests executable that links real libssl (instead of OpenSslFake) and exercises SolidSyslogTlsStream end-to-end via an in-process BIO pair. This sits between unit tests (fast, fake-backed) and BDD (end-to-end via syslog-ng) in the testing pyramid, and will carry the SL4 failure-mode coverage in subsequent commits (expired cert, wrong CN/SAN, wrong CA, unsupported cipher). Harness components: - BioPairStream: SolidSyslogStream adapter over one side of a BIO pair with a cooperative pump callback, so SolidSyslogTlsStream's synchronous SSL_connect path works unchanged over non-blocking in-memory BIOs. - TlsTestCert: programmatic X509 cert generation (OpenSSL 3.0 EVP_RSA_gen), parameterised by CN, SAN DNS names, validity window, and optional issuer for self-signed or chain scenarios. - TlsTestServer: server-side SSL_CTX + SSL + BIO pair; Pump() drives SSL_accept one step at a time as the client-side handshake progresses. Smoke test HandshakeSucceedsAgainstTrustedServerCert verifies all four pieces wire together against a self-signed localhost cert. CI: new openssl-integration job runs in parallel with build-and-test, self-contained (own configure + target build), quality-monitor entry added. Clang and sanitize presets deliberately skip the integration suite — unit tests remain the coverage engine; integration is supplementary evidence. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 46 +++++++++ Tests/CMakeLists.txt | 4 + Tests/OpenSslIntegration/BioPairStream.c | 94 ++++++++++++++++++ Tests/OpenSslIntegration/BioPairStream.h | 20 ++++ Tests/OpenSslIntegration/CMakeLists.txt | 27 ++++++ .../SolidSyslogTlsStreamIntegrationTest.cpp | 68 +++++++++++++ Tests/OpenSslIntegration/TlsTestCert.c | 95 +++++++++++++++++++ Tests/OpenSslIntegration/TlsTestCert.h | 32 +++++++ Tests/OpenSslIntegration/TlsTestServer.c | 74 +++++++++++++++ Tests/OpenSslIntegration/TlsTestServer.h | 32 +++++++ Tests/OpenSslIntegration/main.cpp | 6 ++ 11 files changed, 498 insertions(+) create mode 100644 Tests/OpenSslIntegration/BioPairStream.c create mode 100644 Tests/OpenSslIntegration/BioPairStream.h create mode 100644 Tests/OpenSslIntegration/CMakeLists.txt create mode 100644 Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp create mode 100644 Tests/OpenSslIntegration/TlsTestCert.c create mode 100644 Tests/OpenSslIntegration/TlsTestCert.h create mode 100644 Tests/OpenSslIntegration/TlsTestServer.c create mode 100644 Tests/OpenSslIntegration/TlsTestServer.h create mode 100644 Tests/OpenSslIntegration/main.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f9890c9..652cc4e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,47 @@ jobs: retention-days: 1 compression-level: 0 + openssl-integration: + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + container: + image: ghcr.io/davidcozens/cpputest:sha-18f19e1 + options: --user root + env: + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: safe.directory + GIT_CONFIG_VALUE_0: '*' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Configure + run: cmake --preset debug + + - name: Build integration tests + run: cmake --build --preset debug --target OpenSslIntegrationTests + + - name: Run integration tests + run: cd build/debug && ./Tests/OpenSslIntegration/OpenSslIntegrationTests -v -ojunit -k OpenSslIntegrationTests + + - name: Test Report + uses: dorny/test-reporter@v3 + if: success() || failure() + with: + name: Test Results (OpenSSL Integration) + path: build/debug/cpputest_*.xml + reporter: java-junit + + - name: Upload JUnit XML + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: junit-openssl-integration + path: build/debug/cpputest_*.xml + retention-days: 1 + clang-build-and-test: runs-on: ubuntu-latest permissions: @@ -563,6 +604,11 @@ jobs: "name": "Unit Tests (Sanitize)", "pattern": "**/junit-sanitize/cpputest_*.xml" }, + { + "id": "junit", + "name": "Integration Tests (OpenSSL)", + "pattern": "**/junit-openssl-integration/cpputest_*.xml" + }, { "id": "junit", "name": "BDD Tests (Linux)", diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 50fa1399..0c60327d 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -112,6 +112,10 @@ if(SOLIDSYSLOG_WINSOCK) target_link_libraries(${PROJECT_NAME}Tests PRIVATE WinsockFakes) endif() +if(SOLIDSYSLOG_OPENSSL) + add_subdirectory(OpenSslIntegration) +endif() + # Standard CppUTest console output via ctest -V add_test(NAME ${PROJECT_NAME}Tests COMMAND ${PROJECT_NAME}Tests) diff --git a/Tests/OpenSslIntegration/BioPairStream.c b/Tests/OpenSslIntegration/BioPairStream.c new file mode 100644 index 00000000..a3866369 --- /dev/null +++ b/Tests/OpenSslIntegration/BioPairStream.c @@ -0,0 +1,94 @@ +#include "BioPairStream.h" +#include "SolidSyslogStreamDefinition.h" + +#include +#include +#include + +struct BioPairStream +{ + struct SolidSyslogStream base; + BIO* bio; + BioPairStreamPumpFunction pump; + void* pumpContext; +}; + +static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void Close(struct SolidSyslogStream* self); + +struct SolidSyslogStream* BioPairStream_Create(BIO* bio) +{ + struct BioPairStream* stream = (struct BioPairStream*) calloc(1, sizeof(struct BioPairStream)); + stream->base.Open = Open; + stream->base.Send = Send; + stream->base.Read = Read; + stream->base.Close = Close; + stream->bio = bio; + return &stream->base; +} + +void BioPairStream_Destroy(struct SolidSyslogStream* self) +{ + free(self); +} + +void BioPairStream_SetPump(struct SolidSyslogStream* self, BioPairStreamPumpFunction pump, void* context) +{ + struct BioPairStream* stream = (struct BioPairStream*) self; + stream->pump = pump; + stream->pumpContext = context; +} + +static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +{ + (void) self; + (void) addr; + return true; +} + +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +{ + struct BioPairStream* stream = (struct BioPairStream*) self; + int written = BIO_write(stream->bio, buffer, (int) size); + return written == (int) size; +} + +/* Reads from the BIO, driving the paired peer via the pump callback whenever the + * read would otherwise block. Models a blocking stream on top of a non-blocking + * BIO pair so SolidSyslogTlsStream's synchronous SSL_connect / SSL_read path + * works unchanged. */ +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) +{ + struct BioPairStream* stream = (struct BioPairStream*) self; + SolidSyslogSsize result = -1; + bool done = false; + while (!done) + { + int bytesRead = BIO_read(stream->bio, buffer, (int) size); + if (bytesRead > 0) + { + result = (SolidSyslogSsize) bytesRead; + done = true; + } + else if (!BIO_should_retry(stream->bio)) + { + done = true; + } + else if (stream->pump == NULL) + { + done = true; + } + else + { + stream->pump(stream->pumpContext); + } + } + return result; +} + +static void Close(struct SolidSyslogStream* self) +{ + (void) self; +} diff --git a/Tests/OpenSslIntegration/BioPairStream.h b/Tests/OpenSslIntegration/BioPairStream.h new file mode 100644 index 00000000..d99a413d --- /dev/null +++ b/Tests/OpenSslIntegration/BioPairStream.h @@ -0,0 +1,20 @@ +#ifndef BIOPAIRSTREAM_H +#define BIOPAIRSTREAM_H + +#include "ExternC.h" +#include "SolidSyslogStream.h" +#include + +EXTERN_C_BEGIN + + typedef void (*BioPairStreamPumpFunction)(void* context); + + struct SolidSyslogStream* BioPairStream_Create(BIO* bio); + void BioPairStream_Destroy(struct SolidSyslogStream* self); + void BioPairStream_SetPump(struct SolidSyslogStream* self, + BioPairStreamPumpFunction pump, + void* context); + +EXTERN_C_END + +#endif /* BIOPAIRSTREAM_H */ diff --git a/Tests/OpenSslIntegration/CMakeLists.txt b/Tests/OpenSslIntegration/CMakeLists.txt new file mode 100644 index 00000000..c4d0d41e --- /dev/null +++ b/Tests/OpenSslIntegration/CMakeLists.txt @@ -0,0 +1,27 @@ +# OpenSSL-backed integration tests: links the real libssl and exercises +# SolidSyslogTlsStream end-to-end against an in-process server. Separate binary +# from SolidSyslogTests because unit tests link OpenSslFakes in place of libssl +# — the two cannot share one executable without fake/real symbol collision. +add_executable(OpenSslIntegrationTests + BioPairStream.c + TlsTestCert.c + TlsTestServer.c + SolidSyslogTlsStreamIntegrationTest.cpp + main.cpp +) + +target_link_libraries(OpenSslIntegrationTests PRIVATE + ${PROJECT_NAME} + CppUTest + CppUTestExt + OpenSSL::SSL + OpenSSL::Crypto + Threads::Threads +) + +target_include_directories(OpenSslIntegrationTests PRIVATE + ${CMAKE_SOURCE_DIR}/Core/Source + ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_test(NAME OpenSslIntegrationTests COMMAND OpenSslIntegrationTests) diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp new file mode 100644 index 00000000..b91744a0 --- /dev/null +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -0,0 +1,68 @@ +#include "BioPairStream.h" +#include "CppUTest/TestHarness.h" +#include "SolidSyslogAddress.h" +#include "SolidSyslogStream.h" +#include "SolidSyslogTlsStream.h" +#include "TlsTestCert.h" +#include "TlsTestServer.h" + +#include +#include +#include +#include + +// clang-format off +TEST_GROUP(TlsStreamIntegration) +{ + struct TlsTestCert cert = {}; + struct TlsTestServer* server = nullptr; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogTlsStreamConfig tlsConfig = {}; + struct SolidSyslogStream* tlsStream = nullptr; + SolidSyslogAddressStorage addrStorage = {}; + struct SolidSyslogAddress* addr = nullptr; + char caPath[64] = {}; + + void setup() override + { + static const char* const sans[] = { "localhost", nullptr }; + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = sans; + TlsTestCert_Create(&certConfig, &cert); + + std::strcpy(caPath, "/tmp/solidsyslog_tls_ca_XXXXXX"); + int fd = mkstemp(caPath); + close(fd); + TlsTestCert_WritePemToFile(&cert, caPath); + + struct TlsTestServerConfig serverConfig = {}; + serverConfig.serverCert = &cert; + server = TlsTestServer_Create(&serverConfig); + + transport = BioPairStream_Create(TlsTestServer_ClientSideBio(server)); + BioPairStream_SetPump(transport, TlsTestServer_Pump, server); + + tlsConfig.transport = transport; + tlsConfig.caBundlePath = caPath; + tlsConfig.serverName = "localhost"; + tlsStream = SolidSyslogTlsStream_Create(&tlsConfig); + + addr = SolidSyslogAddress_FromStorage(&addrStorage); + } + + void teardown() override + { + SolidSyslogTlsStream_Destroy(); + BioPairStream_Destroy(transport); + TlsTestServer_Destroy(server); + TlsTestCert_Destroy(&cert); + unlink(caPath); + } +}; +// clang-format on + +TEST(TlsStreamIntegration, HandshakeSucceedsAgainstTrustedServerCert) +{ + CHECK_TRUE(SolidSyslogStream_Open(tlsStream, addr)); +} diff --git a/Tests/OpenSslIntegration/TlsTestCert.c b/Tests/OpenSslIntegration/TlsTestCert.c new file mode 100644 index 00000000..e041cbc4 --- /dev/null +++ b/Tests/OpenSslIntegration/TlsTestCert.c @@ -0,0 +1,95 @@ +#include "TlsTestCert.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TLS_TEST_CERT_RSA_BITS 2048 +#define TLS_TEST_CERT_DEFAULT_VALIDITY_SECONDS 3600 + +static void SetValidity(X509* cert, time_t notBefore, time_t notAfter); +static void SetSubject(X509* cert, const char* commonName); +static void AddSubjectAltNames(X509* cert, const char* const* dnsNames); + +void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out) +{ + EVP_PKEY* key = EVP_RSA_gen(TLS_TEST_CERT_RSA_BITS); + X509* cert = X509_new(); + + X509_set_version(cert, 2); /* X.509 v3 */ + ASN1_INTEGER_set(X509_get_serialNumber(cert), 1); + + SetValidity(cert, config->notBefore, config->notAfter); + SetSubject(cert, config->commonName); + AddSubjectAltNames(cert, config->subjectAltDnsNames); + + X509* issuerCert = (config->issuer != NULL) ? config->issuer->cert : cert; + EVP_PKEY* issuerKey = (config->issuer != NULL) ? config->issuer->key : key; + X509_set_issuer_name(cert, X509_get_subject_name(issuerCert)); + X509_set_pubkey(cert, key); + X509_sign(cert, issuerKey, EVP_sha256()); + + out->cert = cert; + out->key = key; +} + +void TlsTestCert_Destroy(struct TlsTestCert* cert) +{ + if (cert->cert != NULL) + { + X509_free(cert->cert); + cert->cert = NULL; + } + if (cert->key != NULL) + { + EVP_PKEY_free(cert->key); + cert->key = NULL; + } +} + +void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path) +{ + FILE* file = fopen(path, "w"); + PEM_write_X509(file, cert->cert); + fclose(file); +} + +static void SetValidity(X509* cert, time_t notBefore, time_t notAfter) +{ + time_t now = time(NULL); + time_t start = (notBefore != 0) ? notBefore : now; + time_t end = (notAfter != 0) ? notAfter : now + TLS_TEST_CERT_DEFAULT_VALIDITY_SECONDS; + X509_time_adj_ex(X509_getm_notBefore(cert), 0, 0, &start); + X509_time_adj_ex(X509_getm_notAfter(cert), 0, 0, &end); +} + +static void SetSubject(X509* cert, const char* commonName) +{ + X509_NAME* name = X509_get_subject_name(cert); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char*) commonName, -1, -1, 0); +} + +static void AddSubjectAltNames(X509* cert, const char* const* dnsNames) +{ + if (dnsNames == NULL) + { + return; + } + STACK_OF(GENERAL_NAME)* sans = sk_GENERAL_NAME_new_null(); + for (int i = 0; dnsNames[i] != NULL; i++) + { + GENERAL_NAME* gen = GENERAL_NAME_new(); + ASN1_IA5STRING* str = ASN1_IA5STRING_new(); + ASN1_STRING_set(str, dnsNames[i], -1); + GENERAL_NAME_set0_value(gen, GEN_DNS, str); + sk_GENERAL_NAME_push(sans, gen); + } + X509_add1_ext_i2d(cert, NID_subject_alt_name, sans, 0, 0); + sk_GENERAL_NAME_pop_free(sans, GENERAL_NAME_free); +} diff --git a/Tests/OpenSslIntegration/TlsTestCert.h b/Tests/OpenSslIntegration/TlsTestCert.h new file mode 100644 index 00000000..71fef052 --- /dev/null +++ b/Tests/OpenSslIntegration/TlsTestCert.h @@ -0,0 +1,32 @@ +#ifndef TLSTESTCERT_H +#define TLSTESTCERT_H + +#include "ExternC.h" +#include +#include +#include + +EXTERN_C_BEGIN + + struct TlsTestCert + { + X509* cert; + EVP_PKEY* key; + }; + + struct TlsTestCertConfig + { + const char* commonName; + const char* const* subjectAltDnsNames; /* NULL-terminated array; NULL if no SAN */ + time_t notBefore; /* 0 = now */ + time_t notAfter; /* 0 = now + 3600 */ + const struct TlsTestCert* issuer; /* NULL = self-signed */ + }; + + void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out); + void TlsTestCert_Destroy(struct TlsTestCert* cert); + void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path); + +EXTERN_C_END + +#endif /* TLSTESTCERT_H */ diff --git a/Tests/OpenSslIntegration/TlsTestServer.c b/Tests/OpenSslIntegration/TlsTestServer.c new file mode 100644 index 00000000..65e43e64 --- /dev/null +++ b/Tests/OpenSslIntegration/TlsTestServer.c @@ -0,0 +1,74 @@ +#include "TlsTestServer.h" + +#include +#include +#include +#include + +struct TlsTestServer +{ + SSL_CTX* ctx; + SSL* ssl; + BIO* serverBio; + BIO* clientBio; + bool handshakeComplete; +}; + +struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* config) +{ + struct TlsTestServer* self = (struct TlsTestServer*) calloc(1, sizeof(struct TlsTestServer)); + + self->ctx = SSL_CTX_new(TLS_server_method()); + SSL_CTX_use_certificate(self->ctx, config->serverCert->cert); + SSL_CTX_use_PrivateKey(self->ctx, config->serverCert->key); + if (config->cipherList != NULL) + { + SSL_CTX_set_cipher_list(self->ctx, config->cipherList); + } + + self->ssl = SSL_new(self->ctx); + BIO_new_bio_pair(&self->serverBio, 0, &self->clientBio, 0); + SSL_set_bio(self->ssl, self->serverBio, self->serverBio); + SSL_set_accept_state(self->ssl); + + return self; +} + +void TlsTestServer_Destroy(struct TlsTestServer* self) +{ + if (self == NULL) + { + return; + } + if (self->ssl != NULL) + { + SSL_free(self->ssl); /* also frees serverBio */ + } + if (self->clientBio != NULL) + { + BIO_free(self->clientBio); + } + if (self->ctx != NULL) + { + SSL_CTX_free(self->ctx); + } + free(self); +} + +BIO* TlsTestServer_ClientSideBio(struct TlsTestServer* self) +{ + return self->clientBio; +} + +void TlsTestServer_Pump(void* context) +{ + struct TlsTestServer* self = (struct TlsTestServer*) context; + if (!self->handshakeComplete) + { + int ret = SSL_accept(self->ssl); + if (ret == 1) + { + self->handshakeComplete = true; + } + } +} diff --git a/Tests/OpenSslIntegration/TlsTestServer.h b/Tests/OpenSslIntegration/TlsTestServer.h new file mode 100644 index 00000000..627004ef --- /dev/null +++ b/Tests/OpenSslIntegration/TlsTestServer.h @@ -0,0 +1,32 @@ +#ifndef TLSTESTSERVER_H +#define TLSTESTSERVER_H + +#include "ExternC.h" +#include "TlsTestCert.h" +#include + +EXTERN_C_BEGIN + + struct TlsTestServer; + + struct TlsTestServerConfig + { + const struct TlsTestCert* serverCert; /* includes matching private key */ + const char* cipherList; /* NULL = server default */ + }; + + struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* config); + void TlsTestServer_Destroy(struct TlsTestServer* self); + + /* Returns the client-facing end of the internal BIO pair. Pass to + * BioPairStream_Create and use as the transport for SolidSyslogTlsStream. */ + BIO* TlsTestServer_ClientSideBio(struct TlsTestServer* self); + + /* Advances the server's state machine one step. Used as a pump callback + * so client-side reads can make cooperative progress. Context is the + * TlsTestServer pointer. */ + void TlsTestServer_Pump(void* context); + +EXTERN_C_END + +#endif /* TLSTESTSERVER_H */ diff --git a/Tests/OpenSslIntegration/main.cpp b/Tests/OpenSslIntegration/main.cpp new file mode 100644 index 00000000..a185088c --- /dev/null +++ b/Tests/OpenSslIntegration/main.cpp @@ -0,0 +1,6 @@ +#include "CppUTest/CommandLineTestRunner.h" + +int main(int argc, char** argv) +{ + return CommandLineTestRunner::RunAllTests(argc, argv); +} From 2cda61810f2578a9f63f8453a8c9b667c6642642 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 21 Apr 2026 23:52:21 +0100 Subject: [PATCH 02/33] test: S03.08 integration tests for TLS handshake rejection paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three harness-based characterisation tests confirming the library's existing SSL_VERIFY_PEER plus SSL_set1_host wiring rejects the three SL4 failure modes named in the ticket acceptance: - HandshakeRejectedWhenServerCertIsExpired — cert notAfter in the past - HandshakeRejectedWhenServerCertHostnameDoesNotMatch — CN/SAN mismatch - HandshakeRejectedWhenClientDoesNotTrustServerCert — CA not in trust All three go green on first write — no production change was driven. The tests still earn their keep as regression protection: a future change that disabled SSL_VERIFY_PEER or dropped SSL_set1_host would turn them red. Setup refactor alongside: extracted buildScenario() helper from setup() so each scenario parameterises the cert config (commonName, SANs, validity window) without reconstructing the whole harness. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SolidSyslogTlsStreamIntegrationTest.cpp | 102 +++++++++++++----- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp index b91744a0..5f858d0b 100644 --- a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -9,28 +9,39 @@ #include #include #include +#include #include // clang-format off TEST_GROUP(TlsStreamIntegration) { - struct TlsTestCert cert = {}; - struct TlsTestServer* server = nullptr; - struct SolidSyslogStream* transport = nullptr; - struct SolidSyslogTlsStreamConfig tlsConfig = {}; - struct SolidSyslogStream* tlsStream = nullptr; - SolidSyslogAddressStorage addrStorage = {}; - struct SolidSyslogAddress* addr = nullptr; - char caPath[64] = {}; + struct TlsTestCert cert = {}; + struct TlsTestServer* server = nullptr; + struct SolidSyslogStream* transport = nullptr; + struct SolidSyslogTlsStreamConfig tlsConfig = {}; + struct SolidSyslogStream* tlsStream = nullptr; + SolidSyslogAddressStorage addrStorage = {}; + struct SolidSyslogAddress* addr = nullptr; + char caPath[64] = {}; void setup() override { - static const char* const sans[] = { "localhost", nullptr }; - struct TlsTestCertConfig certConfig = {}; - certConfig.commonName = "localhost"; - certConfig.subjectAltDnsNames = sans; - TlsTestCert_Create(&certConfig, &cert); + addr = SolidSyslogAddress_FromStorage(&addrStorage); + } + + void teardown() override + { + if (tlsStream != nullptr) { SolidSyslogTlsStream_Destroy(); } + if (transport != nullptr) { BioPairStream_Destroy(transport); } + if (server != nullptr) { TlsTestServer_Destroy(server); } + if (cert.cert != nullptr) { TlsTestCert_Destroy(&cert); } + if (caPath[0] != '\0') { unlink(caPath); } + } + void buildScenario(const struct TlsTestCertConfig& certConfig, + const char* clientServerName = "localhost") + { + TlsTestCert_Create(&certConfig, &cert); std::strcpy(caPath, "/tmp/solidsyslog_tls_ca_XXXXXX"); int fd = mkstemp(caPath); close(fd); @@ -45,24 +56,65 @@ TEST_GROUP(TlsStreamIntegration) tlsConfig.transport = transport; tlsConfig.caBundlePath = caPath; - tlsConfig.serverName = "localhost"; + tlsConfig.serverName = clientServerName; tlsStream = SolidSyslogTlsStream_Create(&tlsConfig); - - addr = SolidSyslogAddress_FromStorage(&addrStorage); - } - - void teardown() override - { - SolidSyslogTlsStream_Destroy(); - BioPairStream_Destroy(transport); - TlsTestServer_Destroy(server); - TlsTestCert_Destroy(&cert); - unlink(caPath); } }; // clang-format on +static const char* const LOCALHOST_SANS[] = {"localhost", nullptr}; + TEST(TlsStreamIntegration, HandshakeSucceedsAgainstTrustedServerCert) { + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(certConfig); + CHECK_TRUE(SolidSyslogStream_Open(tlsStream, addr)); } + +TEST(TlsStreamIntegration, HandshakeRejectedWhenServerCertIsExpired) +{ + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = LOCALHOST_SANS; + certConfig.notBefore = std::time(nullptr) - 7200; + certConfig.notAfter = std::time(nullptr) - 3600; + buildScenario(certConfig); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); +} + +TEST(TlsStreamIntegration, HandshakeRejectedWhenServerCertHostnameDoesNotMatch) +{ + static const char* const otherSans[] = {"someone-else.example", nullptr}; + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "someone-else.example"; + certConfig.subjectAltDnsNames = otherSans; + buildScenario(certConfig); /* client.serverName defaults to "localhost" */ + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); +} + +TEST(TlsStreamIntegration, HandshakeRejectedWhenClientDoesNotTrustServerCert) +{ + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = LOCALHOST_SANS; + buildScenario(certConfig); + + /* Overwrite the client's trust file with an unrelated self-signed cert + * so the server's cert is no longer anchored in the trust store. The CA + * file is loaded on Open, so this replacement takes effect for the next + * handshake attempt. */ + struct TlsTestCertConfig untrustedConfig = {}; + untrustedConfig.commonName = "some-other-entity.example"; + struct TlsTestCert untrusted = {}; + TlsTestCert_Create(&untrustedConfig, &untrusted); + TlsTestCert_WritePemToFile(&untrusted, caPath); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); + + TlsTestCert_Destroy(&untrusted); +} From 321fbc526ca4f1a9b347dc3a34c212702b7983b4 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 21 Apr 2026 23:57:30 +0100 Subject: [PATCH 03/33] fix: S03.08 block handshake when SSL_set1_host fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSL_set1_host configures the expected-hostname check that makes SSL_VERIFY_PEER reject a cert whose subject doesn't match the serverName. If set1_host fails silently (returns 0), the client still runs SSL_connect but without the hostname check — a MITM risk, since any trust-chain-valid cert from any host in the trust store would then be accepted. Return-value check now aborts Open instead. Driven by OpenReturnsFalseWhenSet1HostFails in the fake-based unit suite; OpenSslFake gains a SetSet1HostFails switch following the same pattern as SetConnectFails / SetWriteFails. Flagged in PR #170 review — first of the return-value-checking bullets in the S03.08 acceptance list. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 5 ++++- Tests/SolidSyslogTlsStreamTest.cpp | 9 +++++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index ac5e8ce9..ffc5e7c0 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -58,7 +58,10 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress if (stream->config.serverName != NULL) { SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName); - SSL_set1_host(stream->ssl, stream->config.serverName); + if (SSL_set1_host(stream->ssl, stream->config.serverName) != 1) + { + return false; + } } return SSL_connect(stream->ssl) > 0; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 56fb3445..afdc4308 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -401,6 +401,15 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenHandshakeFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSet1HostFails) +{ + SolidSyslogTlsStream_Destroy(); + config.serverName = "logs.example"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetSet1HostFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index b083a9bd..597b177a 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -70,6 +70,7 @@ static const char* lastSniHostname; /* SSL_set1_host */ static SSL* lastSet1HostSslArg; static const char* lastSet1Host; +static bool set1HostFails; /* SSL_connect */ static int connectCallCount; @@ -137,6 +138,7 @@ void OpenSslFake_Reset(void) lastSniHostname = NULL; lastSet1HostSslArg = NULL; lastSet1Host = NULL; + set1HostFails = false; connectCallCount = 0; lastConnectSslArg = NULL; connectFails = false; @@ -554,7 +556,12 @@ int SSL_set1_host(SSL* ssl, const char* hostname) { lastSet1HostSslArg = ssl; lastSet1Host = hostname; - return 1; + return set1HostFails ? 0 : 1; +} + +void OpenSslFake_SetSet1HostFails(bool fails) +{ + set1HostFails = fails; } int SSL_connect(SSL* ssl) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index dc1e5b96..89faafa6 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -19,6 +19,7 @@ EXTERN_C_BEGIN /* Failure-mode switches */ void OpenSslFake_SetConnectFails(bool fails); void OpenSslFake_SetWriteFails(bool fails); + void OpenSslFake_SetSet1HostFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From e993666a699f546555cd03143ca18163fc24cd6e Mon Sep 17 00:00:00 2001 From: David Cozens Date: Tue, 21 Apr 2026 23:58:22 +0100 Subject: [PATCH 04/33] fix: S03.08 block handshake when SNI hostname setup fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallel fix to the SSL_set1_host return-value check: SSL_set_tlsext_host_name (the SNI extension setter) can also fail silently. If it does, the client proceeds with SSL_connect but without advertising the intended hostname to the server — the server may then present a default certificate that the client's SSL_set1_host check accepts for a different tenant. Return-value check now aborts Open instead. Driven by OpenReturnsFalseWhenSniHostnameSetupFails; OpenSslFake gains a SetSniHostnameFails switch on the SSL_ctrl SET_TLSEXT_HOSTNAME path. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 5 ++++- Tests/SolidSyslogTlsStreamTest.cpp | 9 +++++++++ Tests/Support/OpenSslFake.c | 8 ++++++++ Tests/Support/OpenSslFake.h | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index ffc5e7c0..73409a5a 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -57,7 +57,10 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress SSL_set_bio(stream->ssl, bio, bio); if (stream->config.serverName != NULL) { - SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName); + if (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) != 1) + { + return false; + } if (SSL_set1_host(stream->ssl, stream->config.serverName) != 1) { return false; diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index afdc4308..0d448aac 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -410,6 +410,15 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSet1HostFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSniHostnameSetupFails) +{ + SolidSyslogTlsStream_Destroy(); + config.serverName = "logs.example"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetSniHostnameFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 597b177a..d9814990 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -66,6 +66,7 @@ static BIO* lastSetBioWriteBioArg; /* SSL_ctrl (SET_TLSEXT_HOSTNAME) */ static SSL* lastSslCtrlSslArg; static const char* lastSniHostname; +static bool sniHostnameFails; /* SSL_set1_host */ static SSL* lastSet1HostSslArg; @@ -136,6 +137,7 @@ void OpenSslFake_Reset(void) lastSetBioWriteBioArg = NULL; lastSslCtrlSslArg = NULL; lastSniHostname = NULL; + sniHostnameFails = false; lastSet1HostSslArg = NULL; lastSet1Host = NULL; set1HostFails = false; @@ -548,10 +550,16 @@ long SSL_ctrl(SSL* ssl, int cmd, long larg, void* parg) if (cmd == SSL_CTRL_SET_TLSEXT_HOSTNAME) { lastSniHostname = (const char*) parg; + return sniHostnameFails ? 0 : 1; } return 1; } +void OpenSslFake_SetSniHostnameFails(bool fails) +{ + sniHostnameFails = fails; +} + int SSL_set1_host(SSL* ssl, const char* hostname) { lastSet1HostSslArg = ssl; diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 89faafa6..d1a8072f 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -20,6 +20,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetConnectFails(bool fails); void OpenSslFake_SetWriteFails(bool fails); void OpenSslFake_SetSet1HostFails(bool fails); + void OpenSslFake_SetSniHostnameFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From a28759293d2b91d142d2460e96ed3cdde0fe5733 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 00:00:12 +0100 Subject: [PATCH 05/33] fix: S03.08 abort Open when SSL_CTX_new returns NULL Previously Open silently proceeded with a NULL SSL_CTX, which would crash on the next libssl call (SSL_CTX_load_verify_locations or SSL_CTX_set_verify deref the CTX). Under fakes this looked like a successful handshake. Either way the library was lying about the Open result. CreateSslContext now returns NULL immediately if SSL_CTX_new fails, and Open treats a NULL CTX as Open failure. Driven by OpenReturnsFalseWhenCtxNewFails; OpenSslFake gains SetCtxNewFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 8 ++++++++ Tests/SolidSyslogTlsStreamTest.cpp | 6 ++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 73409a5a..548fa11e 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -51,6 +51,10 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress return false; } stream->ctx = CreateSslContext(stream->config.caBundlePath); + if (stream->ctx == NULL) + { + return false; + } stream->ssl = SSL_new(stream->ctx); BIO* bio = CreateTransportBio(); BIO_set_data(bio, stream->config.transport); @@ -143,6 +147,10 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) static SSL_CTX* CreateSslContext(const char* caBundlePath) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); + if (ctx == NULL) + { + return NULL; + } SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL); SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 0d448aac..80ea6d7e 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -419,6 +419,12 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSniHostnameSetupFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCtxNewFails) +{ + OpenSslFake_SetCtxNewFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index d9814990..38ed9fba 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -22,6 +22,7 @@ static char fakeBioStorage; /* SSL_CTX_new */ static int ctxNewCallCount; static const SSL_METHOD* lastCtxNewMethodArg; +static bool ctxNewFails; /* SSL_CTX_load_verify_locations */ static SSL_CTX* lastLoadVerifyLocationsCtxArg; @@ -111,6 +112,7 @@ void OpenSslFake_Reset(void) { ctxNewCallCount = 0; lastCtxNewMethodArg = NULL; + ctxNewFails = false; lastLoadVerifyLocationsCtxArg = NULL; lastCaBundlePath = NULL; lastSetVerifyCtxArg = NULL; @@ -432,7 +434,12 @@ SSL_CTX* SSL_CTX_new(const SSL_METHOD* method) { ctxNewCallCount++; lastCtxNewMethodArg = method; - return (SSL_CTX*) &fakeCtxStorage; + return ctxNewFails ? NULL : (SSL_CTX*) &fakeCtxStorage; +} + +void OpenSslFake_SetCtxNewFails(bool fails) +{ + ctxNewFails = fails; } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by OpenSSL API diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index d1a8072f..2c33b1b5 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -21,6 +21,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetWriteFails(bool fails); void OpenSslFake_SetSet1HostFails(bool fails); void OpenSslFake_SetSniHostnameFails(bool fails); + void OpenSslFake_SetCtxNewFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From 9dbb645d79b40665a1c251f96578fd2d817c3ff8 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 00:01:04 +0100 Subject: [PATCH 06/33] fix: S03.08 abort Open when SSL_new returns NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the SSL_CTX_new guard: if SSL_new returns NULL, the subsequent SSL_set_bio / SSL_ctrl / SSL_set1_host / SSL_connect all deref the NULL pointer. Check the return and bail early. The SSL_CTX allocated on the line above is released by Destroy — the caller is expected to call Destroy after a failed Open, which Destroy already handles. No new cleanup needed here; a broader lifecycle sweep is scheduled for Phase 4. Driven by OpenReturnsFalseWhenSslNewFails; OpenSslFake gains SetSslNewFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 6 +++++- Tests/SolidSyslogTlsStreamTest.cpp | 6 ++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 548fa11e..9efa1b2b 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -56,7 +56,11 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress return false; } stream->ssl = SSL_new(stream->ctx); - BIO* bio = CreateTransportBio(); + if (stream->ssl == NULL) + { + return false; + } + BIO* bio = CreateTransportBio(); BIO_set_data(bio, stream->config.transport); SSL_set_bio(stream->ssl, bio, bio); if (stream->config.serverName != NULL) diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 80ea6d7e..ac6dbf9a 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -425,6 +425,12 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCtxNewFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSslNewFails) +{ + OpenSslFake_SetSslNewFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 38ed9fba..d6fa4827 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -39,6 +39,7 @@ static long lastMinProtoVersion; /* SSL_new */ static int sslNewCallCount; static SSL_CTX* lastSslNewCtxArg; +static bool sslNewFails; /* BIO_meth_set_read / BIO_meth_set_write */ static BIO_METHOD* lastBioMethSetReadMethodArg; @@ -121,6 +122,7 @@ void OpenSslFake_Reset(void) lastMinProtoVersion = 0; sslNewCallCount = 0; lastSslNewCtxArg = NULL; + sslNewFails = false; lastBioMethSetReadMethodArg = NULL; lastBioReadCallback = NULL; lastBioCtrlCallback = NULL; @@ -476,7 +478,12 @@ SSL* SSL_new(SSL_CTX* ctx) { sslNewCallCount++; lastSslNewCtxArg = ctx; - return (SSL*) &fakeSslStorage; + return sslNewFails ? NULL : (SSL*) &fakeSslStorage; +} + +void OpenSslFake_SetSslNewFails(bool fails) +{ + sslNewFails = fails; } BIO_METHOD* BIO_meth_new(int type, const char* name) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 2c33b1b5..7506a11e 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -22,6 +22,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetSet1HostFails(bool fails); void OpenSslFake_SetSniHostnameFails(bool fails); void OpenSslFake_SetCtxNewFails(bool fails); + void OpenSslFake_SetSslNewFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From c7ddb1510b1705cb2494b8f5ea82a72452f61c9d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 00:02:41 +0100 Subject: [PATCH 07/33] fix: S03.08 abort Open when SSL_CTX_load_verify_locations fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the caller's CA bundle file is missing, unreadable, or malformed, libssl refuses to load any trust anchors. Continuing would make the handshake fail anyway, but failing fast surfaces the misconfiguration cleanly. First of the return-value checks that needs in-function cleanup — CreateSslContext frees the CTX it just allocated before returning NULL, so the caller doesn't have to know about the partial state. Driven by two tests: one for the false return (Open fails), one for the CTX_free call count (no leak). OpenSslFake gains SetLoadVerifyLocationsFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 6 +++++- Tests/SolidSyslogTlsStreamTest.cpp | 13 +++++++++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 9efa1b2b..aa404a01 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -155,7 +155,11 @@ static SSL_CTX* CreateSslContext(const char* caBundlePath) { return NULL; } - SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL); + if (SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) != 1) + { + SSL_CTX_free(ctx); + return NULL; + } SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); return ctx; diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index ac6dbf9a..4a48c595 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -431,6 +431,19 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenSslNewFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenLoadVerifyLocationsFails) +{ + OpenSslFake_SetLoadVerifyLocationsFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, LoadVerifyLocationsFailureFreesCtx) +{ + OpenSslFake_SetLoadVerifyLocationsFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index d6fa4827..25c980f9 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -27,6 +27,7 @@ static bool ctxNewFails; /* SSL_CTX_load_verify_locations */ static SSL_CTX* lastLoadVerifyLocationsCtxArg; static const char* lastCaBundlePath; +static bool loadVerifyLocationsFails; /* SSL_CTX_set_verify */ static SSL_CTX* lastSetVerifyCtxArg; @@ -116,6 +117,7 @@ void OpenSslFake_Reset(void) ctxNewFails = false; lastLoadVerifyLocationsCtxArg = NULL; lastCaBundlePath = NULL; + loadVerifyLocationsFails = false; lastSetVerifyCtxArg = NULL; lastVerifyMode = 0; lastSslCtxCtrlCtxArg = NULL; @@ -450,7 +452,12 @@ int SSL_CTX_load_verify_locations(SSL_CTX* ctx, const char* CAfile, const char* (void) CApath; lastLoadVerifyLocationsCtxArg = ctx; lastCaBundlePath = CAfile; - return 1; + return loadVerifyLocationsFails ? 0 : 1; +} + +void OpenSslFake_SetLoadVerifyLocationsFails(bool fails) +{ + loadVerifyLocationsFails = fails; } void SSL_CTX_set_verify(SSL_CTX* ctx, int mode, SSL_verify_cb verify_callback) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 7506a11e..3631883c 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -23,6 +23,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetSniHostnameFails(bool fails); void OpenSslFake_SetCtxNewFails(bool fails); void OpenSslFake_SetSslNewFails(bool fails); + void OpenSslFake_SetLoadVerifyLocationsFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From 6214b75785c4ca093e59a49d1ff7190a867a1e33 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 00:03:29 +0100 Subject: [PATCH 08/33] fix: S03.08 abort Open when TLS 1.2 floor cannot be set SSL_CTX_set_min_proto_version is a ctrl macro that can fail if libssl rejects the version constant (build-time configuration mismatch or future-version rejection). Treating it as non-failing means the library could silently fall through to a lower protocol floor, breaking SL4 assumptions about TLS 1.2 minimum. CreateSslContext now returns NULL and frees the CTX it allocated on SET_MIN_PROTO_VERSION failure, matching the load_verify_locations path. Last of the CreateSslContext return value checks. Driven by OpenReturnsFalseWhenMinProtoVersionFails and MinProtoVersionFailureFreesCtx; OpenSslFake gains SetMinProtoVersionFails on the SSL_CTX_ctrl SET_MIN_PROTO_VERSION command. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 6 +++++- Tests/SolidSyslogTlsStreamTest.cpp | 13 +++++++++++++ Tests/Support/OpenSslFake.c | 8 ++++++++ Tests/Support/OpenSslFake.h | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index aa404a01..daebfceb 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -161,6 +161,10 @@ static SSL_CTX* CreateSslContext(const char* caBundlePath) return NULL; } SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) != 1) + { + SSL_CTX_free(ctx); + return NULL; + } return ctx; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 4a48c595..738b903a 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -444,6 +444,19 @@ TEST(SolidSyslogTlsStream, LoadVerifyLocationsFailureFreesCtx) LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenMinProtoVersionFails) +{ + OpenSslFake_SetMinProtoVersionFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, MinProtoVersionFailureFreesCtx) +{ + OpenSslFake_SetMinProtoVersionFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 25c980f9..86170ae9 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -36,6 +36,7 @@ static int lastVerifyMode; /* SSL_CTX_ctrl (SET_MIN_PROTO_VERSION) */ static SSL_CTX* lastSslCtxCtrlCtxArg; static long lastMinProtoVersion; +static bool minProtoVersionFails; /* SSL_new */ static int sslNewCallCount; @@ -122,6 +123,7 @@ void OpenSslFake_Reset(void) lastVerifyMode = 0; lastSslCtxCtrlCtxArg = NULL; lastMinProtoVersion = 0; + minProtoVersionFails = false; sslNewCallCount = 0; lastSslNewCtxArg = NULL; sslNewFails = false; @@ -477,10 +479,16 @@ long SSL_CTX_ctrl(SSL_CTX* ctx, int cmd, long larg, void* parg) if (cmd == SSL_CTRL_SET_MIN_PROTO_VERSION) { lastMinProtoVersion = larg; + return minProtoVersionFails ? 0 : 1; } return 1; } +void OpenSslFake_SetMinProtoVersionFails(bool fails) +{ + minProtoVersionFails = fails; +} + SSL* SSL_new(SSL_CTX* ctx) { sslNewCallCount++; diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 3631883c..70995568 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -24,6 +24,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetCtxNewFails(bool fails); void OpenSslFake_SetSslNewFails(bool fails); void OpenSslFake_SetLoadVerifyLocationsFails(bool fails); + void OpenSslFake_SetMinProtoVersionFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From 45ee8a00e9475983c0c46a6452f50123b08f9afb Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:33:50 +0100 Subject: [PATCH 09/33] fix: S03.08 free BIO_METHOD on Close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateTransportBio allocated a fresh BIO_METHOD per Open and lost the pointer after returning — a pure leak. Now the stream remembers the method pointer on the instance, and Close pairs the allocation with BIO_meth_free. Per-Open lifecycle matches the symmetry with SSL_CTX_new/SSL_free and SSL_new/SSL_free — one Open allocates a full set, one Close releases it. Driven by CloseFreesBioMethod; OpenSslFake gains a counted BIO_meth_free stub that was previously implicit (the method was never freed, so the fake didn't model the call). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 9 +++++--- Tests/SolidSyslogTlsStreamTest.cpp | 7 ++++++ Tests/Support/OpenSslFake.c | 22 +++++++++++++++++++ Tests/Support/OpenSslFake.h | 4 ++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index daebfceb..f401b386 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -9,6 +9,7 @@ struct SolidSyslogTlsStream struct SolidSyslogTlsStreamConfig config; SSL_CTX* ctx; SSL* ssl; + BIO_METHOD* bioMethod; }; static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); @@ -16,7 +17,7 @@ static bool Send(struct SolidSyslogStream* self, const void* buffer, static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); static SSL_CTX* CreateSslContext(const char* caBundlePath); -static BIO* CreateTransportBio(void); +static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); static int TransportBioRead(BIO* bio, char* buffer, int size); static int TransportBioWrite(BIO* bio, const char* buffer, int size); @@ -60,7 +61,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress { return false; } - BIO* bio = CreateTransportBio(); + BIO* bio = CreateTransportBio(stream); BIO_set_data(bio, stream->config.transport); SSL_set_bio(stream->ssl, bio, bio); if (stream->config.serverName != NULL) @@ -94,16 +95,18 @@ static void Close(struct SolidSyslogStream* self) struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; SSL_shutdown(stream->ssl); SSL_free(stream->ssl); + BIO_meth_free(stream->bioMethod); SolidSyslogStream_Close(stream->config.transport); } -static BIO* CreateTransportBio(void) +static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); BIO_meth_set_create(method, TransportBioCreate); BIO_meth_set_read(method, TransportBioRead); BIO_meth_set_write(method, TransportBioWrite); BIO_meth_set_ctrl(method, TransportBioCtrl); + stream->bioMethod = method; return BIO_new(method); } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 738b903a..73da6eee 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -271,6 +271,13 @@ TEST(SolidSyslogTlsStream, CloseClosesTransport) LONGS_EQUAL(1, StreamFake_CloseCallCount(transport)); } +TEST(SolidSyslogTlsStream, CloseFreesBioMethod) +{ + SolidSyslogStream_Open(stream, addr); + SolidSyslogStream_Close(stream); + LONGS_EQUAL(1, OpenSslFake_BioMethFreeCallCount()); +} + TEST(SolidSyslogTlsStream, DestroyFreesSslContext) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 86170ae9..706f4b42 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -43,6 +43,10 @@ static int sslNewCallCount; static SSL_CTX* lastSslNewCtxArg; static bool sslNewFails; +/* BIO_meth_free */ +static int bioMethFreeCallCount; +static BIO_METHOD* lastBioMethFreeArg; + /* BIO_meth_set_read / BIO_meth_set_write */ static BIO_METHOD* lastBioMethSetReadMethodArg; static int (*lastBioReadCallback)(BIO*, char*, int); @@ -127,6 +131,8 @@ void OpenSslFake_Reset(void) sslNewCallCount = 0; lastSslNewCtxArg = NULL; sslNewFails = false; + bioMethFreeCallCount = 0; + lastBioMethFreeArg = NULL; lastBioMethSetReadMethodArg = NULL; lastBioReadCallback = NULL; lastBioCtrlCallback = NULL; @@ -508,6 +514,22 @@ BIO_METHOD* BIO_meth_new(int type, const char* name) return (BIO_METHOD*) &fakeBioMethStorage; } +void BIO_meth_free(BIO_METHOD* biom) +{ + bioMethFreeCallCount++; + lastBioMethFreeArg = biom; +} + +int OpenSslFake_BioMethFreeCallCount(void) +{ + return bioMethFreeCallCount; +} + +BIO_METHOD* OpenSslFake_LastBioMethFreeArg(void) +{ + return lastBioMethFreeArg; +} + int BIO_meth_set_read(BIO_METHOD* biom, int (*read)(BIO*, char*, int)) { lastBioMethSetReadMethodArg = biom; diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 70995568..2fc4967e 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -51,6 +51,10 @@ EXTERN_C_BEGIN /* BIO_meth_new */ struct bio_method_st* OpenSslFake_LastBioMethReturned(void); + /* BIO_meth_free */ + int OpenSslFake_BioMethFreeCallCount(void); + struct bio_method_st* OpenSslFake_LastBioMethFreeArg(void); + /* BIO_meth_set_read */ struct bio_method_st* OpenSslFake_LastBioMethSetReadMethodArg(void); int (*OpenSslFake_LastBioReadCallback(void))(struct bio_st*, char*, int); From c43a59fcf81c7c06e623ead113db0be81768d807 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:35:43 +0100 Subject: [PATCH 10/33] fix: S03.08 BIO_METHOD freed exactly once across Close/Destroy Destroy now covers the case where Close wasn't called (e.g. Open partially failed, or the caller's lifecycle skipped Close), releasing the BIO_METHOD the way Close would have. Close in turn nulls the stored bioMethod pointer so a subsequent Destroy won't double-free. Two tests pin the contract: DestroyFreesBioMethodWhenCloseNotCalled and DestroyAfterCloseDoesNotDoubleFreeBioMethod. Together they say: BIO_METHOD is freed once, whether the lifecycle ended at Close, ended at Destroy, or went through both. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 6 ++++++ Tests/SolidSyslogTlsStreamTest.cpp | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index f401b386..e39a5f70 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -37,6 +37,11 @@ struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTl void SolidSyslogTlsStream_Destroy(void) { + if (instance.bioMethod != NULL) + { + BIO_meth_free(instance.bioMethod); + instance.bioMethod = NULL; + } if (instance.ctx != NULL) { SSL_CTX_free(instance.ctx); @@ -96,6 +101,7 @@ static void Close(struct SolidSyslogStream* self) SSL_shutdown(stream->ssl); SSL_free(stream->ssl); BIO_meth_free(stream->bioMethod); + stream->bioMethod = NULL; SolidSyslogStream_Close(stream->config.transport); } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 73da6eee..1a997aa2 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -286,6 +286,22 @@ TEST(SolidSyslogTlsStream, DestroyFreesSslContext) /* teardown re-Destroys safely */ } +TEST(SolidSyslogTlsStream, DestroyFreesBioMethodWhenCloseNotCalled) +{ + SolidSyslogStream_Open(stream, addr); + SolidSyslogTlsStream_Destroy(); + LONGS_EQUAL(1, OpenSslFake_BioMethFreeCallCount()); + /* teardown re-Destroys safely */ +} + +TEST(SolidSyslogTlsStream, DestroyAfterCloseDoesNotDoubleFreeBioMethod) +{ + SolidSyslogStream_Open(stream, addr); + SolidSyslogStream_Close(stream); + SolidSyslogTlsStream_Destroy(); + LONGS_EQUAL(1, OpenSslFake_BioMethFreeCallCount()); +} + /* ------------------------------------------------------------------------- * Pointer-chain assertions: each OpenSSL call must receive the handle * returned by the preceding call, not some stale or NULL pointer. From 7b2f6b3e0b65e49ec184815b4b7ed6677ed6b2ab Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:36:22 +0100 Subject: [PATCH 11/33] fix: S03.08 SSL freed exactly once across Close/Destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror of the BIO_METHOD idempotence commit for SSL: Destroy now releases the SSL handle if Close wasn't called (e.g. Open failed after SSL_new but before a successful connect). Close nulls the stored pointer so a subsequent Destroy doesn't double-free. Previously Destroy only handled SSL_CTX — any caller that hit an Open failure after SSL_new was leaking an SSL handle until process exit. Not caught under fakes (SSL_free is a stub), real libssl under valgrind would flag it. Driven by DestroyFreesSslWhenCloseNotCalled and DestroyAfterCloseDoesNotDoubleFreeSsl. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 6 ++++++ Tests/SolidSyslogTlsStreamTest.cpp | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index e39a5f70..647e1bf8 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -37,6 +37,11 @@ struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTl void SolidSyslogTlsStream_Destroy(void) { + if (instance.ssl != NULL) + { + SSL_free(instance.ssl); + instance.ssl = NULL; + } if (instance.bioMethod != NULL) { BIO_meth_free(instance.bioMethod); @@ -100,6 +105,7 @@ static void Close(struct SolidSyslogStream* self) struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; SSL_shutdown(stream->ssl); SSL_free(stream->ssl); + stream->ssl = NULL; BIO_meth_free(stream->bioMethod); stream->bioMethod = NULL; SolidSyslogStream_Close(stream->config.transport); diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 1a997aa2..31c089e4 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -302,6 +302,21 @@ TEST(SolidSyslogTlsStream, DestroyAfterCloseDoesNotDoubleFreeBioMethod) LONGS_EQUAL(1, OpenSslFake_BioMethFreeCallCount()); } +TEST(SolidSyslogTlsStream, DestroyFreesSslWhenCloseNotCalled) +{ + SolidSyslogStream_Open(stream, addr); + SolidSyslogTlsStream_Destroy(); + LONGS_EQUAL(1, OpenSslFake_FreeCallCount()); +} + +TEST(SolidSyslogTlsStream, DestroyAfterCloseDoesNotDoubleFreeSsl) +{ + SolidSyslogStream_Open(stream, addr); + SolidSyslogStream_Close(stream); + SolidSyslogTlsStream_Destroy(); + LONGS_EQUAL(1, OpenSslFake_FreeCallCount()); +} + /* ------------------------------------------------------------------------- * Pointer-chain assertions: each OpenSSL call must receive the handle * returned by the preceding call, not some stale or NULL pointer. From d8d98602e9690715da015b86f01769e41b63ef47 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:37:28 +0100 Subject: [PATCH 12/33] fix: S03.08 abort Open when BIO_meth_new returns NULL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under memory pressure BIO_meth_new can return NULL. Continuing with a NULL BIO_METHOD would crash the subsequent BIO_meth_set_* calls in real libssl. CreateTransportBio now returns NULL on failure, and Open checks the result. The partially-initialised SSL_CTX and SSL are released by Destroy — the test covers return-value behaviour; the lifecycle invariants set in the Destroy-idempotence commits already guarantee no leak. Driven by OpenReturnsFalseWhenBioMethNewFails; OpenSslFake gains SetBioMethNewFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 8 ++++++++ Tests/SolidSyslogTlsStreamTest.cpp | 6 ++++++ Tests/Support/OpenSslFake.c | 11 ++++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 647e1bf8..a13eefda 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -72,6 +72,10 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress return false; } BIO* bio = CreateTransportBio(stream); + if (bio == NULL) + { + return false; + } BIO_set_data(bio, stream->config.transport); SSL_set_bio(stream->ssl, bio, bio); if (stream->config.serverName != NULL) @@ -114,6 +118,10 @@ static void Close(struct SolidSyslogStream* self) static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); + if (method == NULL) + { + return NULL; + } BIO_meth_set_create(method, TransportBioCreate); BIO_meth_set_read(method, TransportBioRead); BIO_meth_set_write(method, TransportBioWrite); diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 31c089e4..0739fecf 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -495,6 +495,12 @@ TEST(SolidSyslogTlsStream, MinProtoVersionFailureFreesCtx) LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenBioMethNewFails) +{ + OpenSslFake_SetBioMethNewFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 706f4b42..54228cae 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -43,6 +43,9 @@ static int sslNewCallCount; static SSL_CTX* lastSslNewCtxArg; static bool sslNewFails; +/* BIO_meth_new */ +static bool bioMethNewFails; + /* BIO_meth_free */ static int bioMethFreeCallCount; static BIO_METHOD* lastBioMethFreeArg; @@ -131,6 +134,7 @@ void OpenSslFake_Reset(void) sslNewCallCount = 0; lastSslNewCtxArg = NULL; sslNewFails = false; + bioMethNewFails = false; bioMethFreeCallCount = 0; lastBioMethFreeArg = NULL; lastBioMethSetReadMethodArg = NULL; @@ -511,7 +515,12 @@ BIO_METHOD* BIO_meth_new(int type, const char* name) { (void) type; (void) name; - return (BIO_METHOD*) &fakeBioMethStorage; + return bioMethNewFails ? NULL : (BIO_METHOD*) &fakeBioMethStorage; +} + +void OpenSslFake_SetBioMethNewFails(bool fails) +{ + bioMethNewFails = fails; } void BIO_meth_free(BIO_METHOD* biom) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 2fc4967e..fe5b74c9 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -25,6 +25,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetSslNewFails(bool fails); void OpenSslFake_SetLoadVerifyLocationsFails(bool fails); void OpenSslFake_SetMinProtoVersionFails(bool fails); + void OpenSslFake_SetBioMethNewFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From c637f086611caa706cb5a8ea76d6e24b6c09a628 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:39:07 +0100 Subject: [PATCH 13/33] fix: S03.08 abort Open when BIO_new returns NULL and free BIO_METHOD Final return-value check in Open. BIO_new can fail under memory pressure; when it does, the BIO_METHOD we just allocated is freed inline rather than leaving it for Destroy, so callers that skip Destroy still don't leak. Two tests drive this: - OpenReturnsFalseWhenBioNewFails (return propagates via the CreateTransportBio-returns-NULL path added earlier) - BioNewFailureFreesBioMethodInline (cleanup co-located with the failure so it happens even without Destroy) Completes the "every OpenSSL call in Open is return-value-checked" and "partial-init failures release all resources" acceptance items. OpenSslFake gains SetBioNewFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 9 ++++++++- Tests/SolidSyslogTlsStreamTest.cpp | 14 ++++++++++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index a13eefda..50d25c43 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -127,7 +127,14 @@ static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) BIO_meth_set_write(method, TransportBioWrite); BIO_meth_set_ctrl(method, TransportBioCtrl); stream->bioMethod = method; - return BIO_new(method); + BIO* bio = BIO_new(method); + if (bio == NULL) + { + BIO_meth_free(method); + stream->bioMethod = NULL; + return NULL; + } + return bio; } /* Called when BIO_new instantiates a BIO from our method. Marking init=1 tells diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 0739fecf..96f167c2 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -501,6 +501,20 @@ TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenBioMethNewFails) CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenBioNewFails) +{ + OpenSslFake_SetBioNewFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, BioNewFailureFreesBioMethodInline) +{ + OpenSslFake_SetBioNewFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_BioMethFreeCallCount()); + /* teardown re-Destroys safely — bioMethod already cleared */ +} + TEST(SolidSyslogTlsStream, SendReturnsTrueOnHappyPath) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 54228cae..cb747408 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -62,6 +62,7 @@ static int (*lastBioWriteCallback)(BIO*, const char*, int); /* BIO_new */ static int bioNewCallCount; static const BIO_METHOD* lastBioNewMethodArg; +static bool bioNewFails; /* BIO_set_data / BIO_get_data */ static BIO* lastSetDataBioArg; @@ -146,6 +147,7 @@ void OpenSslFake_Reset(void) lastBioWriteCallback = NULL; bioNewCallCount = 0; lastBioNewMethodArg = NULL; + bioNewFails = false; lastSetDataBioArg = NULL; lastSetDataArg = NULL; lastGetDataBioArg = NULL; @@ -577,7 +579,12 @@ BIO* BIO_new(const BIO_METHOD* type) { bioNewCallCount++; lastBioNewMethodArg = type; - return (BIO*) &fakeBioStorage; + return bioNewFails ? NULL : (BIO*) &fakeBioStorage; +} + +void OpenSslFake_SetBioNewFails(bool fails) +{ + bioNewFails = fails; } void BIO_set_data(BIO* a, void* ptr) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index fe5b74c9..229ba07e 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -26,6 +26,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetLoadVerifyLocationsFails(bool fails); void OpenSslFake_SetMinProtoVersionFails(bool fails); void OpenSslFake_SetBioMethNewFails(bool fails); + void OpenSslFake_SetBioNewFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From 9e34a2d013474f42393b7d268b6e1cbebd77c6a5 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:47:01 +0100 Subject: [PATCH 14/33] feat: S03.08 forward SolidSyslogTlsStreamConfig.cipherList to libssl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional cipherList field to SolidSyslogTlsStreamConfig. Non- NULL values are forwarded to SSL_CTX_set_cipher_list; NULL leaves OpenSSL's default cipher selection in place. The library stays policy-free — no baked-in SL4 cipher list. Callers that need SL4-compliant ciphers set the list at their level, where the security profile lives. docs/iec62443.md will document a recommended example. Driven by OpenPassesCipherListToSslCtx (list forwarded verbatim) and OpenSkipsCipherListSetupWhenNotConfigured (NULL means no call). Failure-handling and cleanup come in the next commit. OpenSslFake gains SSL_CTX_set_cipher_list stub + accessors. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Interface/SolidSyslogTlsStream.h | 1 + .../OpenSsl/Source/SolidSyslogTlsStream.c | 10 ++++-- Tests/SolidSyslogTlsStreamTest.cpp | 15 +++++++++ Tests/Support/OpenSslFake.c | 31 +++++++++++++++++++ Tests/Support/OpenSslFake.h | 5 +++ 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h b/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h index f6855aca..dc21b951 100644 --- a/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h +++ b/Platform/OpenSsl/Interface/SolidSyslogTlsStream.h @@ -10,6 +10,7 @@ EXTERN_C_BEGIN struct SolidSyslogStream* transport; /* underlying byte stream — caller owns */ const char* caBundlePath; /* PEM file of trust anchors */ const char* serverName; /* SNI + cert hostname check; NULL to skip */ + const char* cipherList; /* TLS 1.2 cipher list; NULL = OpenSSL default */ }; struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTlsStreamConfig* config); diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 50d25c43..4a01549b 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -16,7 +16,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidS static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); -static SSL_CTX* CreateSslContext(const char* caBundlePath); +static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherList); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); static int TransportBioRead(BIO* bio, char* buffer, int size); @@ -61,7 +61,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress { return false; } - stream->ctx = CreateSslContext(stream->config.caBundlePath); + stream->ctx = CreateSslContext(stream->config.caBundlePath, stream->config.cipherList); if (stream->ctx == NULL) { return false; @@ -178,7 +178,7 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) } } -static SSL_CTX* CreateSslContext(const char* caBundlePath) +static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherList) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); if (ctx == NULL) @@ -196,5 +196,9 @@ static SSL_CTX* CreateSslContext(const char* caBundlePath) SSL_CTX_free(ctx); return NULL; } + if (cipherList != NULL) + { + SSL_CTX_set_cipher_list(ctx, cipherList); + } return ctx; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index 96f167c2..a90d8765 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -79,6 +79,21 @@ TEST(SolidSyslogTlsStream, OpenSetsTls12Floor) LONGS_EQUAL(TLS1_2_VERSION, OpenSslFake_LastMinProtoVersion()); } +TEST(SolidSyslogTlsStream, OpenPassesCipherListToSslCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.cipherList = "ECDHE+AESGCM"; + stream = SolidSyslogTlsStream_Create(&config); + SolidSyslogStream_Open(stream, addr); + STRCMP_EQUAL("ECDHE+AESGCM", OpenSslFake_LastCipherList()); +} + +TEST(SolidSyslogTlsStream, OpenSkipsCipherListSetupWhenNotConfigured) +{ + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(0, OpenSslFake_SetCipherListCallCount()); +} + TEST(SolidSyslogTlsStream, OpenCreatesSslSession) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index cb747408..3d32f885 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -38,6 +38,11 @@ static SSL_CTX* lastSslCtxCtrlCtxArg; static long lastMinProtoVersion; static bool minProtoVersionFails; +/* SSL_CTX_set_cipher_list */ +static int setCipherListCallCount; +static SSL_CTX* lastSetCipherListCtxArg; +static const char* lastCipherList; + /* SSL_new */ static int sslNewCallCount; static SSL_CTX* lastSslNewCtxArg; @@ -132,6 +137,9 @@ void OpenSslFake_Reset(void) lastSslCtxCtrlCtxArg = NULL; lastMinProtoVersion = 0; minProtoVersionFails = false; + setCipherListCallCount = 0; + lastSetCipherListCtxArg = NULL; + lastCipherList = NULL; sslNewCallCount = 0; lastSslNewCtxArg = NULL; sslNewFails = false; @@ -501,6 +509,29 @@ void OpenSslFake_SetMinProtoVersionFails(bool fails) minProtoVersionFails = fails; } +int SSL_CTX_set_cipher_list(SSL_CTX* ctx, const char* str) +{ + setCipherListCallCount++; + lastSetCipherListCtxArg = ctx; + lastCipherList = str; + return 1; +} + +int OpenSslFake_SetCipherListCallCount(void) +{ + return setCipherListCallCount; +} + +SSL_CTX* OpenSslFake_LastSetCipherListCtxArg(void) +{ + return lastSetCipherListCtxArg; +} + +const char* OpenSslFake_LastCipherList(void) +{ + return lastCipherList; +} + SSL* SSL_new(SSL_CTX* ctx) { sslNewCallCount++; diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 229ba07e..2cecfff5 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -45,6 +45,11 @@ EXTERN_C_BEGIN struct ssl_ctx_st* OpenSslFake_LastSslCtxCtrlCtxArg(void); long OpenSslFake_LastMinProtoVersion(void); + /* SSL_CTX_set_cipher_list */ + int OpenSslFake_SetCipherListCallCount(void); + struct ssl_ctx_st* OpenSslFake_LastSetCipherListCtxArg(void); + const char* OpenSslFake_LastCipherList(void); + /* SSL_new */ int OpenSslFake_SslNewCallCount(void); struct ssl_ctx_st* OpenSslFake_LastSslNewCtxArg(void); From 7ddb34df131990271cae78e5454fb9cb1801b5b6 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:47:59 +0100 Subject: [PATCH 15/33] fix: S03.08 abort Open when cipher list is rejected by libssl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSL_CTX_set_cipher_list returns 0 when every cipher in the list is unrecognised or unavailable in the linked libssl build. Continuing with a CTX that has no cipher suites to offer produces a handshake failure with no meaningful client-side error message — so fail fast at config time and surface the misconfiguration. CreateSslContext frees the CTX it allocated before returning NULL, matching the pattern of the other return-value checks. Driven by OpenReturnsFalseWhenCipherListRejected and CipherListFailureFreesCtx; OpenSslFake gains SetCipherListFails. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 5 +++-- Tests/SolidSyslogTlsStreamTest.cpp | 19 +++++++++++++++++++ Tests/Support/OpenSslFake.c | 9 ++++++++- Tests/Support/OpenSslFake.h | 1 + 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 4a01549b..f5cd03b7 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -196,9 +196,10 @@ static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherLis SSL_CTX_free(ctx); return NULL; } - if (cipherList != NULL) + if (cipherList != NULL && SSL_CTX_set_cipher_list(ctx, cipherList) != 1) { - SSL_CTX_set_cipher_list(ctx, cipherList); + SSL_CTX_free(ctx); + return NULL; } return ctx; } diff --git a/Tests/SolidSyslogTlsStreamTest.cpp b/Tests/SolidSyslogTlsStreamTest.cpp index a90d8765..7239a86e 100644 --- a/Tests/SolidSyslogTlsStreamTest.cpp +++ b/Tests/SolidSyslogTlsStreamTest.cpp @@ -94,6 +94,25 @@ TEST(SolidSyslogTlsStream, OpenSkipsCipherListSetupWhenNotConfigured) LONGS_EQUAL(0, OpenSslFake_SetCipherListCallCount()); } +TEST(SolidSyslogTlsStream, OpenReturnsFalseWhenCipherListRejected) +{ + SolidSyslogTlsStream_Destroy(); + config.cipherList = "not-a-real-cipher"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetCipherListFails(true); + CHECK_FALSE(SolidSyslogStream_Open(stream, addr)); +} + +TEST(SolidSyslogTlsStream, CipherListFailureFreesCtx) +{ + SolidSyslogTlsStream_Destroy(); + config.cipherList = "not-a-real-cipher"; + stream = SolidSyslogTlsStream_Create(&config); + OpenSslFake_SetCipherListFails(true); + SolidSyslogStream_Open(stream, addr); + LONGS_EQUAL(1, OpenSslFake_CtxFreeCallCount()); +} + TEST(SolidSyslogTlsStream, OpenCreatesSslSession) { SolidSyslogStream_Open(stream, addr); diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 3d32f885..e5039452 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -42,6 +42,7 @@ static bool minProtoVersionFails; static int setCipherListCallCount; static SSL_CTX* lastSetCipherListCtxArg; static const char* lastCipherList; +static bool setCipherListFails; /* SSL_new */ static int sslNewCallCount; @@ -140,6 +141,7 @@ void OpenSslFake_Reset(void) setCipherListCallCount = 0; lastSetCipherListCtxArg = NULL; lastCipherList = NULL; + setCipherListFails = false; sslNewCallCount = 0; lastSslNewCtxArg = NULL; sslNewFails = false; @@ -514,7 +516,12 @@ int SSL_CTX_set_cipher_list(SSL_CTX* ctx, const char* str) setCipherListCallCount++; lastSetCipherListCtxArg = ctx; lastCipherList = str; - return 1; + return setCipherListFails ? 0 : 1; +} + +void OpenSslFake_SetCipherListFails(bool fails) +{ + setCipherListFails = fails; } int OpenSslFake_SetCipherListCallCount(void) diff --git a/Tests/Support/OpenSslFake.h b/Tests/Support/OpenSslFake.h index 2cecfff5..ccf1a9b5 100644 --- a/Tests/Support/OpenSslFake.h +++ b/Tests/Support/OpenSslFake.h @@ -27,6 +27,7 @@ EXTERN_C_BEGIN void OpenSslFake_SetMinProtoVersionFails(bool fails); void OpenSslFake_SetBioMethNewFails(bool fails); void OpenSslFake_SetBioNewFails(bool fails); + void OpenSslFake_SetCipherListFails(bool fails); /* SSL_CTX_new */ int OpenSslFake_CtxNewCallCount(void); From 063bd0c2a301be1142e6a18ef61173ccc987f43b Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:49:30 +0100 Subject: [PATCH 16/33] test: S03.08 integration test for cipher list rejection Pairs with the fake-based OpenReturnsFalseWhenCipherListRejected: real libssl rejects a cipher name it doesn't recognise, and the library surfaces that as Open failure. Covers the "unsupported cipher" item in the S03.08 acceptance list. Exercises the real SSL_CTX_set_cipher_list validation path, which the fake can only approximate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../SolidSyslogTlsStreamIntegrationTest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp index 5f858d0b..392f9ca7 100644 --- a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -118,3 +118,14 @@ TEST(TlsStreamIntegration, HandshakeRejectedWhenClientDoesNotTrustServerCert) TlsTestCert_Destroy(&untrusted); } + +TEST(TlsStreamIntegration, HandshakeRejectedWhenCipherListIsUnsupported) +{ + struct TlsTestCertConfig certConfig = {}; + certConfig.commonName = "localhost"; + certConfig.subjectAltDnsNames = LOCALHOST_SANS; + tlsConfig.cipherList = "NOT-A-REAL-CIPHER"; + buildScenario(certConfig); + + CHECK_FALSE(SolidSyslogStream_Open(tlsStream, addr)); +} From 78a2f5266ab15abdb433ea5ca82ed5416d355106 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:51:35 +0100 Subject: [PATCH 17/33] docs: S03.08 document SL4 TLS hardening in iec62443.md Captures the SL4 controls landed by S03.08 as part of the SL4 "what's in place" narrative, without promoting the TLS sender from Planned to Available (that's S03.11's scope): - Hostname verification (SNI + cert check) with return-checked setup calls. - Pinned SSL_VERIFY_PEER on the CTX. - Return-checked TLS 1.2 floor. - cipherList config field for caller-supplied SL4 cipher pinning, with a starting-point example (ECDHE+AESGCM:ECDHE+CHACHA20). - Idempotent Close/Destroy lifecycle. Traceability matrix cross-references the S03.08 story for CR 3.9. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/iec62443.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/iec62443.md b/docs/iec62443.md index e60955dc..aa42122b 100644 --- a/docs/iec62443.md +++ b/docs/iec62443.md @@ -152,6 +152,35 @@ for SL4 extension but some components are not yet implemented. - Full RFC 5424 character compliance — PRINTUSASCII validation on header fields, UTF-8 safe truncation on message body +**TLS hardening (SL4 substrate in place).** `SolidSyslogTlsStream` — the +OpenSSL-backed client-side TLS substrate — ships with the SL4 controls that +depend on the client: + +- **Hostname verification.** `SolidSyslogTlsStreamConfig.serverName` is + forwarded to both SNI (`SSL_set_tlsext_host_name`) and the cert check + (`SSL_set1_host`). Both return values are checked; handshake is aborted on + any setup failure, preventing a silent "handshake succeeded without checking + the name" bypass. +- **Trust chain.** `caBundlePath` loads trust anchors via + `SSL_CTX_load_verify_locations`; `SSL_VERIFY_PEER` is pinned on the CTX. +- **TLS 1.2 floor.** `SSL_CTX_set_min_proto_version(TLS1_2_VERSION)` is + return-checked; Open fails if libssl refuses the floor. +- **Cipher pinning.** `SolidSyslogTlsStreamConfig.cipherList` is forwarded to + `SSL_CTX_set_cipher_list`. The library ships no baked-in list — the + SL4-appropriate cipher policy lives with the caller, which understands its + security profile. A reasonable starting point is + `"ECDHE+AESGCM:ECDHE+CHACHA20"` (TLS 1.2 AEAD with forward secrecy); tune to + match the libssl build on the target platform. +- **Resource lifecycle.** `Close`/`Destroy` release `SSL_CTX`, `SSL`, and + `BIO_METHOD` idempotently — no leaks on partial Open failure, no double + frees if both are called. + +Remaining E3 work for a full SL4 TLS posture: mutual TLS +([S03.09](https://github.com/DavidCozens/solid-syslog/issues/173)), cert +rotation ([S03.10](https://github.com/DavidCozens/solid-syslog/issues/174)), +and the formal doc promotion from Planned to Available +([S03.11](https://github.com/DavidCozens/solid-syslog/issues/175)). + ## Architecture for Security SolidSyslog's architecture directly supports IEC 62443-4-1 (secure development @@ -221,6 +250,6 @@ the collector can alert on missing sequence numbers without polling the device. | CR 2.10 Response to audit failures | `SolidSyslogStoreFullCallback`, halt policy | Callback on storage full, optional halt | | CR 2.11 Timestamps | `SolidSyslogClockFunction`, `SolidSyslogTimeQualitySd` | Injected clock with quality metadata | | CR 2.12 Non-repudiation | `SolidSyslogMetaSd` (sequenceId), `SolidSyslogCrc16Policy` | Sequence gaps detectable by SIEM | -| CR 3.9 Audit information protection | `SolidSyslogCrc16Policy` (SL3), TLS + encryption (SL4) | SL4: [E3](https://github.com/DavidCozens/solid-syslog/issues/5), [E17](https://github.com/DavidCozens/solid-syslog/issues/105) | +| CR 3.9 Audit information protection | `SolidSyslogCrc16Policy` (SL3), TLS + encryption (SL4) | SL4 TLS substrate hardened by [S03.08](https://github.com/DavidCozens/solid-syslog/issues/172) (hostname verify, TLS 1.2 floor, cipher pinning); remaining TLS work in [E3](https://github.com/DavidCozens/solid-syslog/issues/5); integrity/encryption at rest in [E17](https://github.com/DavidCozens/solid-syslog/issues/105) | | SR 6.1 Audit log accessibility | `SolidSyslog_Service`, sender vtable | Non-blocking Log, background Service | | SR 6.2 Continuous monitoring | TCP/TLS transport, store-and-forward | Assured delivery via replay on reconnect | From af27c49519ca213fde3368b1d956573c001640fd Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 05:52:59 +0100 Subject: [PATCH 18/33] docs: update DEVLOG for S03.08 TLS hardening session Covers the phased planning pass, what worked (test-support trio, pump-on-read, upfront surprises list), what didn't (BDD-first reversal, Phase 2/4 coupling, commit count), the key decisions (cipher policy at caller, per-Open BIO lifecycle, coverage stays unit-only), and deferred items (per-BIO fake refactor, TLS 1.3 cipher suites, max_proto_version control, S03.11 doc promotion). Co-Authored-By: Claude Opus 4.7 (1M context) --- DEVLOG.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/DEVLOG.md b/DEVLOG.md index faf70774..23c0392b 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -841,3 +841,115 @@ S3.9 / S3.10 / S3.11 as placeholder issues under E3. because it rides on the already-reliable auto-load. Worth recording: separating "conventions that must be read" from "conventions the tool auto-loads" is a real gap, and signposting from CLAUDE.md bridges it. + +## 2026-04-21/22 — S03.08 TLS hardening: cert validation, cipher pinning, lifecycle + +### What went well + +- **Planning turn before coding paid off.** Before any C, I laid out a + phase plan and six API-shape surprises — integration harness must + live in its own binary (fake interposes libssl), cert generation + programmatic not fixtures, cipher policy at caller not library, BIO + lifecycle per-Open, the `OpenSslFake` per-BIO aliasing issue. The + user corrected one call (BDD scenarios for negative cases → + integration harness instead, much faster feedback), parked three + (cipher shape, BIO lifecycle, `SSL_VERIFY_FAIL_IF_NO_PEER_CERT`), + and greenlit the rest. Every surprise came up once, got a decision, + then didn't recur. + +- **Composable test-support trio.** `TlsTestCert` + `TlsTestServer` + + `BioPairStream` under `Tests/OpenSslIntegration/` each do one thing; + new scenarios are a single test body with a cert config and an + assertion. The fourth scenario (unsupported cipher) was a + seven-line addition. This is the scaffolding paying for itself + instantly, not in six months. + +- **Pump-on-read pattern.** `BioPairStream::Read` drives the server + side cooperatively when the BIO pair is empty, so + `SolidSyslogTlsStream`'s synchronous `SSL_connect` works unchanged + over non-blocking in-memory BIOs — no threads in the harness, no + production changes to accommodate testing. + +### What didn't go well / process refinements + +- **BDD-first plan got reversed.** My Phase 1 was "BDD pending + scenarios for SL4 failure modes." User pushed back: integration + harness covers it faster with no PO-visibility loss. Should have + surfaced this trade-off *myself* in the opening surprises list + rather than defaulting to BDD. Memory entry + `feedback_integration_over_bdd.md` captures the pattern: for + crypto/TLS failure modes, integration harness beats BDD. + +- **Phase 2 / Phase 4 coupling I didn't spot up front.** Plan + separated "return-value checks" (Phase 2) from "resource lifecycle" + (Phase 4). In practice, every new early-return path made a + pre-existing leak worse until `Destroy` covered partial state. + Ended up interleaving — not a real problem but the phase plan was + cleaner than reality. + +- **Commit count.** 17 commits on the branch is a lot of tight red/green + pairs. Each is a clear atomic behaviour change and reads well in + `git log --oneline`, but the PR-review surface is larger than a + single "TLS hardening" story arguably needed. Next time, consider + batching several return-value checks into one commit when they + share a test shape. + +- **Characterisation vs red/green mismatch.** Three of the integration + tests went green on first write — OpenSSL defaults already rejected + expired / wrong-hostname / wrong-CA. Under strict "only write + production code driven by a failing test" this feels off, but the + tests still earn their keep as regression protection. I flagged + this explicitly rather than pretending it was red/green, and the + user accepted the framing. Calling out "this is characterisation, + not TDD" is probably the right move whenever it happens. + +- **One stray production branch.** The `if (ctx == NULL) return NULL;` + inside `CreateSslContext` wasn't strictly driven by a failing test — + it was the natural implementation alongside the `OpenReturnsFalseWhenCtxNewFails` + fix. User reminded me mid-session to "only write a new branch of code + when you have a failing test." Tightened up for the rest of the session. + +### Decisions captured this session + +- **Integration test executable is its own CI job** (option C of A/B/C) + — runs in parallel with `build-and-test`, self-contained configure + + build of only `OpenSslIntegrationTests`. Clang and sanitize + presets don't run it; unit tests remain the coverage engine. +- **Cipher list is caller policy.** `SolidSyslogTlsStreamConfig.cipherList` + is forwarded to `SSL_CTX_set_cipher_list` if non-NULL; library ships + no SL4 default. `docs/iec62443.md` names + `ECDHE+AESGCM:ECDHE+CHACHA20` as a reasonable starting point, not a + baked-in list. +- **BIO_METHOD lifecycle is per-Open.** Symmetric with SSL/SSL_CTX — + each Open allocates a fresh set, each Close releases. Destroy + covers partial-init state idempotently. +- **Coverage stays unit-test-only.** Integration tests are supplementary + evidence of real-libssl acceptance, not the coverage engine. Memory + entry `project_coverage_integration.md` records the trigger for + revisiting (multi-platform, or a real-libssl-only code path). + +### Deferred + +- **`OpenSslFake` per-BIO data refactor.** Listed as S03.08 acceptance + but no production code path or test requires distinct BIO data yet + (the library creates exactly one BIO per Open). Forward-looking + for mTLS (S03.09) and future cases. Suggest a short follow-up + story rather than doing it here without a test that fails without it. +- **TLS 1.3 cipher suite control.** `SSL_CTX_set_cipher_list` governs + TLS 1.2 only; TLS 1.3 uses `SSL_CTX_set_ciphersuites`. Out of the + ticket's acceptance. Follow-up. +- **`max_proto_version` control.** Library pins min but not max. No + current need; might emerge with an SL4 policy that mandates TLS 1.3. +- **Formal Planned → Available promotion** for TLS in + `docs/iec62443.md` — S03.11 scope. This session added the SL4 + substrate narrative without touching the headline status. + +### Open questions for the blog source + +- **When is characterisation a legitimate TDD outcome?** Three tests + went green on first write because OpenSSL already did the right + thing. Keeping them is evidence and regression protection; rejecting + them would be dogmatic. The useful lens: if the test protects against + a realistic future regression, it's load-bearing — TDD discipline is + about "production code driven by tests", not "every test must have + been red at some point." From a476ec10810d505ae7ad8596f2f206a1ed595027 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:09:47 +0100 Subject: [PATCH 19/33] chore: S03.08 satisfy clang-format and clang-tidy on new TLS code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI checks flagged: - Format: the new `Tests/OpenSslIntegration/` sources and the `Tests/Support/OpenSslFake*` additions didn't match the project's clang-format aggressive alignment rules. Auto-formatted via `clang-format -i`. - Tidy: * CreateSslContext had two adjacent `const char*` params (bugprone-easily-swappable-parameters). Refactored to take the `SolidSyslogTlsStreamConfig*` directly — same logic, one pointer, future-proof for further config fields. * BioPairStream::Read had two `done = true` arms with identical bodies (bugprone-branch-clone). Combined under one `||` condition. * TlsTestCert had `#define` macros for integral constants (modernize-macro-to-enum). Promoted to an anonymous enum. * SetValidity had two adjacent `time_t` params. Refactored to take the TlsTestCertConfig* directly, same pattern as CreateSslContext. * TlsTestCert_WritePemToFile didn't check fopen. Added a NULL-guard early return. No behaviour change; 747 unit tests + 5 integration tests remain green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 10 +++---- Tests/OpenSslIntegration/BioPairStream.c | 6 +---- Tests/OpenSslIntegration/BioPairStream.h | 8 +++--- .../SolidSyslogTlsStreamIntegrationTest.cpp | 1 + Tests/OpenSslIntegration/TlsTestCert.c | 27 ++++++++++++------- Tests/OpenSslIntegration/TlsTestCert.h | 4 +-- Tests/OpenSslIntegration/TlsTestServer.h | 4 +-- Tests/Support/OpenSslFake.c | 2 +- 8 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index f5cd03b7..dbfff044 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -16,7 +16,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidS static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); -static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherList); +static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); static int TransportBioRead(BIO* bio, char* buffer, int size); @@ -61,7 +61,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress { return false; } - stream->ctx = CreateSslContext(stream->config.caBundlePath, stream->config.cipherList); + stream->ctx = CreateSslContext(&stream->config); if (stream->ctx == NULL) { return false; @@ -178,14 +178,14 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) } } -static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherList) +static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); if (ctx == NULL) { return NULL; } - if (SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) != 1) + if (SSL_CTX_load_verify_locations(ctx, config->caBundlePath, NULL) != 1) { SSL_CTX_free(ctx); return NULL; @@ -196,7 +196,7 @@ static SSL_CTX* CreateSslContext(const char* caBundlePath, const char* cipherLis SSL_CTX_free(ctx); return NULL; } - if (cipherList != NULL && SSL_CTX_set_cipher_list(ctx, cipherList) != 1) + if (config->cipherList != NULL && SSL_CTX_set_cipher_list(ctx, config->cipherList) != 1) { SSL_CTX_free(ctx); return NULL; diff --git a/Tests/OpenSslIntegration/BioPairStream.c b/Tests/OpenSslIntegration/BioPairStream.c index a3866369..79e3bae7 100644 --- a/Tests/OpenSslIntegration/BioPairStream.c +++ b/Tests/OpenSslIntegration/BioPairStream.c @@ -72,11 +72,7 @@ static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_ result = (SolidSyslogSsize) bytesRead; done = true; } - else if (!BIO_should_retry(stream->bio)) - { - done = true; - } - else if (stream->pump == NULL) + else if (!BIO_should_retry(stream->bio) || stream->pump == NULL) { done = true; } diff --git a/Tests/OpenSslIntegration/BioPairStream.h b/Tests/OpenSslIntegration/BioPairStream.h index d99a413d..a2783e25 100644 --- a/Tests/OpenSslIntegration/BioPairStream.h +++ b/Tests/OpenSslIntegration/BioPairStream.h @@ -9,11 +9,9 @@ EXTERN_C_BEGIN typedef void (*BioPairStreamPumpFunction)(void* context); - struct SolidSyslogStream* BioPairStream_Create(BIO* bio); - void BioPairStream_Destroy(struct SolidSyslogStream* self); - void BioPairStream_SetPump(struct SolidSyslogStream* self, - BioPairStreamPumpFunction pump, - void* context); + struct SolidSyslogStream* BioPairStream_Create(BIO * bio); + void BioPairStream_Destroy(struct SolidSyslogStream * self); + void BioPairStream_SetPump(struct SolidSyslogStream * self, BioPairStreamPumpFunction pump, void* context); EXTERN_C_END diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp index 392f9ca7..a394e6ba 100644 --- a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -60,6 +60,7 @@ TEST_GROUP(TlsStreamIntegration) tlsStream = SolidSyslogTlsStream_Create(&tlsConfig); } }; + // clang-format on static const char* const LOCALHOST_SANS[] = {"localhost", nullptr}; diff --git a/Tests/OpenSslIntegration/TlsTestCert.c b/Tests/OpenSslIntegration/TlsTestCert.c index e041cbc4..e4f7466c 100644 --- a/Tests/OpenSslIntegration/TlsTestCert.c +++ b/Tests/OpenSslIntegration/TlsTestCert.c @@ -10,22 +10,25 @@ #include #include -#define TLS_TEST_CERT_RSA_BITS 2048 -#define TLS_TEST_CERT_DEFAULT_VALIDITY_SECONDS 3600 +enum +{ + RSA_BITS = 2048, + DEFAULT_VALIDITY_SECONDS = 3600, +}; -static void SetValidity(X509* cert, time_t notBefore, time_t notAfter); +static void SetValidity(X509* cert, const struct TlsTestCertConfig* config); static void SetSubject(X509* cert, const char* commonName); -static void AddSubjectAltNames(X509* cert, const char* const* dnsNames); +static void AddSubjectAltNames(X509* cert, const char* const * dnsNames); void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out) { - EVP_PKEY* key = EVP_RSA_gen(TLS_TEST_CERT_RSA_BITS); + EVP_PKEY* key = EVP_RSA_gen(RSA_BITS); X509* cert = X509_new(); X509_set_version(cert, 2); /* X.509 v3 */ ASN1_INTEGER_set(X509_get_serialNumber(cert), 1); - SetValidity(cert, config->notBefore, config->notAfter); + SetValidity(cert, config); SetSubject(cert, config->commonName); AddSubjectAltNames(cert, config->subjectAltDnsNames); @@ -56,15 +59,19 @@ void TlsTestCert_Destroy(struct TlsTestCert* cert) void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path) { FILE* file = fopen(path, "w"); + if (file == NULL) + { + return; + } PEM_write_X509(file, cert->cert); fclose(file); } -static void SetValidity(X509* cert, time_t notBefore, time_t notAfter) +static void SetValidity(X509* cert, const struct TlsTestCertConfig* config) { time_t now = time(NULL); - time_t start = (notBefore != 0) ? notBefore : now; - time_t end = (notAfter != 0) ? notAfter : now + TLS_TEST_CERT_DEFAULT_VALIDITY_SECONDS; + time_t start = (config->notBefore != 0) ? config->notBefore : now; + time_t end = (config->notAfter != 0) ? config->notAfter : now + DEFAULT_VALIDITY_SECONDS; X509_time_adj_ex(X509_getm_notBefore(cert), 0, 0, &start); X509_time_adj_ex(X509_getm_notAfter(cert), 0, 0, &end); } @@ -75,7 +82,7 @@ static void SetSubject(X509* cert, const char* commonName) X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (const unsigned char*) commonName, -1, -1, 0); } -static void AddSubjectAltNames(X509* cert, const char* const* dnsNames) +static void AddSubjectAltNames(X509* cert, const char* const * dnsNames) { if (dnsNames == NULL) { diff --git a/Tests/OpenSslIntegration/TlsTestCert.h b/Tests/OpenSslIntegration/TlsTestCert.h index 71fef052..f34e1f89 100644 --- a/Tests/OpenSslIntegration/TlsTestCert.h +++ b/Tests/OpenSslIntegration/TlsTestCert.h @@ -17,14 +17,14 @@ EXTERN_C_BEGIN struct TlsTestCertConfig { const char* commonName; - const char* const* subjectAltDnsNames; /* NULL-terminated array; NULL if no SAN */ + const char* const * subjectAltDnsNames; /* NULL-terminated array; NULL if no SAN */ time_t notBefore; /* 0 = now */ time_t notAfter; /* 0 = now + 3600 */ const struct TlsTestCert* issuer; /* NULL = self-signed */ }; void TlsTestCert_Create(const struct TlsTestCertConfig* config, struct TlsTestCert* out); - void TlsTestCert_Destroy(struct TlsTestCert* cert); + void TlsTestCert_Destroy(struct TlsTestCert * cert); void TlsTestCert_WritePemToFile(const struct TlsTestCert* cert, const char* path); EXTERN_C_END diff --git a/Tests/OpenSslIntegration/TlsTestServer.h b/Tests/OpenSslIntegration/TlsTestServer.h index 627004ef..32d538ca 100644 --- a/Tests/OpenSslIntegration/TlsTestServer.h +++ b/Tests/OpenSslIntegration/TlsTestServer.h @@ -16,11 +16,11 @@ EXTERN_C_BEGIN }; struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* config); - void TlsTestServer_Destroy(struct TlsTestServer* self); + void TlsTestServer_Destroy(struct TlsTestServer * self); /* Returns the client-facing end of the internal BIO pair. Pass to * BioPairStream_Create and use as the transport for SolidSyslogTlsStream. */ - BIO* TlsTestServer_ClientSideBio(struct TlsTestServer* self); + BIO* TlsTestServer_ClientSideBio(struct TlsTestServer * self); /* Advances the server's state machine one step. Used as a pump callback * so client-side reads can make cooperative progress. Context is the diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index e5039452..1c6a88e9 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -50,7 +50,7 @@ static SSL_CTX* lastSslNewCtxArg; static bool sslNewFails; /* BIO_meth_new */ -static bool bioMethNewFails; +static bool bioMethNewFails; /* BIO_meth_free */ static int bioMethFreeCallCount; From db553c98c8eafb834f208f5254a7a30d0b44266a Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:24:43 +0100 Subject: [PATCH 20/33] chore: S03.08 address CodeRabbit review on PR #183 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accepted: - Integration test now fails loudly on mkstemp failure instead of closing -1 and writing to the unexpanded template path. - CMakeLists.txt enforces OpenSSL >= 3.0 when SOLIDSYSLOG_OPENSSL is ON — the integration harness uses EVP_RSA_gen which is 3.0-only. Configure fails with a clear message on older OpenSSL rather than failing to link with a confusing missing-symbol error. - quality-summary job now depends on openssl-integration so the new JUnit artifact is guaranteed downloaded before the summary runs. - TlsTestServer nulls serverBio after SSL_set_bio to document the ownership transfer (the BIO pair's server side is now owned by SSL, freed via SSL_free). Deferred to S03.14 (#182): - TLS 1.3 ciphersuite control (SSL_CTX_set_ciphersuites) in production and in OpenSslFake. Library stays policy-free on cipher choice per the S03.08 design decision; TLS 1.3 plumbing plus a BDD proof is a coherent separate story. Declined: - Full return-value checks on every OpenSSL and libc call inside TlsTestServer_Create. This is test-harness code running in a controlled CI container where allocations are deterministic; defensive branches create untested paths with no realistic failure mode to cover. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 2 +- CMakeLists.txt | 9 +++++++-- .../SolidSyslogTlsStreamIntegrationTest.cpp | 5 +++++ Tests/OpenSslIntegration/TlsTestServer.c | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 652cc4e2..14aacb26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -549,7 +549,7 @@ jobs: quality-summary: if: always() && github.event_name == 'pull_request' - needs: [build-and-test, clang-build-and-test, sanitize, coverage, tidy, cppcheck, format, bdd, windows-build-and-test, bdd-windows] + needs: [build-and-test, clang-build-and-test, sanitize, coverage, tidy, cppcheck, format, bdd, windows-build-and-test, bdd-windows, openssl-integration] runs-on: ubuntu-latest permissions: contents: read diff --git a/CMakeLists.txt b/CMakeLists.txt index 6020f6b8..74931f1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,8 +88,13 @@ endif() # OpenSSL. find_package(OpenSSL QUIET) option(SOLIDSYSLOG_OPENSSL "Include OpenSSL-based TLS support" ${OpenSSL_FOUND}) -if(SOLIDSYSLOG_OPENSSL AND NOT OpenSSL_FOUND) - message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL=ON but OpenSSL was not found; install OpenSSL or pass -DSOLIDSYSLOG_OPENSSL=OFF") +if(SOLIDSYSLOG_OPENSSL) + if(NOT OpenSSL_FOUND) + message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL=ON but OpenSSL was not found; install OpenSSL or pass -DSOLIDSYSLOG_OPENSSL=OFF") + endif() + if(OPENSSL_VERSION VERSION_LESS 3.0) + message(FATAL_ERROR "SOLIDSYSLOG_OPENSSL requires OpenSSL 3.0 or later; found ${OPENSSL_VERSION}. The integration harness uses EVP_RSA_gen (3.0+).") + endif() endif() add_subdirectory(Core/Source) diff --git a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp index a394e6ba..37625609 100644 --- a/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp +++ b/Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp @@ -44,6 +44,11 @@ TEST_GROUP(TlsStreamIntegration) TlsTestCert_Create(&certConfig, &cert); std::strcpy(caPath, "/tmp/solidsyslog_tls_ca_XXXXXX"); int fd = mkstemp(caPath); + CHECK_TRUE(fd >= 0); + if (fd < 0) + { + return; + } close(fd); TlsTestCert_WritePemToFile(&cert, caPath); diff --git a/Tests/OpenSslIntegration/TlsTestServer.c b/Tests/OpenSslIntegration/TlsTestServer.c index 65e43e64..059ef81f 100644 --- a/Tests/OpenSslIntegration/TlsTestServer.c +++ b/Tests/OpenSslIntegration/TlsTestServer.c @@ -29,6 +29,7 @@ struct TlsTestServer* TlsTestServer_Create(const struct TlsTestServerConfig* con self->ssl = SSL_new(self->ctx); BIO_new_bio_pair(&self->serverBio, 0, &self->clientBio, 0); SSL_set_bio(self->ssl, self->serverBio, self->serverBio); + self->serverBio = NULL; /* ownership transferred to SSL via SSL_set_bio */ SSL_set_accept_state(self->ssl); return self; From a707a6b7790228420b6a6c7856bfe47b0d3df4b5 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:42:41 +0100 Subject: [PATCH 21/33] refactor: extract ConfigureExpectedHostname from Open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open was mixing orchestration with OpenSSL hostname setup detail. The extracted helper collapses eleven lines of nested ifs and five return- points into a single named step whose short-circuit `&&` preserves the original "skip set1_host if SNI setup failed" semantics. Single-exit, positive-logic (== 1 rather than != 1) to match the MISRA style we're moving toward. Open itself still has eight early returns — addressed in subsequent refactorings as the remaining stages (transport BIO wiring, handshake) are extracted. No behaviour change; all seven hostname-related unit tests and the integration suite remain green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index dbfff044..912b9970 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -16,6 +16,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidS static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); +static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); @@ -78,16 +79,9 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress } BIO_set_data(bio, stream->config.transport); SSL_set_bio(stream->ssl, bio, bio); - if (stream->config.serverName != NULL) + if (!ConfigureExpectedHostname(stream)) { - if (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) != 1) - { - return false; - } - if (SSL_set1_host(stream->ssl, stream->config.serverName) != 1) - { - return false; - } + return false; } return SSL_connect(stream->ssl) > 0; } @@ -115,6 +109,17 @@ static void Close(struct SolidSyslogStream* self) SolidSyslogStream_Close(stream->config.transport); } +static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) +{ + bool ok = true; + if (stream->config.serverName != NULL) + { + ok = (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) == 1) + && (SSL_set1_host(stream->ssl, stream->config.serverName) == 1); + } + return ok; +} + static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); From fcf238bf8a67d415f81ac987d7e745fbf69474f0 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:46:47 +0100 Subject: [PATCH 22/33] refactor: extract AttachTransportBio from Open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps the BIO-over-transport wiring (create the custom BIO, attach the transport as its backing data, hand it to the SSL handle) in a single named step. Open loses four lines of OpenSSL-vocabulary detail. Single-exit, positive-logic (`bio != NULL`) form. Eight-line helper replaces four inline lines in Open, for net clarity at the call site where the named operation is doing the explaining. No behaviour change — same BIO_new / BIO_set_data / SSL_set_bio calls in the same order; pointer-chain tests (LastBio, LastSetData*, LastSetBio*) stay green. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 912b9970..a816b77e 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -16,6 +16,7 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidS static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); +static bool AttachTransportBio(struct SolidSyslogTlsStream* stream); static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); @@ -72,13 +73,10 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress { return false; } - BIO* bio = CreateTransportBio(stream); - if (bio == NULL) + if (!AttachTransportBio(stream)) { return false; } - BIO_set_data(bio, stream->config.transport); - SSL_set_bio(stream->ssl, bio, bio); if (!ConfigureExpectedHostname(stream)) { return false; @@ -109,6 +107,18 @@ static void Close(struct SolidSyslogStream* self) SolidSyslogStream_Close(stream->config.transport); } +static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) +{ + BIO* bio = CreateTransportBio(stream); + bool ok = bio != NULL; + if (ok) + { + BIO_set_data(bio, stream->config.transport); + SSL_set_bio(stream->ssl, bio, bio); + } + return ok; +} + static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) { bool ok = true; From 6887bf2f7f3cfb6744c36a9daf59233395392717 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:52:02 +0100 Subject: [PATCH 23/33] refactor: extract ReleaseHandshakeState from Close and Destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pulls the SSL/BIO_METHOD free-and-null pair out of both lifecycle endpoints. Close reads as three steps (shutdown, release, close transport). Destroy reads as two (release handshake state, free context). The null-guarding makes ReleaseHandshakeState safe to call from either path regardless of which subset of resources is live — already covered by the existing idempotence tests. No behaviour change; lifecycle-count tests (Close/Destroy variants for SSL_free and BIO_meth_free) stay green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index a816b77e..2f30588c 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -18,6 +18,7 @@ static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_ static void Close(struct SolidSyslogStream* self); static bool AttachTransportBio(struct SolidSyslogTlsStream* stream); static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); +static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); @@ -39,16 +40,7 @@ struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTl void SolidSyslogTlsStream_Destroy(void) { - if (instance.ssl != NULL) - { - SSL_free(instance.ssl); - instance.ssl = NULL; - } - if (instance.bioMethod != NULL) - { - BIO_meth_free(instance.bioMethod); - instance.bioMethod = NULL; - } + ReleaseHandshakeState(&instance); if (instance.ctx != NULL) { SSL_CTX_free(instance.ctx); @@ -100,13 +92,24 @@ static void Close(struct SolidSyslogStream* self) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; SSL_shutdown(stream->ssl); - SSL_free(stream->ssl); - stream->ssl = NULL; - BIO_meth_free(stream->bioMethod); - stream->bioMethod = NULL; + ReleaseHandshakeState(stream); SolidSyslogStream_Close(stream->config.transport); } +static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +{ + if (stream->ssl != NULL) + { + SSL_free(stream->ssl); + stream->ssl = NULL; + } + if (stream->bioMethod != NULL) + { + BIO_meth_free(stream->bioMethod); + stream->bioMethod = NULL; + } +} + static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) { BIO* bio = CreateTransportBio(stream); From a85ca00d55cffa732adbb126fa916dad87b76384 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:54:58 +0100 Subject: [PATCH 24/33] refactor: single-exit Open via && chain of named steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open is now a narrative of six named handshake stages combined with short-circuit &&, returning once. One return, all positive logic, one level of abstraction throughout. Introduces three small stream-side wrappers so every step in the chain has the uniform bool(*)(stream) signature: - InitSslContext — stores stream->ctx, returns bool - InitSslSession — stores stream->ssl, returns bool - PerformHandshake — wraps SSL_connect > 0 No behaviour change. Short-circuit evaluation preserves the original bail-out semantics: a failed step skips all subsequent ones exactly as the prior early-return chain did. All 20+ Open-path tests stay green — identical OpenSSL call sequence, unchanged call counts, unchanged pointer-chain assertions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 2f30588c..4dcc38f1 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -16,8 +16,11 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidS static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); static void Close(struct SolidSyslogStream* self); +static bool InitSslContext(struct SolidSyslogTlsStream* stream); +static bool InitSslSession(struct SolidSyslogTlsStream* stream); static bool AttachTransportBio(struct SolidSyslogTlsStream* stream); static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); +static bool PerformHandshake(struct SolidSyslogTlsStream* stream); static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); @@ -51,29 +54,12 @@ void SolidSyslogTlsStream_Destroy(void) static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; - if (!SolidSyslogStream_Open(stream->config.transport, addr)) - { - return false; - } - stream->ctx = CreateSslContext(&stream->config); - if (stream->ctx == NULL) - { - return false; - } - stream->ssl = SSL_new(stream->ctx); - if (stream->ssl == NULL) - { - return false; - } - if (!AttachTransportBio(stream)) - { - return false; - } - if (!ConfigureExpectedHostname(stream)) - { - return false; - } - return SSL_connect(stream->ssl) > 0; + return SolidSyslogStream_Open(stream->config.transport, addr) + && InitSslContext(stream) + && InitSslSession(stream) + && AttachTransportBio(stream) + && ConfigureExpectedHostname(stream) + && PerformHandshake(stream); } static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) @@ -96,18 +82,16 @@ static void Close(struct SolidSyslogStream* self) SolidSyslogStream_Close(stream->config.transport); } -static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +static bool InitSslContext(struct SolidSyslogTlsStream* stream) { - if (stream->ssl != NULL) - { - SSL_free(stream->ssl); - stream->ssl = NULL; - } - if (stream->bioMethod != NULL) - { - BIO_meth_free(stream->bioMethod); - stream->bioMethod = NULL; - } + stream->ctx = CreateSslContext(&stream->config); + return stream->ctx != NULL; +} + +static bool InitSslSession(struct SolidSyslogTlsStream* stream) +{ + stream->ssl = SSL_new(stream->ctx); + return stream->ssl != NULL; } static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) @@ -133,6 +117,25 @@ static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) return ok; } +static bool PerformHandshake(struct SolidSyslogTlsStream* stream) +{ + return SSL_connect(stream->ssl) > 0; +} + +static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +{ + if (stream->ssl != NULL) + { + SSL_free(stream->ssl); + stream->ssl = NULL; + } + if (stream->bioMethod != NULL) + { + BIO_meth_free(stream->bioMethod); + stream->bioMethod = NULL; + } +} + static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); From a36d9b2aab6b5f019f751fe740ef1a867c1097cc Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 06:57:48 +0100 Subject: [PATCH 25/33] refactor: single-exit CreateSslContext with named configurers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateSslContext now reads as allocate, configure, clean up on failure — three concepts, single exit. The per-concern configure step becomes: ConfigureTrustAnchors(ctx, path) — load CA bundle + pin SSL_VERIFY_PEER ConfigureProtocolFloor(ctx) — TLS 1.2 floor ConfigureCipherList(ctx, list) — optional TLS 1.2 cipher pinning composed by ConfigureSslContext via && chain. Each helper does one thing at one level of abstraction, named for what it does. Four duplicated SSL_CTX_free + return NULL branches collapse to a single cleanup site in CreateSslContext. Minor ordering shift: SSL_CTX_set_verify now runs after (rather than between) the fallible configuration steps — grouped with the trust-chain concern it belongs to. Semantically equivalent; no test pins the ordering. All 12+ CreateSslContext-facing tests stay green: identical OpenSSL call set with unchanged arg captures, call counts, and CTX-free counts on failure paths. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 4dcc38f1..44cb02c3 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -23,6 +23,10 @@ static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* s static bool PerformHandshake(struct SolidSyslogTlsStream* stream); static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); +static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); +static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); +static bool ConfigureProtocolFloor(SSL_CTX* ctx); +static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static int TransportBioCreate(BIO* bio); static int TransportBioRead(BIO* bio, char* buffer, int size); @@ -202,25 +206,42 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); - if (ctx == NULL) - { - return NULL; - } - if (SSL_CTX_load_verify_locations(ctx, config->caBundlePath, NULL) != 1) + if (ctx != NULL && !ConfigureSslContext(ctx, config)) { SSL_CTX_free(ctx); - return NULL; + ctx = NULL; } - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); - if (SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) != 1) + return ctx; +} + +static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) +{ + return ConfigureTrustAnchors(ctx, config->caBundlePath) + && ConfigureProtocolFloor(ctx) + && ConfigureCipherList(ctx, config->cipherList); +} + +static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) +{ + bool ok = SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) == 1; + if (ok) { - SSL_CTX_free(ctx); - return NULL; + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); } - if (config->cipherList != NULL && SSL_CTX_set_cipher_list(ctx, config->cipherList) != 1) + return ok; +} + +static bool ConfigureProtocolFloor(SSL_CTX* ctx) +{ + return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; +} + +static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) +{ + bool ok = true; + if (cipherList != NULL) { - SSL_CTX_free(ctx); - return NULL; + ok = SSL_CTX_set_cipher_list(ctx, cipherList) == 1; } - return ctx; + return ok; } From 896072c7dcdc12b5b503ae2bafce60a234fd1013 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:00:48 +0100 Subject: [PATCH 26/33] refactor: split CreateTransportBioMethod out of CreateTransportBio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two concerns — building the BIO_METHOD vtable and using it to allocate a BIO — are now separate functions, each with a single exit and one clear job. CreateTransportBioMethod handles BIO_meth_new + BIO_meth_set_*; returns NULL if allocation fails (guarding the setters). CreateTransportBio reads as: get the method, use it to make a BIO, clean up inline if BIO allocation fails so the BioNewFailureFreesBioMethodInline contract still holds. Three returns drop to one per function. No behaviour change — OpenSSL call sequence, arg captures, and failure-path cleanup counts all match the existing BIO-pathway tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 44cb02c3..a41d3d72 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -28,6 +28,7 @@ static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundle static bool ConfigureProtocolFloor(SSL_CTX* ctx); static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); +static BIO_METHOD* CreateTransportBioMethod(void); static int TransportBioCreate(BIO* bio); static int TransportBioRead(BIO* bio, char* buffer, int size); static int TransportBioWrite(BIO* bio, const char* buffer, int size); @@ -142,24 +143,31 @@ static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) { - BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); - if (method == NULL) + stream->bioMethod = CreateTransportBioMethod(); + BIO* bio = NULL; + if (stream->bioMethod != NULL) { - return NULL; + bio = BIO_new(stream->bioMethod); + if (bio == NULL) + { + BIO_meth_free(stream->bioMethod); + stream->bioMethod = NULL; + } } - BIO_meth_set_create(method, TransportBioCreate); - BIO_meth_set_read(method, TransportBioRead); - BIO_meth_set_write(method, TransportBioWrite); - BIO_meth_set_ctrl(method, TransportBioCtrl); - stream->bioMethod = method; - BIO* bio = BIO_new(method); - if (bio == NULL) + return bio; +} + +static BIO_METHOD* CreateTransportBioMethod(void) +{ + BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); + if (method != NULL) { - BIO_meth_free(method); - stream->bioMethod = NULL; - return NULL; + BIO_meth_set_create(method, TransportBioCreate); + BIO_meth_set_read(method, TransportBioRead); + BIO_meth_set_write(method, TransportBioWrite); + BIO_meth_set_ctrl(method, TransportBioCtrl); } - return bio; + return method; } /* Called when BIO_new instantiates a BIO from our method. Marking init=1 tells From 79dadcd18a4887872f0827ca676387db0519b895 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:03:16 +0100 Subject: [PATCH 27/33] refactor: single-exit TransportBioCtrl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch/case with two returns becomes a single return over a result accumulator that defaults to 0 (failure) and is set to 1 for the recognised commands. Same behaviour — the three BIO_CTRL tests stay green. Last multi-return function in SolidSyslogTlsStream.c. The file is now fully single-exit, positive-logic, with every function doing one thing at one level of abstraction. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index a41d3d72..f05e5737 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -199,16 +199,19 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) (void) bio; (void) larg; (void) parg; + long result = 0; switch (cmd) { case BIO_CTRL_FLUSH: case BIO_CTRL_PUSH: case BIO_CTRL_POP: case BIO_CTRL_DUP: - return 1; + result = 1; + break; default: - return 0; + break; } + return result; } static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) From 53ad1ea212e4022f251b74bcf796ae1ba38a7314 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:11:48 +0100 Subject: [PATCH 28/33] refactor: order functions top-down, depth-first from callers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Function bodies are now in the "helpers directly beneath first caller" order that the rest of the codebase follows: Create Destroy ReleaseHandshakeState (Destroy's helper, also shared with Close) Open InitSslContext (1st step) CreateSslContext ConfigureSslContext ConfigureTrustAnchors ConfigureProtocolFloor ConfigureCipherList InitSslSession (2nd step) AttachTransportBio (3rd step) CreateTransportBio CreateTransportBioMethod TransportBioCreate (registered callbacks) TransportBioCtrl TransportBioRead TransportBioWrite ConfigureExpectedHostname (4th step) PerformHandshake (5th step) Send Read Close Reading top-to-bottom takes the reader from public API down through the handshake pipeline into the raw OpenSSL wiring at the bottom. Each scroll-down goes one level deeper. Forward declarations at the top are now alphabetical — a predictable table of contents that doesn't need manual re-sorting when helpers are added. No behaviour change; full test / tidy / format run green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index f05e5737..851db409 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -12,27 +12,27 @@ struct SolidSyslogTlsStream BIO_METHOD* bioMethod; }; -static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); -static void Close(struct SolidSyslogStream* self); -static bool InitSslContext(struct SolidSyslogTlsStream* stream); -static bool InitSslSession(struct SolidSyslogTlsStream* stream); static bool AttachTransportBio(struct SolidSyslogTlsStream* stream); +static void Close(struct SolidSyslogStream* self); +static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); -static bool PerformHandshake(struct SolidSyslogTlsStream* stream); -static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); -static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); +static bool ConfigureProtocolFloor(SSL_CTX* ctx); static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); -static bool ConfigureProtocolFloor(SSL_CTX* ctx); -static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); +static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); static BIO_METHOD* CreateTransportBioMethod(void); +static bool InitSslContext(struct SolidSyslogTlsStream* stream); +static bool InitSslSession(struct SolidSyslogTlsStream* stream); +static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool PerformHandshake(struct SolidSyslogTlsStream* stream); +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static int TransportBioCreate(BIO* bio); +static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); static int TransportBioRead(BIO* bio, char* buffer, int size); static int TransportBioWrite(BIO* bio, const char* buffer, int size); -static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); static struct SolidSyslogTlsStream instance; @@ -56,6 +56,20 @@ void SolidSyslogTlsStream_Destroy(void) } } +static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +{ + if (stream->ssl != NULL) + { + SSL_free(stream->ssl); + stream->ssl = NULL; + } + if (stream->bioMethod != NULL) + { + BIO_meth_free(stream->bioMethod); + stream->bioMethod = NULL; + } +} + static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; @@ -67,78 +81,71 @@ static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress && PerformHandshake(stream); } -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) -{ - struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; - return SSL_write(stream->ssl, buffer, (int) size) > 0; -} - -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) -{ - struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; - return (SolidSyslogSsize) SSL_read(stream->ssl, buffer, (int) size); -} - -static void Close(struct SolidSyslogStream* self) -{ - struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; - SSL_shutdown(stream->ssl); - ReleaseHandshakeState(stream); - SolidSyslogStream_Close(stream->config.transport); -} - static bool InitSslContext(struct SolidSyslogTlsStream* stream) { stream->ctx = CreateSslContext(&stream->config); return stream->ctx != NULL; } -static bool InitSslSession(struct SolidSyslogTlsStream* stream) +static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) { - stream->ssl = SSL_new(stream->ctx); - return stream->ssl != NULL; + SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); + if (ctx != NULL && !ConfigureSslContext(ctx, config)) + { + SSL_CTX_free(ctx); + ctx = NULL; + } + return ctx; } -static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) +static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) { - BIO* bio = CreateTransportBio(stream); - bool ok = bio != NULL; + return ConfigureTrustAnchors(ctx, config->caBundlePath) + && ConfigureProtocolFloor(ctx) + && ConfigureCipherList(ctx, config->cipherList); +} + +static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) +{ + bool ok = SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) == 1; if (ok) { - BIO_set_data(bio, stream->config.transport); - SSL_set_bio(stream->ssl, bio, bio); + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); } return ok; } -static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) +static bool ConfigureProtocolFloor(SSL_CTX* ctx) +{ + return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; +} + +static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) { bool ok = true; - if (stream->config.serverName != NULL) + if (cipherList != NULL) { - ok = (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) == 1) - && (SSL_set1_host(stream->ssl, stream->config.serverName) == 1); + ok = SSL_CTX_set_cipher_list(ctx, cipherList) == 1; } return ok; } -static bool PerformHandshake(struct SolidSyslogTlsStream* stream) +static bool InitSslSession(struct SolidSyslogTlsStream* stream) { - return SSL_connect(stream->ssl) > 0; + stream->ssl = SSL_new(stream->ctx); + return stream->ssl != NULL; } -static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) { - if (stream->ssl != NULL) - { - SSL_free(stream->ssl); - stream->ssl = NULL; - } - if (stream->bioMethod != NULL) + BIO* bio = CreateTransportBio(stream); + bool ok = bio != NULL; + if (ok) { - BIO_meth_free(stream->bioMethod); - stream->bioMethod = NULL; + BIO_set_data(bio, stream->config.transport); + SSL_set_bio(stream->ssl, bio, bio); } + return ok; } static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) @@ -214,45 +221,38 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) return result; } -static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) +static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) { - SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); - if (ctx != NULL && !ConfigureSslContext(ctx, config)) + bool ok = true; + if (stream->config.serverName != NULL) { - SSL_CTX_free(ctx); - ctx = NULL; + ok = (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) == 1) + && (SSL_set1_host(stream->ssl, stream->config.serverName) == 1); } - return ctx; + return ok; } -static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) +static bool PerformHandshake(struct SolidSyslogTlsStream* stream) { - return ConfigureTrustAnchors(ctx, config->caBundlePath) - && ConfigureProtocolFloor(ctx) - && ConfigureCipherList(ctx, config->cipherList); + return SSL_connect(stream->ssl) > 0; } -static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) +static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) { - bool ok = SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) == 1; - if (ok) - { - SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); - } - return ok; + struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; + return SSL_write(stream->ssl, buffer, (int) size) > 0; } -static bool ConfigureProtocolFloor(SSL_CTX* ctx) +static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) { - return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; + struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; + return (SolidSyslogSsize) SSL_read(stream->ssl, buffer, (int) size); } -static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) +static void Close(struct SolidSyslogStream* self) { - bool ok = true; - if (cipherList != NULL) - { - ok = SSL_CTX_set_cipher_list(ctx, cipherList) == 1; - } - return ok; + struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; + SSL_shutdown(stream->ssl); + ReleaseHandshakeState(stream); + SolidSyslogStream_Close(stream->config.transport); } From 152e3bbfc2e8034c0f25476b6afdea8f864f1478 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:15:35 +0100 Subject: [PATCH 29/33] refactor: extract ReleaseSsl, ReleaseBioMethod, ReleaseSslContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every "if X != NULL, free X, null X" block in the file now lives in a one-job helper named for the resource it releases: ReleaseSsl — frees and nulls stream->ssl ReleaseBioMethod — frees and nulls stream->bioMethod ReleaseSslContext — frees and nulls stream->ctx Destroy becomes two lines of named operations: ReleaseHandshakeState(&instance); ReleaseSslContext(&instance); ReleaseHandshakeState becomes: ReleaseSsl(stream); ReleaseBioMethod(stream); CreateTransportBio's inline BIO_meth_free cleanup collapses to ReleaseBioMethod(stream) — same operation, same name. No behaviour change; all lifecycle-count tests and the integration suite stay green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 851db409..d7534901 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -27,7 +27,10 @@ static bool InitSslSession(struct SolidSyslogTlsStream* stream); static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); static bool PerformHandshake(struct SolidSyslogTlsStream* stream); static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void ReleaseBioMethod(struct SolidSyslogTlsStream* stream); static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); +static void ReleaseSsl(struct SolidSyslogTlsStream* stream); +static void ReleaseSslContext(struct SolidSyslogTlsStream* stream); static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); static int TransportBioCreate(BIO* bio); static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); @@ -49,20 +52,26 @@ struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTl void SolidSyslogTlsStream_Destroy(void) { ReleaseHandshakeState(&instance); - if (instance.ctx != NULL) - { - SSL_CTX_free(instance.ctx); - instance.ctx = NULL; - } + ReleaseSslContext(&instance); } static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +{ + ReleaseSsl(stream); + ReleaseBioMethod(stream); +} + +static void ReleaseSsl(struct SolidSyslogTlsStream* stream) { if (stream->ssl != NULL) { SSL_free(stream->ssl); stream->ssl = NULL; } +} + +static void ReleaseBioMethod(struct SolidSyslogTlsStream* stream) +{ if (stream->bioMethod != NULL) { BIO_meth_free(stream->bioMethod); @@ -70,6 +79,15 @@ static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) } } +static void ReleaseSslContext(struct SolidSyslogTlsStream* stream) +{ + if (stream->ctx != NULL) + { + SSL_CTX_free(stream->ctx); + stream->ctx = NULL; + } +} + static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; @@ -157,8 +175,7 @@ static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) bio = BIO_new(stream->bioMethod); if (bio == NULL) { - BIO_meth_free(stream->bioMethod); - stream->bioMethod = NULL; + ReleaseBioMethod(stream); } } return bio; From 5755cce387de4c388b3b0ed3bf627396ff4cf521 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:18:04 +0100 Subject: [PATCH 30/33] refactor: vtable is a static const, not per-instance assignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four function-pointer slots on the base struct are constant for the lifetime of the module — they never change after Create and never vary between instances (there is only one instance). Lifting them into a file-scope const VTABLE and assigning it as a struct copy in Create communicates that invariant and collapses four per-call writes into one. Create now reads: instance.config = *config; instance.base = VTABLE; No behaviour change; test/tidy/format all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- Platform/OpenSsl/Source/SolidSyslogTlsStream.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index d7534901..9f6f9b47 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -37,15 +37,19 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* par static int TransportBioRead(BIO* bio, char* buffer, int size); static int TransportBioWrite(BIO* bio, const char* buffer, int size); +static const struct SolidSyslogStream VTABLE = { + .Open = Open, + .Send = Send, + .Read = Read, + .Close = Close, +}; + static struct SolidSyslogTlsStream instance; struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTlsStreamConfig* config) { - instance.config = *config; - instance.base.Open = Open; - instance.base.Send = Send; - instance.base.Read = Read; - instance.base.Close = Close; + instance.config = *config; + instance.base = VTABLE; return &instance.base; } From 913ec4e5989b17c527714c46b2c40afbd760724d Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:27:08 +0100 Subject: [PATCH 31/33] refactor: prefix all TU-local functions with TlsStream_ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every static function in the file gets the TlsStream_ prefix, so the 21 internal-linkage identifiers are unique across the codebase and stack traces / debuggers name the module explicitly — e.g. TlsStream_Open instead of bare Open (which collides with Open in PosixTcpStream.c). Addresses MISRA C:2012 Rule 5.9 (internal-linkage identifier uniqueness) for this file. Consistent with the SolidSyslogTlsStream_ prefix on the public API: public gets the full project name, private gets the module-name shorthand. Rest of the codebase (PosixTcpStream.c, PosixUdpSender.c, etc.) will follow the same pattern in a separate chore; this commit establishes the convention on the file we've been refactoring. No behaviour change; test/tidy/format all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index 9f6f9b47..e8173c24 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -12,36 +12,36 @@ struct SolidSyslogTlsStream BIO_METHOD* bioMethod; }; -static bool AttachTransportBio(struct SolidSyslogTlsStream* stream); -static void Close(struct SolidSyslogStream* self); -static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); -static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); -static bool ConfigureProtocolFloor(SSL_CTX* ctx); -static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); -static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); -static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); -static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream); -static BIO_METHOD* CreateTransportBioMethod(void); -static bool InitSslContext(struct SolidSyslogTlsStream* stream); -static bool InitSslSession(struct SolidSyslogTlsStream* stream); -static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); -static bool PerformHandshake(struct SolidSyslogTlsStream* stream); -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size); -static void ReleaseBioMethod(struct SolidSyslogTlsStream* stream); -static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); -static void ReleaseSsl(struct SolidSyslogTlsStream* stream); -static void ReleaseSslContext(struct SolidSyslogTlsStream* stream); -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size); -static int TransportBioCreate(BIO* bio); -static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); -static int TransportBioRead(BIO* bio, char* buffer, int size); -static int TransportBioWrite(BIO* bio, const char* buffer, int size); +static bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream); +static void TlsStream_Close(struct SolidSyslogStream* self); +static bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); +static bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); +static bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx); +static bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); +static bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); +static SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); +static BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream); +static BIO_METHOD* TlsStream_CreateTransportBioMethod(void); +static bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream); +static bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream); +static bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream); +static SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream); +static void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); +static void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream); +static void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream); +static bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static int TlsStream_TransportBioCreate(BIO* bio); +static long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); +static int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size); +static int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size); static const struct SolidSyslogStream VTABLE = { - .Open = Open, - .Send = Send, - .Read = Read, - .Close = Close, + .Open = TlsStream_Open, + .Send = TlsStream_Send, + .Read = TlsStream_Read, + .Close = TlsStream_Close, }; static struct SolidSyslogTlsStream instance; @@ -55,17 +55,17 @@ struct SolidSyslogStream* SolidSyslogTlsStream_Create(const struct SolidSyslogTl void SolidSyslogTlsStream_Destroy(void) { - ReleaseHandshakeState(&instance); - ReleaseSslContext(&instance); + TlsStream_ReleaseHandshakeState(&instance); + TlsStream_ReleaseSslContext(&instance); } -static void ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +static void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) { - ReleaseSsl(stream); - ReleaseBioMethod(stream); + TlsStream_ReleaseSsl(stream); + TlsStream_ReleaseBioMethod(stream); } -static void ReleaseSsl(struct SolidSyslogTlsStream* stream) +static void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream) { if (stream->ssl != NULL) { @@ -74,7 +74,7 @@ static void ReleaseSsl(struct SolidSyslogTlsStream* stream) } } -static void ReleaseBioMethod(struct SolidSyslogTlsStream* stream) +static void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream) { if (stream->bioMethod != NULL) { @@ -83,7 +83,7 @@ static void ReleaseBioMethod(struct SolidSyslogTlsStream* stream) } } -static void ReleaseSslContext(struct SolidSyslogTlsStream* stream) +static void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream) { if (stream->ctx != NULL) { @@ -92,27 +92,27 @@ static void ReleaseSslContext(struct SolidSyslogTlsStream* stream) } } -static bool Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +static bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; return SolidSyslogStream_Open(stream->config.transport, addr) - && InitSslContext(stream) - && InitSslSession(stream) - && AttachTransportBio(stream) - && ConfigureExpectedHostname(stream) - && PerformHandshake(stream); + && TlsStream_InitSslContext(stream) + && TlsStream_InitSslSession(stream) + && TlsStream_AttachTransportBio(stream) + && TlsStream_ConfigureExpectedHostname(stream) + && TlsStream_PerformHandshake(stream); } -static bool InitSslContext(struct SolidSyslogTlsStream* stream) +static bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream) { - stream->ctx = CreateSslContext(&stream->config); + stream->ctx = TlsStream_CreateSslContext(&stream->config); return stream->ctx != NULL; } -static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) +static SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); - if (ctx != NULL && !ConfigureSslContext(ctx, config)) + if (ctx != NULL && !TlsStream_ConfigureSslContext(ctx, config)) { SSL_CTX_free(ctx); ctx = NULL; @@ -120,14 +120,14 @@ static SSL_CTX* CreateSslContext(const struct SolidSyslogTlsStreamConfig* config return ctx; } -static bool ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) +static bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) { - return ConfigureTrustAnchors(ctx, config->caBundlePath) - && ConfigureProtocolFloor(ctx) - && ConfigureCipherList(ctx, config->cipherList); + return TlsStream_ConfigureTrustAnchors(ctx, config->caBundlePath) + && TlsStream_ConfigureProtocolFloor(ctx) + && TlsStream_ConfigureCipherList(ctx, config->cipherList); } -static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) +static bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) { bool ok = SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) == 1; if (ok) @@ -137,12 +137,12 @@ static bool ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) return ok; } -static bool ConfigureProtocolFloor(SSL_CTX* ctx) +static bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx) { return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; } -static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) +static bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) { bool ok = true; if (cipherList != NULL) @@ -152,15 +152,15 @@ static bool ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) return ok; } -static bool InitSslSession(struct SolidSyslogTlsStream* stream) +static bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream) { stream->ssl = SSL_new(stream->ctx); return stream->ssl != NULL; } -static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) +static bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream) { - BIO* bio = CreateTransportBio(stream); + BIO* bio = TlsStream_CreateTransportBio(stream); bool ok = bio != NULL; if (ok) { @@ -170,49 +170,49 @@ static bool AttachTransportBio(struct SolidSyslogTlsStream* stream) return ok; } -static BIO* CreateTransportBio(struct SolidSyslogTlsStream* stream) +static BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream) { - stream->bioMethod = CreateTransportBioMethod(); + stream->bioMethod = TlsStream_CreateTransportBioMethod(); BIO* bio = NULL; if (stream->bioMethod != NULL) { bio = BIO_new(stream->bioMethod); if (bio == NULL) { - ReleaseBioMethod(stream); + TlsStream_ReleaseBioMethod(stream); } } return bio; } -static BIO_METHOD* CreateTransportBioMethod(void) +static BIO_METHOD* TlsStream_CreateTransportBioMethod(void) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); if (method != NULL) { - BIO_meth_set_create(method, TransportBioCreate); - BIO_meth_set_read(method, TransportBioRead); - BIO_meth_set_write(method, TransportBioWrite); - BIO_meth_set_ctrl(method, TransportBioCtrl); + BIO_meth_set_create(method, TlsStream_TransportBioCreate); + BIO_meth_set_read(method, TlsStream_TransportBioRead); + BIO_meth_set_write(method, TlsStream_TransportBioWrite); + BIO_meth_set_ctrl(method, TlsStream_TransportBioCtrl); } return method; } /* Called when BIO_new instantiates a BIO from our method. Marking init=1 tells * OpenSSL the BIO is ready for I/O; without it SSL_connect bails early. */ -static int TransportBioCreate(BIO* bio) +static int TlsStream_TransportBioCreate(BIO* bio) { BIO_set_init(bio, 1); return 1; } -static int TransportBioRead(BIO* bio, char* buffer, int size) +static int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size) { struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio); return (int) SolidSyslogStream_Read(transport, buffer, (size_t) size); } -static int TransportBioWrite(BIO* bio, const char* buffer, int size) +static int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size) { struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio); return SolidSyslogStream_Send(transport, buffer, (size_t) size) ? size : -1; @@ -222,7 +222,7 @@ static int TransportBioWrite(BIO* bio, const char* buffer, int size) * during normal operation; returning 1 for the common lifecycle commands lets * SSL_connect / SSL_write / SSL_shutdown proceed. Unknown commands return 0. */ // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by OpenSSL BIO_ctrl_fn contract -static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) +static long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) { (void) bio; (void) larg; @@ -242,7 +242,7 @@ static long TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) return result; } -static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) +static bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) { bool ok = true; if (stream->config.serverName != NULL) @@ -253,27 +253,27 @@ static bool ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) return ok; } -static bool PerformHandshake(struct SolidSyslogTlsStream* stream) +static bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream) { return SSL_connect(stream->ssl) > 0; } -static bool Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +static bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; return SSL_write(stream->ssl, buffer, (int) size) > 0; } -static SolidSyslogSsize Read(struct SolidSyslogStream* self, void* buffer, size_t size) +static SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; return (SolidSyslogSsize) SSL_read(stream->ssl, buffer, (int) size); } -static void Close(struct SolidSyslogStream* self) +static void TlsStream_Close(struct SolidSyslogStream* self) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; SSL_shutdown(stream->ssl); - ReleaseHandshakeState(stream); + TlsStream_ReleaseHandshakeState(stream); SolidSyslogStream_Close(stream->config.transport); } From 9703ab8218d122e3581607dc488d90c38ee8ee73 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:32:54 +0100 Subject: [PATCH 32/33] refactor: mark all TU-local static functions static inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uniform static inline on every private function in the file. The compiler was free to inline these at -O2 anyway; the explicit keyword: - makes intent visible at the declaration - gives the same signal at -O0 (debug builds) - does not break vtable / callback address-taking — static inline still generates an addressable definition when needed MISRA C:2012 has no rule against static inline. Coverage remains 100% (gcov attributes source lines regardless of inlining). No behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../OpenSsl/Source/SolidSyslogTlsStream.c | 112 +++++++++--------- 1 file changed, 53 insertions(+), 59 deletions(-) diff --git a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c index e8173c24..91a7be53 100644 --- a/Platform/OpenSsl/Source/SolidSyslogTlsStream.c +++ b/Platform/OpenSsl/Source/SolidSyslogTlsStream.c @@ -12,30 +12,30 @@ struct SolidSyslogTlsStream BIO_METHOD* bioMethod; }; -static bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream); -static void TlsStream_Close(struct SolidSyslogStream* self); -static bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); -static bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); -static bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx); -static bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); -static bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); -static SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); -static BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream); -static BIO_METHOD* TlsStream_CreateTransportBioMethod(void); -static bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream); -static bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream); -static bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); -static bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream); -static SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size); -static void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream); -static void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); -static void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream); -static void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream); -static bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size); -static int TlsStream_TransportBioCreate(BIO* bio); -static long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); -static int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size); -static int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size); +static inline bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream); +static inline void TlsStream_Close(struct SolidSyslogStream* self); +static inline bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList); +static inline bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream); +static inline bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx); +static inline bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config); +static inline bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath); +static inline SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config); +static inline BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream); +static inline BIO_METHOD* TlsStream_CreateTransportBioMethod(void); +static inline bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream); +static inline bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream); +static inline bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr); +static inline bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream); +static inline SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size); +static inline void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream); +static inline void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream); +static inline void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream); +static inline void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream); +static inline bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size); +static inline int TlsStream_TransportBioCreate(BIO* bio); +static inline long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg); +static inline int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size); +static inline int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size); static const struct SolidSyslogStream VTABLE = { .Open = TlsStream_Open, @@ -59,13 +59,13 @@ void SolidSyslogTlsStream_Destroy(void) TlsStream_ReleaseSslContext(&instance); } -static void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) +static inline void TlsStream_ReleaseHandshakeState(struct SolidSyslogTlsStream* stream) { TlsStream_ReleaseSsl(stream); TlsStream_ReleaseBioMethod(stream); } -static void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream) +static inline void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream) { if (stream->ssl != NULL) { @@ -74,7 +74,7 @@ static void TlsStream_ReleaseSsl(struct SolidSyslogTlsStream* stream) } } -static void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream) +static inline void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream) { if (stream->bioMethod != NULL) { @@ -83,7 +83,7 @@ static void TlsStream_ReleaseBioMethod(struct SolidSyslogTlsStream* stream) } } -static void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream) +static inline void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream) { if (stream->ctx != NULL) { @@ -92,24 +92,20 @@ static void TlsStream_ReleaseSslContext(struct SolidSyslogTlsStream* stream) } } -static bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) +static inline bool TlsStream_Open(struct SolidSyslogStream* self, const struct SolidSyslogAddress* addr) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; - return SolidSyslogStream_Open(stream->config.transport, addr) - && TlsStream_InitSslContext(stream) - && TlsStream_InitSslSession(stream) - && TlsStream_AttachTransportBio(stream) - && TlsStream_ConfigureExpectedHostname(stream) - && TlsStream_PerformHandshake(stream); + return SolidSyslogStream_Open(stream->config.transport, addr) && TlsStream_InitSslContext(stream) && TlsStream_InitSslSession(stream) && + TlsStream_AttachTransportBio(stream) && TlsStream_ConfigureExpectedHostname(stream) && TlsStream_PerformHandshake(stream); } -static bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream) +static inline bool TlsStream_InitSslContext(struct SolidSyslogTlsStream* stream) { stream->ctx = TlsStream_CreateSslContext(&stream->config); return stream->ctx != NULL; } -static SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) +static inline SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConfig* config) { SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); if (ctx != NULL && !TlsStream_ConfigureSslContext(ctx, config)) @@ -120,14 +116,13 @@ static SSL_CTX* TlsStream_CreateSslContext(const struct SolidSyslogTlsStreamConf return ctx; } -static bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) +static inline bool TlsStream_ConfigureSslContext(SSL_CTX* ctx, const struct SolidSyslogTlsStreamConfig* config) { - return TlsStream_ConfigureTrustAnchors(ctx, config->caBundlePath) - && TlsStream_ConfigureProtocolFloor(ctx) - && TlsStream_ConfigureCipherList(ctx, config->cipherList); + return TlsStream_ConfigureTrustAnchors(ctx, config->caBundlePath) && TlsStream_ConfigureProtocolFloor(ctx) && + TlsStream_ConfigureCipherList(ctx, config->cipherList); } -static bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) +static inline bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePath) { bool ok = SSL_CTX_load_verify_locations(ctx, caBundlePath, NULL) == 1; if (ok) @@ -137,12 +132,12 @@ static bool TlsStream_ConfigureTrustAnchors(SSL_CTX* ctx, const char* caBundlePa return ok; } -static bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx) +static inline bool TlsStream_ConfigureProtocolFloor(SSL_CTX* ctx) { return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) == 1; } -static bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) +static inline bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) { bool ok = true; if (cipherList != NULL) @@ -152,13 +147,13 @@ static bool TlsStream_ConfigureCipherList(SSL_CTX* ctx, const char* cipherList) return ok; } -static bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream) +static inline bool TlsStream_InitSslSession(struct SolidSyslogTlsStream* stream) { stream->ssl = SSL_new(stream->ctx); return stream->ssl != NULL; } -static bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream) +static inline bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream) { BIO* bio = TlsStream_CreateTransportBio(stream); bool ok = bio != NULL; @@ -170,7 +165,7 @@ static bool TlsStream_AttachTransportBio(struct SolidSyslogTlsStream* stream) return ok; } -static BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream) +static inline BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream) { stream->bioMethod = TlsStream_CreateTransportBioMethod(); BIO* bio = NULL; @@ -185,7 +180,7 @@ static BIO* TlsStream_CreateTransportBio(struct SolidSyslogTlsStream* stream) return bio; } -static BIO_METHOD* TlsStream_CreateTransportBioMethod(void) +static inline BIO_METHOD* TlsStream_CreateTransportBioMethod(void) { BIO_METHOD* method = BIO_meth_new(BIO_TYPE_SOURCE_SINK, "SolidSyslog transport BIO"); if (method != NULL) @@ -200,19 +195,19 @@ static BIO_METHOD* TlsStream_CreateTransportBioMethod(void) /* Called when BIO_new instantiates a BIO from our method. Marking init=1 tells * OpenSSL the BIO is ready for I/O; without it SSL_connect bails early. */ -static int TlsStream_TransportBioCreate(BIO* bio) +static inline int TlsStream_TransportBioCreate(BIO* bio) { BIO_set_init(bio, 1); return 1; } -static int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size) +static inline int TlsStream_TransportBioRead(BIO* bio, char* buffer, int size) { struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio); return (int) SolidSyslogStream_Read(transport, buffer, (size_t) size); } -static int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size) +static inline int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size) { struct SolidSyslogStream* transport = (struct SolidSyslogStream*) BIO_get_data(bio); return SolidSyslogStream_Send(transport, buffer, (size_t) size) ? size : -1; @@ -222,7 +217,7 @@ static int TlsStream_TransportBioWrite(BIO* bio, const char* buffer, int size) * during normal operation; returning 1 for the common lifecycle commands lets * SSL_connect / SSL_write / SSL_shutdown proceed. Unknown commands return 0. */ // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) -- signature fixed by OpenSSL BIO_ctrl_fn contract -static long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) +static inline long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) { (void) bio; (void) larg; @@ -242,35 +237,34 @@ static long TlsStream_TransportBioCtrl(BIO* bio, int cmd, long larg, void* parg) return result; } -static bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) +static inline bool TlsStream_ConfigureExpectedHostname(struct SolidSyslogTlsStream* stream) { bool ok = true; if (stream->config.serverName != NULL) { - ok = (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) == 1) - && (SSL_set1_host(stream->ssl, stream->config.serverName) == 1); + ok = (SSL_set_tlsext_host_name(stream->ssl, stream->config.serverName) == 1) && (SSL_set1_host(stream->ssl, stream->config.serverName) == 1); } return ok; } -static bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream) +static inline bool TlsStream_PerformHandshake(struct SolidSyslogTlsStream* stream) { return SSL_connect(stream->ssl) > 0; } -static bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size) +static inline bool TlsStream_Send(struct SolidSyslogStream* self, const void* buffer, size_t size) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; return SSL_write(stream->ssl, buffer, (int) size) > 0; } -static SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) +static inline SolidSyslogSsize TlsStream_Read(struct SolidSyslogStream* self, void* buffer, size_t size) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; return (SolidSyslogSsize) SSL_read(stream->ssl, buffer, (int) size); } -static void TlsStream_Close(struct SolidSyslogStream* self) +static inline void TlsStream_Close(struct SolidSyslogStream* self) { struct SolidSyslogTlsStream* stream = (struct SolidSyslogTlsStream*) self; SSL_shutdown(stream->ssl); From 69b5fdf3792bf257a97994587eef8e5793b2ec37 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 22 Apr 2026 07:59:43 +0100 Subject: [PATCH 33/33] fix: S03.08 OpenSslFake stores BIO data per-instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BIO_set_data and BIO_get_data shared a single global storage slot, so multiple BIOs aliased — bio2's data overwrote bio1's. BIO_new also returned a singleton pointer, hiding the aliasing further (every BIO was the same pointer). Replaced with a small fixed-size pool. Each pool slot is independent; the address of slot.slot is the BIO handle, slot.data is the per-BIO storage that backs BIO_set_data / BIO_get_data. BIO_new hands out the next free slot, Reset zeros the pool. Pool size 4 is plenty for current tests (production uses one BIO per Open); easy to bump if a future test needs more. The existing global lastSetDataArg is preserved so the existing OpenSslFake_LastSetDataArg accessor (which reports "the most recent data passed to BIO_set_data") continues to work. Driven by BioDataIsStoredPerInstance in OpenSslFakeTest.cpp: - BIO_new returns distinct pointers per call - BIO_get_data(bio1) returns the data set on bio1, not on bio2 Closes the last outstanding S03.08 acceptance bullet ("OpenSslFake stores BIO data per-instance"). Co-Authored-By: Claude Opus 4.7 (1M context) --- Tests/OpenSslFakeTest.cpp | 13 +++++ Tests/Support/OpenSslFake.c | 113 +++++++++++++++++++++++++----------- 2 files changed, 91 insertions(+), 35 deletions(-) diff --git a/Tests/OpenSslFakeTest.cpp b/Tests/OpenSslFakeTest.cpp index 8822a368..3037de89 100644 --- a/Tests/OpenSslFakeTest.cpp +++ b/Tests/OpenSslFakeTest.cpp @@ -179,6 +179,19 @@ TEST(OpenSslFake, BioGetDataReturnsPreviouslySetData) POINTERS_EQUAL(&sentinel, BIO_get_data(bio)); } +TEST(OpenSslFake, BioDataIsStoredPerInstance) +{ + BIO_METHOD* method = BIO_meth_new(0, "fake"); + BIO* bio1 = BIO_new(method); + BIO* bio2 = BIO_new(method); + int a = 0; + int b = 0; + BIO_set_data(bio1, &a); + BIO_set_data(bio2, &b); + POINTERS_EQUAL(&a, BIO_get_data(bio1)); + POINTERS_EQUAL(&b, BIO_get_data(bio2)); +} + // NOLINTNEXTLINE(readability-non-const-parameter) -- signature fixed by OpenSSL BIO_meth_set_read contract static int DummyRead(BIO* bio, char* buf, int size) { diff --git a/Tests/Support/OpenSslFake.c b/Tests/Support/OpenSslFake.c index 1c6a88e9..38a5220f 100644 --- a/Tests/Support/OpenSslFake.c +++ b/Tests/Support/OpenSslFake.c @@ -17,7 +17,24 @@ static char fakeCtxStorage; static char fakeMethodStorage; static char fakeSslStorage; static char fakeBioMethStorage; -static char fakeBioStorage; + +/* Pool of fake BIOs. Each slot is independent so callers can exercise + * multiple BIOs without aliasing. The address of `slot` is the BIO handle + * returned from BIO_new; `data` is the per-BIO storage that backs + * BIO_set_data / BIO_get_data. */ +typedef struct +{ + char slot; + void* data; +} FakeBio; + +enum +{ + FAKE_BIO_POOL_SIZE = 4 +}; + +static FakeBio fakeBios[FAKE_BIO_POOL_SIZE]; +static int fakeBioCount; /* SSL_CTX_new */ static int ctxNewCallCount; @@ -158,37 +175,42 @@ void OpenSslFake_Reset(void) bioNewCallCount = 0; lastBioNewMethodArg = NULL; bioNewFails = false; - lastSetDataBioArg = NULL; - lastSetDataArg = NULL; - lastGetDataBioArg = NULL; - setBioCallCount = 0; - lastSetBioSslArg = NULL; - lastSetBioReadBioArg = NULL; - lastSetBioWriteBioArg = NULL; - lastSslCtrlSslArg = NULL; - lastSniHostname = NULL; - sniHostnameFails = false; - lastSet1HostSslArg = NULL; - lastSet1Host = NULL; - set1HostFails = false; - connectCallCount = 0; - lastConnectSslArg = NULL; - connectFails = false; - writeCallCount = 0; - lastWriteSslArg = NULL; - lastWriteBuf = NULL; - lastWriteSize = 0; - writeFails = false; - sslReadCallCount = 0; - lastSslReadSslArg = NULL; - lastSslReadBuf = NULL; - lastSslReadSize = 0; - shutdownCallCount = 0; - lastShutdownSslArg = NULL; - freeCallCount = 0; - lastFreeSslArg = NULL; - ctxFreeCallCount = 0; - lastCtxFreeCtxArg = NULL; + fakeBioCount = 0; + for (int i = 0; i < FAKE_BIO_POOL_SIZE; i++) + { + fakeBios[i].data = NULL; + } + lastSetDataBioArg = NULL; + lastSetDataArg = NULL; + lastGetDataBioArg = NULL; + setBioCallCount = 0; + lastSetBioSslArg = NULL; + lastSetBioReadBioArg = NULL; + lastSetBioWriteBioArg = NULL; + lastSslCtrlSslArg = NULL; + lastSniHostname = NULL; + sniHostnameFails = false; + lastSet1HostSslArg = NULL; + lastSet1Host = NULL; + set1HostFails = false; + connectCallCount = 0; + lastConnectSslArg = NULL; + connectFails = false; + writeCallCount = 0; + lastWriteSslArg = NULL; + lastWriteBuf = NULL; + lastWriteSize = 0; + writeFails = false; + sslReadCallCount = 0; + lastSslReadSslArg = NULL; + lastSslReadBuf = NULL; + lastSslReadSize = 0; + shutdownCallCount = 0; + lastShutdownSslArg = NULL; + freeCallCount = 0; + lastFreeSslArg = NULL; + ctxFreeCallCount = 0; + lastCtxFreeCtxArg = NULL; } /* ------------------------------------------------------------------------- @@ -307,7 +329,7 @@ const BIO_METHOD* OpenSslFake_LastBioNewMethodArg(void) BIO* OpenSslFake_LastBioReturned(void) { - return (BIO*) &fakeBioStorage; + return fakeBioCount > 0 ? (BIO*) &fakeBios[fakeBioCount - 1].slot : NULL; } BIO* OpenSslFake_LastSetDataBioArg(void) @@ -617,7 +639,13 @@ BIO* BIO_new(const BIO_METHOD* type) { bioNewCallCount++; lastBioNewMethodArg = type; - return bioNewFails ? NULL : (BIO*) &fakeBioStorage; + BIO* bio = NULL; + if (!bioNewFails && fakeBioCount < FAKE_BIO_POOL_SIZE) + { + bio = (BIO*) &fakeBios[fakeBioCount].slot; + fakeBioCount++; + } + return bio; } void OpenSslFake_SetBioNewFails(bool fails) @@ -629,12 +657,27 @@ void BIO_set_data(BIO* a, void* ptr) { lastSetDataBioArg = a; lastSetDataArg = ptr; + for (int i = 0; i < fakeBioCount; i++) + { + if ((BIO*) &fakeBios[i].slot == a) + { + fakeBios[i].data = ptr; + } + } } void* BIO_get_data(BIO* a) { lastGetDataBioArg = a; - return lastSetDataArg; + void* result = NULL; + for (int i = 0; i < fakeBioCount; i++) + { + if ((BIO*) &fakeBios[i].slot == a) + { + result = fakeBios[i].data; + } + } + return result; } void SSL_set_bio(SSL* ssl, BIO* rbio, BIO* wbio)