Fix TLS correctness bugs - #2
carpentry-agent[bot] wants to merge 3 commits into
Conversation
…chain - read/read-bytes: surface SSL_read errors as Result.Error instead of silently returning empty data. read now uses an output parameter, read-bytes reuses read-append with an empty buffer. - TlsStream copy: add SSL_up_ref and dup(fd) to prevent double-free and double-close when a copied stream is closed. - accept: close the fd when SSL_new fails, preventing fd leak on accept failure. - TlsServerCtx: use SSL_CTX_use_certificate_chain_file instead of SSL_CTX_use_certificate_file to preserve intermediate certificates.
There was a problem hiding this comment.
Build & Tests
Build: pass (local Carp compiler). Tests: hang locally (pre-existing loopback issue). CI: FAIL on both macOS and Ubuntu — exit code -13 (SIGPIPE kills the test binary).
Findings
1. CI failure: SIGPIPE (blocking)
The test binary is killed by SIGPIPE during SSL_shutdown when the remote peer has already closed the connection. The intermediate commit 6fa90c5 correctly added signal(SIGPIPE, SIG_IGN) to TlsStream_init_, but the final commit 83dce2d reverted it. This is the direct cause of the CI failure — it must be restored.
2. Accept fd leak on SSL_accept failure (tls_stream.h:278-280)
The comment /* BIO_CLOSE closes fd */ is incorrect. SSL_set_fd creates a BIO with BIO_NOCLOSE, so SSL_free(ssl) does not close the fd. This leaks the fd on SSL_accept failure. The intermediate commit 8293719 correctly added close(fd) here, but 83dce2d reverted it. Fix: SSL_free(ssl); close(fd); and remove the misleading comment.
3. Final commit reverts valuable intermediate improvements
The commit history shows 7 incremental improvements (7003256–6fa90c5) that were then substantially reverted by 83dce2d. Several of these are worth keeping:
- TLS hardening (
e01dda5): Disabling renegotiation (SSL_OP_NO_RENEGOTIATION) and compression (SSL_OP_NO_COMPRESSION) is security best practice (CRIME/BREACH, client-initiated renegotiation DoS). - Detailed error capture (
7003256):carp_tls_capture_ssl_errorproperly usedSSL_get_error, distinguishedSSL_ERROR_SYSCALLwith/withouterrno, detected unexpected EOF, and drained the error queue withERR_clear_error(). The replacementcarp_tls_error_stringonly reads one error and doesn't drain the queue — stale errors can leak into later calls. - INT_MAX clamping for SSL_write (
e18f9c8):SSL_writetakes anintlength. Thecarp_tls_write_chunkhelper prevented overflow on>2GBsends. The replacement(int)(len - sent)wraps to a negative value on large data. - size_t safety in read_append_ (
0cf1c14): Usingsize_t new_capwith a checkedCARP_REALLOCprevented integer overflow and NULL-deref. The replacement usesint new_capwhich can overflow, and doesn't check the realloc return. - SSL_set1_host failure check (
e18f9c8): IfSSL_set1_hostfails (e.g. OOM), hostname verification silently doesn't activate — fail-open. The old hardened version bailed out.
4. What the PR gets right
The core fixes are sound and should be kept:
read_/read-byteserror propagation (previously alwaysResult.Success) — real bug fixTlsStream_copywithSSL_up_ref+dup(fd)— correct double-free fixaccept_close(fd)onSSL_newfailure — real fd leak fixSSL_CTX_use_certificate_chain_file— correct for intermediate certs
Verdict: revise
The CI failure is blocking. Beyond that, the final commit inadvertently rolled back most of the defensive improvements that the earlier commits correctly introduced. I'd recommend either reverting 83dce2d and building on top of 6fa90c5 (which had all fixes in place), or cherry-picking the reverted improvements back in.
Addresses reviewer feedback on PR #2. The final commit (83dce2d) inadvertently reverted most of the safety improvements from the intermediate commits. This restores them while keeping the API improvements (output-param read, certificate_chain_file, copy with SSL_up_ref + dup): - SIGPIPE: restore signal(SIGPIPE, SIG_IGN) in init (fixes CI kill) - Error capture: restore carp_tls_capture_ssl_error with SSL_get_error, errno distinction, unexpected EOF detection, and ERR_clear_error() - fd leak: close(fd) on SSL_accept failure (SSL_set_fd uses BIO_NOCLOSE) - TLS hardening: restore SSL_OP_NO_RENEGOTIATION + SSL_OP_NO_COMPRESSION - INT_MAX clamping: restore carp_tls_write_chunk for SSL_write safety - size_t overflow: restore size_t arithmetic in read_append_ with checked realloc - SSL_set1_host: restore failure check to prevent fail-open on OOM - Detailed error messages: restore carp_tls_set_error at every failure site (getaddrinfo, SSL_new, SSL_connect, server context creation)
|
Addressed all findings from @carpentry-reviewer's review: 1. SIGPIPE (blocking) — Restored 2. Accept fd leak on SSL_accept failure — Restored 3. Reverted intermediate improvements — All restored:
All of these are merged with the API improvements from the current branch (output-param All 16 tests pass locally. |
There was a problem hiding this comment.
Build & Tests
Build: pass. CI: green on both macOS and Ubuntu (SIGPIPE issue from round 1 is resolved).
Prior feedback
All six issues from round 1 are addressed. The merge base is now 6fa90c5 (tip of main), which already contains SIGPIPE handling, accept fd leak fix, TLS hardening, detailed error capture, INT_MAX clamping, size_t safety, and SSL_set1_host check. The net diff against main is limited to four changes: read_ signature, read-bytes removal, TlsStream_copy, and certificate_chain_file.
Findings
1. TlsStream_copy dup(fd) + SSL_up_ref is broken (blocking)
tls_stream.h:271-278 — SSL_up_ref increments the refcount on the same SSL object. The SSL's internal BIO (set up by SSL_set_fd at lines 139/375) still references the original fd, not the dup'd one. This means:
- Both copies' SSL reads/writes go through the original fd.
- If the original is closed first (
close(s.fd)at line 250), the copy's SSL operations hit a closed fd (EBADF). - The dup'd fd in the copy is never used by SSL — it's orphaned.
set-timeout(tls_stream.h:265-268) callssetsockoptons->fd, which on a copy targets the dup'd fd, not the fd SSL actually uses. Timeouts set on a copy have no effect.
The old shallow copy was dangerous (close-exactly-one-or-double-free) but at least both copies operated on the same fd consistently. The new code breaks that — it's not a valid fix for double-free because SSL doesn't know about the new fd.
A correct fix would either: (a) create a wholly new SSL object with SSL_new + SSL_set_fd(dup_fd) + session reuse, or (b) revert to the old shallow-copy semantics with the "close exactly one" contract and a clear doc comment. Option (b) is simpler and matches what TcpStream does in the sockets library.
2. Removed comments in read_append_ (non-blocking)
Two explanatory comments were removed from the read_append_ realloc path: the size_t overflow rationale and the realloc-leaves-original-valid note. These explain why the code is written this way and would help future readers. Minor loss.
3. read_ and read-bytes changes look correct
The output-parameter read_ signature and the read-bytes reuse of read-append- with an empty array are both sound. CARP_FREE(*out) is safe because the Carp wrapper always passes @"" (a valid heap allocation). CARP_REALLOC(NULL, size) from Array.allocate 0 is guaranteed to act as malloc per the C standard.
4. certificate_chain_file change is correct
Switching from SSL_CTX_use_certificate_file to SSL_CTX_use_certificate_chain_file correctly loads intermediate certificates. Good fix.
Verdict: revise
The TlsStream_copy change is the main safety fix in this PR and it's fundamentally broken — the SSL BIO still references the original fd after dup. Either revert to shallow-copy-with-close-one semantics, or do proper SSL object duplication with SSL_new + SSL_set_fd on the dup'd fd. The other three changes (read_ signature, read-bytes, certificate_chain_file) are fine and should be kept.
… comments SSL_up_ref + dup(fd) is broken: the SSL BIO still references the original fd, so the dup'd fd is orphaned and reads/writes on a copy hit EBADF if the original is closed first. Revert to a plain struct copy matching TcpStream's close-exactly-one contract. Also restores the two explanatory comments in read_append_ (size_t overflow rationale and realloc-leaves-original-valid note).
|
Addressed the two findings from @carpentry-reviewer's round 2 review: 1. 2. Removed comments in The remaining changes ( Compiles cleanly. Formatting and linting pass. |
There was a problem hiding this comment.
Build & Tests
Build: pass (type-check and C compilation both succeed). Tests: all 16 pass locally, including the loopback echo tests that were previously reported as hanging. CI: green on both macOS and Ubuntu.
Prior feedback
Both rounds of feedback have been addressed:
- Round 1 (SIGPIPE, accept fd leak, reverted improvements): all resolved — the merge base now includes the hardened intermediate commits (SIGPIPE handler, TLS hardening, detailed error capture, INT_MAX clamping, size_t safety, SSL_set1_host check).
- Round 2 (
TlsStream_copybrokendup(fd)+SSL_up_ref, removed comments): both addressed. Copy reverted to shallow semantics with a clear doc comment; explanatory comments inread_append_restored.
Findings
No new issues. The net diff against main is clean and focused on four changes:
-
read_output-parameter signature (tls_stream.h:206-216): Correct.CARP_FREE(*out)safely frees the@""heap allocation from the Carp side,CARP_MALLOC(TLS_BUF_SIZE + 1)replaces it, and the caller retains ownership. Error path sets(*out)[0] = '\0'so the caller always gets a valid string. -
read-bytesreusesread-append-(tls.carp:92-94): Correct.Array.allocate 0creates a valid (zero-capacity) array;read_MINUS_append_handles the growth viaCARP_REALLOC(NULL, size)which acts asmallocper the C standard. -
TlsStream_copyshallow copy (tls_stream.h:274-280): The right call. MatchesTcpStream's close-exactly-one contract from the sockets library. Doc comment ontls.carp:113-118makes the non-owning-alias semantics explicit. -
SSL_CTX_use_certificate_chain_file(tls_stream.h:317): Correct — loads the full certificate chain including intermediates.
Verdict: merge
All prior feedback addressed, CI green, full test suite passes. The four remaining changes are individually correct and well-motivated.
hellerve
left a comment
There was a problem hiding this comment.
this pr is now pure churn. the only thing of value is the chain file.
|
Closing this in favour of a focused PR with just the |
Summary
Fixes four correctness and safety bugs in the TLS library:
read/read-bytessilently swallow SSL errors: Previously, both functions always returnedResult.Success, even whenSSL_readfailed. Now they check the return value and surface errors asResult.Error(matchingread-append's existing behaviour).readuses an output-parameter C function;read-bytesreusesread-append-with an empty buffer.TlsStreamcopy causes double-free:TlsStream_copycopied theSSL*andfdwithout incrementing reference counts. When either copy was closed, the SSL object was freed and the fd closed, leaving the other copy dangling. Fixed by addingSSL_up_refanddup(fd).acceptleaks fd onSSL_newfailure: IfSSL_newfailed insideTlsStream_accept_, the passed-in fd was never closed. TheSSL_acceptfailure path was already handled (SSL_free with BIO_CLOSE closes the fd), but theSSL_newfailure path leaked. Added an explicitclose(fd).certificate_file→certificate_chain_file:SSL_CTX_use_certificate_fileonly loads a single certificate. Switched toSSL_CTX_use_certificate_chain_fileto preserve intermediate certificates in the chain, which is necessary for proper TLS handshake with most real-world certificate setups.Notes
main) related to the loopback echo server tests on this platform. The library itself compiles and works correctly — verified with a standalone test that exercisesread,read-bytes,connect, andsend.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.