tcp: do not leak SACKed-then-ACKed segments into Outstanding outside recovery - #14101
Open
davidbell217 wants to merge 1 commit into
Open
tcp: do not leak SACKed-then-ACKed segments into Outstanding outside recovery#14101davidbell217 wants to merge 1 commit into
davidbell217 wants to merge 1 commit into
Conversation
…recovery The cumulative-ACK removal loop in handleRcvdSegment skips the Outstanding decrement for segments that were previously SACKed, on the premise that SetPipe() already accounted for them. That premise only holds during loss recovery: SetPipe() returns immediately when recovery is not active, and nothing else removes those packets. A segment SACKed outside recovery (benign reordering) and then cumulatively ACKed therefore leaks its packet count into Outstanding permanently. On a long-lived connection over a reordering-but-lossless path the phantoms accumulate until the sender believes it is cwnd-limited while almost nothing is in flight, throttling bulk transfers (observed: a wedged sender with Outstanding=86, cwnd=86 and ~5 packets actually in flight, and a completed transfer running at ~8% of its believed window). Track packets newly SACKed while no recovery is in progress in a sender-local counter, and consume it when those segments are cumulatively ACKed. The counter resets wherever Outstanding is recomputed or reset wholesale (recovery entry, RTO, full window drain), so recovery-path accounting is unchanged. Two simpler shapes were measured and rejected: - decrementing Outstanding unconditionally in the removal loop's SACKed branch double-subtracts segments SetPipe() already accounted for (SACKed during recovery, cumulatively ACKed at/after exit) and breaks TestSACKRecovery's post-recovery pacing; - decrementing at SACK arrival (walkSACK) frees congestion window slots before the cumulative ACK, effectively unbounded limited transmit, and breaks TestRACKWithWindowFull with a transmission after a zero-window ACK. Fixes google#14092 Assisted-by: Claude Code
davidbell217
force-pushed
the
fix-outstanding-sack-leak-outside-recovery
branch
from
August 13, 2026 12:12
208f3b2 to
df000e6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14092.
The cumulative-ACK removal loop in
handleRcvdSegmentskips theOutstandingdecrement for previously-SACKed segments, on the premise thatSetPipe()already accounted for them. That premise only holds during loss recovery —SetPipe()is a no-op otherwise — so a segment SACKed outside recovery (benign reordering, RACK correctly declaring no loss) and then cumulatively ACKed leaks its packet count intoOutstandingpermanently. On long-lived bulk transfers over reordering-but-lossless paths the phantoms accumulate untilsendData'sOutstanding < SndCwndgate throttles a healthy connection (details and repro in the issue).The fix: track packets newly SACKed while no recovery is in progress in a sender-local counter (
sackedOutsideRecovery), consume it when those segments are cumulatively ACKed, and reset it whereverOutstandingis recomputed or reset wholesale (recovery entry, RTO, full window drain). Recovery-path accounting is unchanged.Why not something simpler — both obvious one-line shapes were implemented and measured, and each breaks an existing test for a legitimate reason:
SetPipe()already accounted for — SACKed during recovery, cumulatively ACKed at/after exit, whereleaveRecoveryhas already run before the removal loop. BreaksTestSACKRecovery(post-recovery pacing sends beyond the expected window).walkSACK): frees congestion-window slots before the cumulative ACK — effectively unbounded limited transmit — and only covers RACK-enabled senders. BreaksTestRACKWithWindowFull(a transmission observed after a zero-window ACK).The counter shape keeps the conservative cumulative-ACK release point, never touches segments
SetPipe()accounted for, and works with RACK on or off.Verification
TestSACKedThenAckedSegmentLeavesOutstanding(both RACK on and off): fails before the fix withOutstanding = 3, want 2— exactly one phantom packet after a SACKed-then-cumulatively-ACKed segment — and passes with it.tcp_sack_test.goandtcp_rack_test.goe2e suites pass with the fix.Assisted-by: Claude Code