You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Repository state: HEAD matched the requested commit; worktree was clean before and after review.
Model: gpt-5.6-sol
Reasoning effort: max
Mode: bounded, defensive, read-only; no files changed, issues filed, subagents used, or other Codex process invoked.
Scope: the requested TCP/TLS record files, StreamTransport, Labeled/Passthrough, record-boundary portions of streams/bridge.rs, focused tests, Cargo.lock, and only the locked rustls 0.23.40 behavior needed to validate candidates.
The principal defect is that the shared bridge treats unauthenticated TCP EOF as equivalent to authenticated TLS close_notify. Once a complete reliable unit has decoded, an EOF without close_notify is therefore reported and committed as a clean exchange rather than TLS truncation.
Separately, a labeled TLS peer that completes TLS and sends close_notify before its label leaves the bridge in an irrecoverable Handshaking state until its deadline, even though rustls will accept no more data.
Fragmentation/coalescing, bounded TLS intake, label gating, whole-unit outbound admission, close ordering, and failure-side output suppression otherwise held under the scoped tests. Neither finding overlaps the reviewed #157–#167 reports.
State and ownership model
Layer
Inbound ownership
Outbound ownership
State/termination
StreamTransport
Caller retains the slice; Done claims all bytes consumed, Pending(n) transfers exactly the prefix
Implementation appends complete transport bytes to caller’s Vec
Failed is terminal only because the bridge terminalizes it
Passthrough
Copies every input byte into inbound, then drains it verbatim
outbound: Vec<u8>; bounded only by the bridge’s one-unit cadence
No handshake or in-band close
Labeled<R>
LabelGate owns partial label bytes; post_label_tail owns plaintext coalesced after the label
A maximum 255-byte label frame is staged into R before application plaintext
Gate validating → validated/rejected; inner handshake is composed with the gate
TlsRecords
rustls owns ciphertext deframing and up to 16 KiB received plaintext; peer_closed is sticky
pending owns whole application plaintext up to max(max_frame + max_frame/16 + 16 KiB, 64 KiB); rustls owns handshake/application/alert ciphertext behind a 2× backstop
TLS handshake → established; peer_closed records close_notify; close_requested defers local close until pending drains
StreamBridge
pending_inbound owns unconsumed ciphertext; recv_accum owns a partial length-delimited reliable unit; pending_eof carries pre-promotion FIN
One encoded reliable unit is admitted to the record layer; fin_sent owns the local close obligation
Handshaking → Established(Active) → {SendClosed, RecvClosed} → BothClosed; failures should be terminal
The locked dependency is rustls 0.23.40: Cargo.lock#L3609-L3612. Its Reader distinguishes TCP EOF without close_notify as UnexpectedEof, but only after EOF has been reported through read_tls.
Violated invariants
Invariant 3: TLS truncation is mistaken for clean completion.
Invariant 6: an unauthenticated transport FIN can authorize buffered application side effects.
Minimal reachable trace
Complete TLS authentication and label validation; promote an inbound bridge.
Receive authenticated ciphertext containing exactly one complete reliable unit. It decodes successfully and its endpoint event remains queued pending close proof.
The peer omits close_notify and closes the TCP write half. The bridge receives handle_transport_data(&[]).
TlsRecords does not tell rustls about EOF and returns Done; peer_has_closed() remains false.
The bridge nevertheless computes fin_seen = true || false, calls Stream::handle_data(&[]), and transitions to RecvClosed.
drain_payload_only commits the queued event. Subsequent local close can produce BothClosed and a clean lifecycle notification.
The same result is reachable when the final TLS/application bytes arrive before promotion and the following TCP EOF is stored in pending_eof.
Impact
A peer closing TLS without close_notify, or an on-path actor capable of truncating the TCP stream, can make a complete TLS plaintext prefix appear cleanly finalized. Already-decoded membership/user events may be committed and the exchange reported as closed rather than errored. Partial reliable units still fail, so the impact is confined to truncation at a complete-unit boundary; no plaintext forgery was established.
Root cause
The bridge’s transport-neutral close expression conflates two different proofs:
raw TCP’s out-of-band FIN, which is its normal close signal;
TLS’s authenticated close_notify, for which bare TCP EOF is truncation.
The trait exposes is_secure() and peer_has_closed(), but has no explicit policy describing whether transport EOF is a valid clean close.
Remediation
Add an explicit transport capability such as requires_authenticated_close() or a close outcome returned by intake.
For TLS, treat eof && !peer_has_closed() as ConnectionLost/Decode, both pre- and post-promotion. Accept later TCP EOF only if close_notify was already latched.
Preserve TCP behavior, where EOF remains the clean close anchor.
Stop collapsing UnexpectedEof with WouldBlock if EOF is routed through rustls in a future API.
Regression tests
Complete TLS handshake and label validation, feed one complete reliable unit without close_notify, then feed TCP EOF; assert Failed, no committed payload event, and StreamErrored.
Repeat with the complete unit buffered pre-promotion followed by EOF.
Invariant 2: a close transition is not immediately terminal.
Invariant 3: authenticated TLS closure is not classified as incomplete label failure.
Invariant 5: a state that can no longer make progress remains allocated until timeout.
Minimal reachable trace
Construct an acceptor Labeled<TlsRecords> with a required label.
Complete the TLS handshake without sending application plaintext. The inner TLS handshake is complete, but the label gate keeps Labeled handshaking.
Send an authenticated close_notify and keep the TCP connection open.
rustls latches the close. consume_inbound_label finds no plaintext, so the gate remains validating and Labeled::handle_transport_data returns Done.
intake_handshaking observes “Done but still handshaking” and returns successfully.
rustls will not read further TLS bytes after close_notify; nevertheless the bridge remains live until its handshake deadline.
Impact
An already TLS-authenticated peer can retain an accept-side bridge and its connection until the configured handshake deadline after explicitly closing TLS. This delays teardown and failure reporting. The resource lifetime is bounded, no Stream is minted, and no local label or application plaintext is exposed; a silent peer could already consume the same timeout, supporting Low severity.
Root cause
The in-band close latch is consulted only by established intake. Composing TLS handshaking with label validation hides the fact that the inner record stream has terminated before the outer gate can succeed.
Remediation
After every pre-stream intake, if records.peer_has_closed() is true while records.is_handshaking() remains true, fail the bridge immediately without promotion or outbound label emission. A stronger design would let Intake carry an explicit Closed { consumed } outcome.
Regression tests
Required-label TLS acceptor: complete TLS only, send close_notify, withhold TCP FIN; assert immediate terminal failure, no Stream, no outbound label, and no ciphertext.
Repeat after a partial fragmented label.
Positive control: a complete matching label and reliable unit coalesced with close_notify still promotes, drains, and closes normally.
Architecture and hardening observations
Intake::Done promises full consumption, but rustls returns Ok(0) after receiving close_notify; a subsequent nonempty slice can therefore be reported Done without being read. Model intake as { consumed, status }, with explicit Open, Backpressured, Closed, and Failed statuses.
Labeled::flush_outbound_label_into_inner ignores write_plaintext’s whole-unit admission result and marks the prefix emitted. This is safe for the two current inners because labels are at most 255 bytes and TLS has a 64 KiB floor, but it violates the generic StreamTransport contract. Propagate failure or make infallible prefix admission a documented bound.
TlsRecords maps every read_tls error to backpressure. Locked rustls behavior plus per-chunk process_new_packets rejected the concrete malformed-record candidates before this became unsafe, so no bug was retained. Matching only the backpressure error kind would make the contract resilient to dependency changes.
Passthrough’s Vec buffers have no intrinsic cap; boundedness depends on the bridge’s current one-unit-per-pump cadence. A local one-unit limit would make ownership explicit rather than architectural.
Validation
All checks were non-mutating.
git rev-parse HEAD → aeef956bc0bdf0ed21ba386b329dab6aae64b47e
git status --porcelain=v1 --untracked-files=all → empty before and after
Coalesced ciphertext above rustls’s 16 KiB plaintext limit does not duplicate or lose bytes: consumed offsets, interleaved drains, retained tails, and large-frame tests held.
Fragmented labels and reliable units preserve order; the gate owns partial headers and post_label_tail precedes later inner plaintext.
Outbound TLS admission is whole-unit: overflow rejects without partial staging; transmit re-feeds short rustls writes until pending empties.
close_notify does not overtake admitted application bytes: it remains deferred until staged plaintext drains.
No application plaintext was found to surface before TLS handshake completion or label validation.
Failed raw exchanges clear labels/application bytes; failed TLS reaps suppress rustls-owned close ciphertext. The focused regression passed, so the known cross-driver output finding was not restated.
Capacity headroom covers the current maximum label, reliable-unit length prefix, TLS framing, and non-inflating compression path. No silent whole-unit drop survived.
Runtime compression/encryption update concerns did not yield a scoped incompatible-epoch byte mix and were not broadened into endpoint-policy review.
The rustls deframer-error-as-backpressure hypothesis was narrowed to hardening: under locked 0.23.40, malformed record sizes reach process_new_packets failure in the reviewed feed cadence.
Limitations
This was a source and focused-test review, not live-socket fault injection. No regression tests were added, so the two failing traces are established from exact control flow and locked rustls semantics rather than newly executed negative tests. Only the ring/lz4/AES-GCM focused feature combination was run; alternate crypto providers share the reviewed rustls connection state machine but were not separately exercised. All requested driver, scheduling, endpoint-wide, certificate-policy, QUIC, DNS, and wire-decoder areas remained excluded.
[Adversarial review] TLS EOF can authorize truncated exchanges; pre-label
close_notifyis non-terminalReview metadata
aeef956bc0bdf0ed21ba386b329dab6aae64b47egpt-5.6-solmaxStreamTransport,Labeled/Passthrough, record-boundary portions ofstreams/bridge.rs, focused tests,Cargo.lock, and only the locked rustls0.23.40behavior needed to validate candidates.close_timeout, peer attribution, cross-driver output/terminalization, embedded socket teardown, and broad endpoint findings.Executive summary
Two findings survived adversarial falsification:
The principal defect is that the shared bridge treats unauthenticated TCP EOF as equivalent to authenticated TLS
close_notify. Once a complete reliable unit has decoded, an EOF withoutclose_notifyis therefore reported and committed as a clean exchange rather than TLS truncation.Separately, a labeled TLS peer that completes TLS and sends
close_notifybefore its label leaves the bridge in an irrecoverableHandshakingstate until its deadline, even though rustls will accept no more data.Fragmentation/coalescing, bounded TLS intake, label gating, whole-unit outbound admission, close ordering, and failure-side output suppression otherwise held under the scoped tests. Neither finding overlaps the reviewed #157–#167 reports.
State and ownership model
StreamTransportDoneclaims all bytes consumed,Pending(n)transfers exactly the prefixVecFailedis terminal only because the bridge terminalizes itPassthroughinbound, then drains it verbatimoutbound: Vec<u8>; bounded only by the bridge’s one-unit cadenceLabeled<R>LabelGateowns partial label bytes;post_label_tailowns plaintext coalesced after the labelRbefore application plaintextTlsRecordspeer_closedis stickypendingowns whole application plaintext up tomax(max_frame + max_frame/16 + 16 KiB, 64 KiB); rustls owns handshake/application/alert ciphertext behind a 2× backstoppeer_closedrecordsclose_notify;close_requesteddefers local close untilpendingdrainsStreamBridgepending_inboundowns unconsumed ciphertext;recv_accumowns a partial length-delimited reliable unit;pending_eofcarries pre-promotion FINfin_sentowns the local close obligationHandshaking → Established(Active) → {SendClosed, RecvClosed} → BothClosed; failures should be terminalF1 — TCP EOF is accepted as clean TLS completion
Severity: Medium (P2)
Confidence: High
Evidence
read_tlsand merely callsprocess_new_packets:tls/records/mod.rs#L255-L295.UnexpectedEof—are collapsed into “no plaintext”:tls/records/mod.rs#L123-L184.streams/bridge.rs#L1022-L1036.fin_seen = eof || records.peer_has_closed():streams/bridge.rs#L1153-L1196.RecvClosedauthorizes queued payload events:streams/bridge.rs#L1353-L1379.0.23.40:Cargo.lock#L3609-L3612. ItsReaderdistinguishes TCP EOF withoutclose_notifyasUnexpectedEof, but only after EOF has been reported throughread_tls.Violated invariants
Minimal reachable trace
close_notifyand closes the TCP write half. The bridge receiveshandle_transport_data(&[]).TlsRecordsdoes not tell rustls about EOF and returnsDone;peer_has_closed()remains false.fin_seen = true || false, callsStream::handle_data(&[]), and transitions toRecvClosed.drain_payload_onlycommits the queued event. Subsequent local close can produceBothClosedand a clean lifecycle notification.The same result is reachable when the final TLS/application bytes arrive before promotion and the following TCP EOF is stored in
pending_eof.Impact
A peer closing TLS without
close_notify, or an on-path actor capable of truncating the TCP stream, can make a complete TLS plaintext prefix appear cleanly finalized. Already-decoded membership/user events may be committed and the exchange reported as closed rather than errored. Partial reliable units still fail, so the impact is confined to truncation at a complete-unit boundary; no plaintext forgery was established.Root cause
The bridge’s transport-neutral close expression conflates two different proofs:
close_notify, for which bare TCP EOF is truncation.The trait exposes
is_secure()andpeer_has_closed(), but has no explicit policy describing whether transport EOF is a valid clean close.Remediation
requires_authenticated_close()or a close outcome returned by intake.eof && !peer_has_closed()asConnectionLost/Decode, both pre- and post-promotion. Accept later TCP EOF only ifclose_notifywas already latched.UnexpectedEofwithWouldBlockif EOF is routed through rustls in a future API.Regression tests
close_notify, then feed TCP EOF; assertFailed, no committed payload event, andStreamErrored.close_notify+ TCP EOF remains clean.F2 —
close_notifybefore label completion leaves an impossible handshake liveSeverity: Low (P3)
Confidence: High
Evidence
peer_closedafter processingclose_notify:tls/records/mod.rs#L255-L295and#L426-L428.Labeled<R>returns the inner intake unchanged and remains handshaking while either the gate or inner is handshaking:streams/labeled/mod.rs#L479-L507.peer_has_closed:streams/bridge.rs#L389-L433.streams/bridge.rs#L712-L725.Violated invariants
Minimal reachable trace
Labeled<TlsRecords>with a required label.Labeledhandshaking.close_notifyand keep the TCP connection open.consume_inbound_labelfinds no plaintext, so the gate remains validating andLabeled::handle_transport_datareturnsDone.intake_handshakingobserves “Done but still handshaking” and returns successfully.close_notify; nevertheless the bridge remains live until its handshake deadline.Impact
An already TLS-authenticated peer can retain an accept-side bridge and its connection until the configured handshake deadline after explicitly closing TLS. This delays teardown and failure reporting. The resource lifetime is bounded, no
Streamis minted, and no local label or application plaintext is exposed; a silent peer could already consume the same timeout, supporting Low severity.Root cause
The in-band close latch is consulted only by established intake. Composing TLS handshaking with label validation hides the fact that the inner record stream has terminated before the outer gate can succeed.
Remediation
After every pre-stream intake, if
records.peer_has_closed()is true whilerecords.is_handshaking()remains true, fail the bridge immediately without promotion or outbound label emission. A stronger design would letIntakecarry an explicitClosed { consumed }outcome.Regression tests
close_notify, withhold TCP FIN; assert immediate terminal failure, noStream, no outbound label, and no ciphertext.close_notifystill promotes, drains, and closes normally.Architecture and hardening observations
Intake::Donepromises full consumption, but rustls returnsOk(0)after receivingclose_notify; a subsequent nonempty slice can therefore be reportedDonewithout being read. Model intake as{ consumed, status }, with explicitOpen,Backpressured,Closed, andFailedstatuses.Labeled::flush_outbound_label_into_innerignoreswrite_plaintext’s whole-unit admission result and marks the prefix emitted. This is safe for the two current inners because labels are at most 255 bytes and TLS has a 64 KiB floor, but it violates the genericStreamTransportcontract. Propagate failure or make infallible prefix admission a documented bound.TlsRecordsmaps everyread_tlserror to backpressure. Locked rustls behavior plus per-chunkprocess_new_packetsrejected the concrete malformed-record candidates before this became unsafe, so no bug was retained. Matching only the backpressure error kind would make the contract resilient to dependency changes.Passthrough’sVecbuffers have no intrinsic cap; boundedness depends on the bridge’s current one-unit-per-pump cadence. A local one-unit limit would make ownership explicit rather than architectural.Validation
All checks were non-mutating.
git rev-parse HEAD→aeef956bc0bdf0ed21ba386b329dab6aae64b47egit status --porcelain=v1 --untracked-files=all→ empty before and aftercargo test -p memberlist-proto --features 'tcp,tls-rustls-ring,lz4,aes-gcm' tls::records::tests:: -- --test-threads=1→ 11 passedstreams::labeled::tests::→ 23 passedtls::bridge::→ 12 passedtcp::bridge::→ 26 passedtls::tests::failed_established_tls_exchange_no_close_notify_before_abort→ 1 passed0.23.40source was checked forread_tls,Reader::UnexpectedEof,process_new_packets, close latching, and buffer-limit behavior.Rejected or narrowed candidates
post_label_tailprecedes later inner plaintext.pendingempties.close_notifydoes not overtake admitted application bytes: it remains deferred until staged plaintext drains.0.23.40, malformed record sizes reachprocess_new_packetsfailure in the reviewed feed cadence.Limitations
This was a source and focused-test review, not live-socket fault injection. No regression tests were added, so the two failing traces are established from exact control flow and locked rustls semantics rather than newly executed negative tests. Only the ring/lz4/AES-GCM focused feature combination was run; alternate crypto providers share the reviewed rustls connection state machine but were not separately exercised. All requested driver, scheduling, endpoint-wide, certificate-policy, QUIC, DNS, and wire-decoder areas remained excluded.