[Bug 296594] TCP: RFC 5961 sends a challenge ACK instead of resetting when a valid RST arrives with SEG.SEQ == RCV.NXT and the receiver has delayed-ACKed data, leaving the connection half-open

[email protected] Fri, 10 Jul 2026 09:33:10 +0000
Newsgroups gmane.os.freebsd.devel.net
Message-ID <[email protected]/bugzilla/>
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=296594

--- Comment #7 from Thomas Grainger <[email protected]> ---
Updating the patch with a third change. The RFC 5961 RST handling has three
sub-cases where a valid RST at rcv_nxt fails to reset under a delayed ACK
(rcv_nxt > last_ack_sent), not two:

  1. in-window, rcv_wnd large: the inner exact-match test accepts only
     last_ack_sent, so the RST draws a challenge ACK instead of resetting;
  2. rcv_wnd shrunk below the delayed-ACK gap: the RST at rcv_nxt falls beyond
     last_ack_sent + rcv_wnd and is silently dropped by the outer window check;
  3. rcv_wnd == 0: with the outer edge fixed to rcv_nxt + rcv_wnd, arm A can
     never admit a RST at rcv_nxt when the window is closed -- SEQ_LT(rcv_nxt,
     rcv_nxt + 0) is false -- so acceptance falls to the zero-window arm,
     which also tests last_ack_sent only and rejects it.

The updated patch (freebsd-current-tcp-rst-rcvnxt.patch, against main) makes
all
three changes -- accept rcv_nxt in the inner reset test, anchor the outer
window
edge on rcv_nxt + rcv_wnd, and accept rcv_nxt in the zero-window arm:

        if ((SEQ_GEQ(th->th_seq, tp->last_ack_sent) &&
            SEQ_LT(th->th_seq, tp->rcv_nxt + tp->rcv_wnd)) ||
            (tp->rcv_wnd == 0 && (tp->last_ack_sent == th->th_seq ||
            tp->rcv_nxt == th->th_seq))) {
                KASSERT(tp->t_state != TCPS_SYN_SENT, ...);
                if (V_tcp_insecure_rst ||
                    tp->last_ack_sent == th->th_seq ||
                    tp->rcv_nxt == th->th_seq) {

None of this weakens the anti-spoofing intent: an off-path attacker must still
guess rcv_nxt (or last_ack_sent) exactly; in-window but non-exact RSTs still
draw a challenge ACK.

Validation. Cases 1 and 2 arise organically -- changes 1 + 2 already take a
stock GENERIC kernel from 15/1,919,209 hangs to 0/31,651,215 connections with
c/repro.c under QEMU/KVM. Case 3 is not reached by a conforming loopback flow
(a
sender will not push data past a zero window), which is why the organic run
does
not exercise it, but it is directly reachable with crafted packets. I built two
15.1-RELEASE GENERIC kernels differing ONLY by change 3 -- RSTBOTH (changes
1 + 2) and RSTARMB (changes 1 + 2 + 3), uname -i verified -- and ran three
deterministic packetdrill tests on each:

  test                                     stock   RSTBOTH(1+2)  
RSTARMB(1+2+3)
  rst-rcvnxt-challenge-ack.pkt (case 1)     RED       GREEN           GREEN
  rst-rcvnxt-window-drop.pkt   (case 2)     RED       GREEN           GREEN
  rst-rcvnxt-zero-window.pkt   (case 3)     RED       RED             GREEN

RED = read() returns EAGAIN (RST not honored); GREEN = read() returns
ECONNRESET. Only the zero-window test flips RED->GREEN across the single-line
change 3, with the other two tests GREEN on both kernels as controls -- so the
zero-window gap is real and change 3 is what closes it.

-- 
You are receiving this mail because:
You are on the CC list for the bug.