Skip to content

fix: S26.03 TlsStream Open unwinds inner transport and SSL state on failure#442

Merged
DavidCozens merged 9 commits into
mainfrom
fix/s26-03-openssl-open-unwind
May 24, 2026
Merged

fix: S26.03 TlsStream Open unwinds inner transport and SSL state on failure#442
DavidCozens merged 9 commits into
mainfrom
fix/s26-03-openssl-open-unwind

Conversation

@DavidCozens

@DavidCozens DavidCozens commented May 24, 2026

Copy link
Copy Markdown
Owner

Closes #441. OpenSSL parity of S26.02 (#420 / merged in PR #440), using the same test pattern and protocol-level error-message style.

Summary

  • TlsStream_Open now unwinds on every failure path after the first allocating step: a tail if (!ok) { TlsStream_Close(base); TlsStream_ReleaseSslContext(self); } closes the inner transport and releases the Ssl / BioMethod / Ctx allocations (each NULL-guarded so the unwind is safe regardless of which step tripped).
  • Five new typed error codes in SolidSyslogTlsStreamErrors.h (CONTEXT_INIT_FAILED, SESSION_INIT_FAILED, SERVER_NAME_NOT_SET, HANDSHAKE_REJECTED, HANDSHAKE_TIMEOUT) emitted via SolidSyslog_Error at severity ERROR. Messages are protocol-level — "TLS handshake rejected by peer or local verification; connection not established" rather than naming the OpenSSL call.
  • PerformHandshake's combined "not-retryable OR budget-exhausted" exit branch split into two else if arms so each emits its own typed code.
  • 15 existing per-failure-mode tests extended with a new CHECK_OPEN_UNWOUND_WITH_ERROR(transport, code) macro that pins the full unwind + error contract. 1 new recovery test (SecondOpenAfterFailedFirstOpenSucceeds).
  • Two cppcheck-misra suppressions for TlsStream.c shifted to their new line numbers (70→74 for 11.3, 16→20 for 5.7). No new suppressions; clean against the full CI include set.

Granularity choice (parity with S26.02)

OpenSSL has more configurable surfaces in SSL_CTX than mbedTLS does (SSL_CTX_load_verify_locations, SSL_CTX_use_certificate_chain_file, etc., all returning int). A literal one-code-per-failing-API expansion would have given 8-9 codes. Consolidated under CONTEXT_INIT_FAILED to match the MbedTls adapter's diagnostic granularity. Finer codes (TRUST_ANCHORS_NOT_LOADED, CLIENT_IDENTITY_INVALID, CIPHER_LIST_REJECTED) are a future enhancement story if integrators ask for the granularity.

Test plan

  • SolidSyslogTests aggregate (1294 tests) + OpenSslIntegrationTests (9 live OpenSSL tests) green under clang-debug
  • Same suites green under sanitize (ASan + UBSan), no leaks/UB
  • cppcheck-misra clean against full CI include set
  • clang-format clean on every touched C/C++ file
  • IWYU clean — every checked file reports "correct #includes/fwd-decls", zero actionable findings (run locally in clang container, same image CI uses)
  • fd-leak smoke check: 50 iterations of HandshakeRejectedWhenClientDoesNotTrustServerCert under ulimit -n 32 — all pass, no EMFILE
  • Full CI suite

Out of scope

  • Finer-grained CONTEXT_INIT codes (deferred — see above)
  • Defensive close-before-open in PosixTcpStream_Open (same argument as S26.02: contract is "every Stream_Open is preceded by Stream_Close", honoured by StreamSender's disconnect-before-reconnect)
  • Winsock/FreeRTOS TCP stream hardening (not reachable once OpenSSL unwinds correctly)
  • StreamSender reconnect flow changes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added detailed error codes for TLS stream failures, including context initialization, session setup, server name configuration, handshake rejection, and handshake timeout scenarios.
  • Bug Fixes

    • Improved resource cleanup when TLS operations fail, ensuring proper stream closure and state unwinding.
    • Enhanced error messages with specific codes at each failure point for better diagnostics.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@DavidCozens, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 1 review/hour. Refill in 51 minutes and 43 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63b5f904-6540-49dc-b6a1-2d286f3f0b91

📥 Commits

Reviewing files that changed from the base of the PR and between fec8b6a and 9b2437d.

📒 Files selected for processing (6)
  • DEVLOG.md
  • Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h
  • Platform/OpenSsl/Source/SolidSyslogTlsStream.c
  • Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c
  • Tests/SolidSyslogTlsStreamTest.cpp
  • misra_suppressions.txt
📝 Walkthrough

Walkthrough

This PR implements open-failure unwinding and per-failure-point error reporting for both MbedTLS and OpenSSL TLS stream implementations. When Open() fails, the stream now explicitly closes/frees resources and reports specific typed error codes for configuration failures and handshake outcomes.

Changes

TLS stream open unwinds and per-failure-point error codes

Layer / File(s) Summary
Error code enums and message mappings
Platform/MbedTls/Interface/SolidSyslogMbedTlsStreamErrors.h, Platform/OpenSsl/Interface/SolidSyslogTlsStreamErrors.h, Platform/MbedTls/Source/SolidSyslogMbedTlsStreamMessages.c, Platform/OpenSsl/Source/SolidSyslogTlsStreamMessages.c
Five new typed error codes defined for each implementation (defaults/context/session init, server name, handshake rejection/timeout) with error-to-string message mappings.
MbedTLS stream implementation with error reporting and unwinding
Platform/MbedTls/Source/SolidSyslogMbedTlsStream.c
MbedTlsStream_Open now explicitly closes on failure; configuration helpers and handshake emit specific typed errors via SolidSyslog_Error; handshake distinguishes rejection from timeout.
OpenSSL stream implementation with error reporting and unwinding
Platform/OpenSsl/Source/SolidSyslogTlsStream.c
TlsStream_Open now explicitly closes and frees context on failure; context/session/BIO init and hostname config emit typed errors; handshake distinguishes rejection from timeout.
MbedTLS test fakes for controlled return values
Tests/Support/MbedTlsFake.h, Tests/Support/MbedTlsFake.c
New setter functions allow tests to configure return values for mbedtls_ssl_config_defaults, mbedtls_ssl_setup, and mbedtls_ssl_set_hostname.
MbedTLS stream tests validating error unwinding and typed codes
Tests/MbedTls/SolidSyslogMbedTlsStreamTest.cpp
Added CHECK_OPEN_UNWOUND_WITH_ERROR macro; expanded ReCreateHandleWithUpdatedConfig to reset error handlers; new tests validate open failure cleanup and typed error reporting for configuration and handshake failures.
OpenSSL stream tests validating error unwinding and typed codes
Tests/SolidSyslogTlsStreamTest.cpp
Added CHECK_OPEN_UNWOUND_WITH_ERROR macro; new ReCreateStreamWithUpdatedConfig helper resets both fakes and error handlers; updated multiple failure-path tests to assert unwinding and specific error codes across context/session init, SNI config, and handshake rejection/timeout scenarios.
Documentation and configuration updates
DEVLOG.md, misra_suppressions.txt
DEVLOG entries document S26.02 and S26.03 work on both implementations, deferred follow-ups, and collateral fixes; MISRA suppression line numbers updated to reflect code movement.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • DavidCozens/solid-syslog#440 — Identical S26.02 MbedTLS changes: new error codes/messages, open unwinding/close-on-failure, handshake rejection vs timeout distinction, and extended fakes/tests.
  • DavidCozens/solid-syslog#283 — Both PRs modify the OpenSSL handshake retry loop to distinguish retryable vs non-retryable/timeout outcomes; main PR adds typed error codes and explicit unwinding.
  • DavidCozens/solid-syslog#183 — Both PRs refactor TlsStream_Open and handshake failure handling; main PR adds typed error codes and unwinding behavior at the same code points.

Poem

🐰 Unwinding streams with care so true,
Each failure point now has its clue,
Both MbedTLS and OpenSSL align,
Close and free when startup times decline,
Typed errors tell the story straight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: TlsStream Open now unwinds and cleans up inner transport and SSL state on failure, matching the PR's primary objective.
Description check ✅ Passed The description provides a comprehensive summary with Purpose (closes #441, OpenSSL parity), Change Description (unwind mechanism, new error codes, test coverage), Test Evidence (comprehensive test plan with multiple validation approaches), and Areas Affected (TlsStream module). All required template sections are adequately addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/s26-03-openssl-open-unwind

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Slice 1 of S26.03 (#441). Five new codes in TlsStreamErrors.h mirroring
the S26.02 mbedTLS pattern (CONTEXT_INIT_FAILED, SESSION_INIT_FAILED,
SERVER_NAME_NOT_SET, HANDSHAKE_REJECTED, HANDSHAKE_TIMEOUT) and
matching protocol-level messages in TlsStreamMessages.c. No emit sites
yet; subsequent slices add the per-helper emissions plus the Open
failure-tail unwind.
… fails

Slice 2 of S26.03 (#441). Mirrors S26.02's per-helper emit + tail
unwind pattern. When TlsStream_InitSslContext returns false (covers
SSL_CTX_new allocation failure plus every sub-step inside
ConfigureSslContext — trust anchors, client identity, protocol floor,
cipher list), InitSslContext emits TLSSTREAM_ERROR_CONTEXT_INIT_FAILED
at severity ERROR. TlsStream_Open's failure tail now calls
TlsStream_Close + TlsStream_ReleaseSslContext, releasing the inner
transport plus any Ssl / BioMethod / Ctx allocated before the failure.
Both helpers are NULL-guarded so the unwind is safe regardless of
which step tripped (matches the S26.02 idempotent-Close pattern).

Test side: introduces CHECK_OPEN_UNWOUND_WITH_ERROR(transport, code)
macro covering the 5 universal post-failure assertions (transport
closed once, ErrorHandler called once, source, code, severity). Per-
mode SSL-resource-free assertions stay in the existing XxxFailureFreesCtx
tests because their counts vary by failure depth. ErrorHandlerFake_Install
added to setup() so every test starts with a fresh handler — unaffected
tests don't observe it.

Subsequent slices (3-7) extend the remaining per-failure tests with the
macro and add the per-helper emissions for the other 4 codes.
… macro

Slice 3 of S26.03 (#441). Each of the 8 existing per-failure-mode
tests inside InitSslContext's scope (LoadVerifyLocations, MinProto,
CipherListRejected, OnlyClientCert, OnlyClientKey, UseCertChainFile,
UsePrivateKeyFile, CheckPrivateKey) now asserts the full unwind +
error contract via CHECK_OPEN_UNWOUND_WITH_ERROR. All pass on arrival
— slice 2's per-helper emit in InitSslContext + tail unwind in Open
covers every CONTEXT_INIT sub-failure uniformly.

Also added ReCreateStreamWithUpdatedConfig() helper to TEST_GROUP,
mirroring S26.02's MbedTls equivalent. Replaces inlined
Destroy/recreate-stream sequences in the six tests that need a
config tweak (CipherList, ClientCertChainPath, ClientKeyPath). The
helper also destroys and recreates the transport + resets OpenSslFake
+ reinstalls ErrorHandlerFake, so post-recreate count assertions
(== 1) observe only the new Open.
Slice 4 of S26.03 (#441). Both per-helper emit sites (InitSslSession's
SSL_new and AttachTransportBio's BIO_meth_new / BIO_new) now emit the
shared TLSSTREAM_ERROR_SESSION_INIT_FAILED code at severity ERROR.
The shared code reflects the protocol-level meaning ("TLS session
could not be initialised against the configured context") — the
integrator does not need to distinguish SSL allocation from BIO
allocation at this level. Slice 2's tail unwind already covers the
resource release on both paths.

Three existing tests extended with the macro to pin the contract:
OpenReturnsFalseWhenSslNewFails, WhenBioMethNewFails, WhenBioNewFails.
Slice 5 of S26.03 (#441). ConfigureExpectedHostname emits
TLSSTREAM_ERROR_SERVER_NAME_NOT_SET at severity ERROR when either
SSL_set_tlsext_host_name or SSL_set1_host fails. Emission stays
inside the ServerName != NULL guard — a caller who never sets
ServerName never reaches these calls, so no spurious error reported.
Slice 2's tail unwind already covers the resource release.

Two existing tests (WhenSet1HostFails, WhenSniHostnameSetupFails)
extended with the macro + switched to ReCreateStreamWithUpdatedConfig
for the ServerName injection.
…branch

Slice 6+7 of S26.03 (#441). Combined into one commit because the
production change is a single PerformHandshake refactor — splitting
the combined "not-retryable OR budget-exhausted" exit branch into two
distinct arms so each exit emits its own protocol-level code:

- Hard SSL error (non-WANT) → TLSSTREAM_ERROR_HANDSHAKE_REJECTED.
  Covers peer rejection, cert chain untrusted, server name mismatch,
  protocol-version incompatibility — every fatal handshake outcome
  the client sees as a single non-retryable rc.
- Budget exhausted (persistent WANT_READ/WRITE for HANDSHAKE_TIMEOUT_
  MILLISECONDS) → TLSSTREAM_ERROR_HANDSHAKE_TIMEOUT. Distinct from
  REJECTED so the integrator can tell a wedged peer from one that
  actively rejected.

Three tests extended with the macro:
- OpenReturnsFalseWhenHandshakeFails — pinned to HANDSHAKE_REJECTED
  (forced via SSL_get_error returning SSL_ERROR_SSL).
- OpenFailsWhenHandshakeNeverCompletes — pinned to HANDSHAKE_TIMEOUT
  (persistent WANT_READ).
- OpenFailsImmediatelyOnHardSslError — pinned to HANDSHAKE_REJECTED;
  keeps its existing once-only Connect and never-Sleep assertions.

Mirrors S26.02 slices 7+8.
Slice 8 of S26.03 (#441). Mirrors S26.02 slice 9. Pins the recovery
contract that the per-failure-point unwinds enable: after a failed
Open, the next Open is a clean Open-Close-Open cycle on the inner
transport. Without the unwind tail added in slice 2, the inner
transport would stay open and the next StreamSender reconnect tick
would re-enter PosixTcpStream_Open, clobbering the prior fd.

Arrangement uses OpenSslFake_SetConnectReturnSequence([fail, ok])
plus a SetGetErrorReturn flip to drive the first handshake to a hard
error (fail-fast) and the second to success. Asserts Transport.OpenCount
== 2, Transport.CloseCount == 1, second Open returns true.

Passed on arrival — no production change.
Slice 9 of S26.03 (#441). TlsStream.c grew by 24 lines in slices
2-7 (per-helper emits in InitSslContext, InitSslSession,
AttachTransportBio, ConfigureExpectedHostname, two PerformHandshake
exit branches; unwind tail in Open). Two existing suppressions for
the file moved to new line numbers:

  70 -> 74  (11.3, SelfFromBase cast)
  16 -> 20  (5.7, anonymous-enum opener)

No new suppressions; no new MISRA violations introduced by the
unwind or the emission helpers. Verified with cppcheck-misra
against the full CI include set — clean exit, matches main baseline.
@DavidCozens DavidCozens force-pushed the fix/s26-03-openssl-open-unwind branch from fec8b6a to 9b2437d Compare May 24, 2026 15:06
@github-actions

Copy link
Copy Markdown
Contributor

☀️   Quality Summary

   🚦   build-linux-gcc: 100% successful (✔️ 1301 passed, 🙈 2 skipped)
   🚦   build-freertos-host-tdd: 100% successful (✔️ 1529 passed, 🙈 2 skipped)
   🚦   build-linux-clang: 100% successful (✔️ 1253 passed, 🙈 2 skipped)
   🚦   sanitize-linux-gcc: 100% successful (✔️ 1253 passed, 🙈 2 skipped)
   🚦   integration-linux-openssl: 100% successful (✔️ 9 passed)
   🚦   integration-linux-mbedtls: 100% successful (✔️ 7 passed)
   🚦   integration-windows-openssl: 100% successful (✔️ 9 passed)
   🚦   bdd-linux-syslog-ng: 94% successful (✔️ 46 passed, 🙈 3 skipped)
   🚦   bdd-windows-otel: 90% successful (✔️ 44 passed, 🙈 5 skipped)
   🚦   bdd-freertos-qemu: 86% successful (✔️ 42 passed, 🙈 7 skipped)
   🚦   build-windows-msvc: 100% successful (✔️ 1137 passed, 🙈 1 skipped)
   🚦   build-linux-tunable-override: 100% successful (✔️ 1253 passed, 🙈 2 skipped)
   ⚠️   Clang-Tidy: No warnings
   ⚠️   CPPCheck: 1 warning (low: 1)


Created by Quality Monitor v1.14.0 (#f3859fd). More details are shown in the GitHub Checks Result.

@DavidCozens DavidCozens merged commit 358bd22 into main May 24, 2026
21 checks passed
@DavidCozens DavidCozens deleted the fix/s26-03-openssl-open-unwind branch May 24, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S26.03: TlsStream Open() unwinds inner transport and SSL state on failure

1 participant