Skip to content

stream: keep a pending FIN schedulable after data is acknowledged - #2804

Open
akasakariko wants to merge 1 commit into
cloudflare:masterfrom
akasakariko:stream-pending-fin
Open

akasakariko wants to merge 1 commit into
cloudflare:masterfrom
akasakariko:stream-pending-fin

Conversation

@akasakariko

Copy link
Copy Markdown

Summary

  • track whether a stream FIN still needs to be sent in SendBuf
  • requeue a lost FIN even when its DATA was acknowledged in another packet
  • keep the stream flushable when only the pending FIN remains after retransmitting data before an acknowledged tail
  • avoid resending a FIN that is still in flight
  • avoid sending a STREAM frame after RESET_STREAM when a FIN-bearing frame is lost
  • add regressions based on the standalone reproducers from stream: pending FIN can be stranded after DATA acknowledgement and retransmission #2800

Testing

  • cargo +nightly-2026-09-15 fmt -- --check
  • cargo clippy --features=async,ffi,qlog,rpk --workspace -- -D warnings
  • cargo test --all-targets --features=async,ffi,qlog,rpk --workspace
  • cargo test --doc --features=async,ffi,qlog,rpk --workspace

Fixes #2800

Made with Cursor

The FIN was never sent in two cases:

- a lost STREAM frame carrying FIN whose data was acknowledged in
  another packet, since only lost zero-length FINs were requeued
- data retransmitted before an already acknowledged tail, since the
  stream stopped being flushable once that data was sent

Track whether the final size still needs to be sent in SendBuf:

- set it when the final size becomes known, or when a FIN-bearing
  frame is lost before the FIN is acknowledged
- clear it when a FIN is emitted or acknowledged, or when the stream
  is reset
- treat a stream as flushable when only its FIN is left to send

This replaces the zero-length FIN special cases, avoids resending a FIN
that is still in flight, and no longer sends a STREAM frame after
RESET_STREAM when a FIN-bearing frame is lost

Fixes cloudflare#2800

Co-authored-by: Cursor <cursoragent@cursor.com>
@akasakariko
akasakariko requested a review from a team as a code owner October 6, 2026 04:35
@akasakariko

Copy link
Copy Markdown
Author

@lixing-lx Thanks for offering to help validate! The fix is up in this PR — would you mind running it against your TrustTunnel test setup to confirm the stranded FIN case is resolved?

@lixing-lx

Copy link
Copy Markdown

Thanks @akasakariko. I validated b040feea6e436f0987d44b25274ebcea5869f57c; it resolves both original stranded-FIN reproducers in my setup.

Deterministic comparison (same dependency lock, BoringSSL 5.2.0, Rust 1.99.0): I appended the two original Issue #2800 reproducer functions unchanged in behavior to separate base/head checkouts. On base 4d1a5341a9c71a3c79be1b2abb9611c389f349a7, all four CUBIC/BBRv2 cases fail; on this PR, all four pass. The complete quiche --lib suite on the PR is 1,224 passed / 0 failed / 0 ignored, including those four cases and the PR's additional pending/in-flight/reset regressions. Nightly-2026-09-15 fmt and strict quiche all-target clippy with ffi/qlog and BoringSSL rpk enabled also pass.

TrustTunnel downstream validation: I compiled both the client and the server against this exact PR's quiche production sources, checking the changed production files against the commit. Client/server Cargo target directories are separate. The server uses the source from TrustTunnel's dependency-upgrade PR #155, aefb830b19158d1095024551112408f287b6b1d5, so it accepts quiche 0.30.0 / BoringSSL 5.2.0. It has no earlier local FIN-scheduling patches or other local server transport/control/stream repairs. This is an explicitly labelled experimental peer, not the v1.1.0 official release binary.

H2/H3 smoke tests pass, covering TCP/UDP, authentication and certificate rejection, half-close and concurrent byte-checked transfers. The sustained H3 run uses client BBRv2 with a 32-packet initial window and the server's unchanged default congestion settings, in a Linux arm64 Docker namespace with network=none, no host ports, read-only root and dropped capabilities:

Phase Duration Completed 64 KiB transfers with EOF
Stable 1,200.711 s 4,365
Latency 1,201.166 s 3,328
3% QUIC loss + latency/bandwidth limits 1,202.854 s 1,124

Total: 8,817 content-and-EOF-verified transfers / 577,830,912 bytes, no missing EOF or transfer timeout. The same run passes 20 session recreations, cancellation of an old read during a blackhole, bounded blackhole dial failure and subsequent recovery. Kernel UDP receive-buffer/send-buffer error counters remain zero; the acceptance validator checks all phase durations/counts and recreation cases, not just the test harness exit code.

The latency/loss relay uses approximately 80 ms ± 10 ms delay per direction and a 10 Mbps cap per direction. The fault conditions, source fingerprints, binary SHA256 values and per-run logs are retained locally. This confirms the standalone scheduling failures and the controlled TrustTunnel path on this head; it is not a claim of public-network or production-server acceptance.

@akasakariko

Copy link
Copy Markdown
Author

Thanks a lot @lixing-lx for the thorough validation, especially the downstream TrustTunnel run under loss and latency. Really appreciate it

Maintainers, this is ready for review whenever you have a chance. Happy to address any feedback

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stream: pending FIN can be stranded after DATA acknowledgement and retransmission

2 participants