Skip to content

fix: reset client transport state after 0-RTT rejection - #2770

Open
namtran1812 wants to merge 3 commits into
cloudflare:masterfrom
namtran1812:feature/zero-rtt-rejection
Open

namtran1812 wants to merge 3 commits into
cloudflare:masterfrom
namtran1812:feature/zero-rtt-rejection

Conversation

@namtran1812

@namtran1812 namtran1812 commented Sep 29, 2026 •

Copy link
Copy Markdown

Summary

When TLS rejects client 0-RTT, early stream state remains allocated and can prevent the application from replaying requests correctly.

Expose the TLS rejection signal, reset early stream and connection byte accounting, and discard Application recovery state while preserving packet numbers. Update the example client to recreate its HTTP session after rejection.

Related to #2676, #2687, and #2677.

Regression coverage

Both rejection regressions run under cubic and bbr2_gcongestion. The server rejects early data without receiving the early application packets.

  • Transport regression: verifies early streams and outstanding Application recovery packets are cleared, connection byte accounting resets, packet numbers are preserved and advance during replay, and the same stream ID delivers replayed data from offset zero.
  • HTTP/3 regression: creates an early HTTP/3 session and request, recreates the session after rejection, reuses the request stream ID, and verifies request headers at the server and response headers at the client.
  • Existing accepted 0-RTT tests remain passing.

Validation

At cd2fd69, on macOS ARM:

  • cargo test -p quiche --lib — 1,144 passed.
  • cargo clippy -p quiche -p quiche_apps --all-targets -- -D warnings — passed.
  • cargo +nightly fmt --all --check — passed.
  • git diff --check — passed.
  • cargo test -p quiche_apps — builds successfully; its test targets contain no tests.

Review focus

Please review the transport rollback and recovery semantics and the application-facing rejection signal. The HTTP/3 regression covers session recreation over the transport; it does not directly exercise the example client's socket event loop.

@namtran1812
namtran1812 marked this pull request as ready for review September 29, 2026 19:20
@namtran1812
namtran1812 requested a review from a team as a code owner September 29, 2026 19:20
@ghedo ghedo added type: bugfix Corrects defective behavior. area: tls Changes related to tls. area: streams Changes related to streams. labels Sep 30, 2026

Copy link
Copy Markdown

I compared this against a later downstream rollback pass and found three pieces of 0-RTT-derived send state that still survive this PR's rejection reset:

  • blocked_limit
  • streams_blocked_bidi_state / streams_blocked_uni_state
  • dgram_send_queue

The first three fields can suppress/regenerate DATA_BLOCKED / STREAMS_BLOCKED based on remembered 0-RTT transport parameters after the server's negotiated parameters have taken over. The DATAGRAM queue is application data queued under the rejected early-data/application configuration and should not silently carry through into 1-RTT after the application is told to rebuild its early state.

Our downstream rejection path clears those alongside stream/recovery/byte state:

self.blocked_limit = None;
self.streams_blocked_bidi_state = None;
self.streams_blocked_uni_state = None;
self.dgram_send_queue.clear();

I added a focused regression, rejected_zero_rtt_clears_auxiliary_send_state, which primes all four before rejection and asserts they are empty afterwards. On current downstream based on upstream 3fc9bc1c, Rust 1.98.1:

running 2 tests
...cubic... ok
...bbr2_gcongestion... ok
2 passed; 0 failed

This looks like a small addition to the existing reset block rather than a separate PR; the rest of #2770's transport/application rollback matches the same direction.

Signed-off-by: namtran1812 <158846154+namtran1812@users.noreply.github.com>
@namtran1812

Copy link
Copy Markdown
Author

Addressed in the latest commit. The rejection reset now clears blocked_limit, both StreamsBlockedState values, and queued DATAGRAMs using purge(|_| true), which also resets queue byte accounting while preserving capacity.
Added a regression under cubic and bbr2_gcongestion that primes all four fields and checks their reset, including DATAGRAM queue length and byte accounting. Both cases failed before the fix and pass afterward.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: streams Changes related to streams. area: tls Changes related to tls. type: bugfix Corrects defective behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants