Re: [PATCH net] sctp: sm_statefuns: Fix zero window detection in T3 timeout handling
Jakub Kicinski <[email protected]> Mon, 3 Aug 2026 18:55:55 -0700
| Newsgroups | org.kernel.vger.netdev,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
This is an AI-generated review of your patch. The human sending this email has considered the AI review valid, or at least plausible. --- --- Patch [1]: [PATCH net] sctp: sm_statefuns: Fix zero window detection in T3 timeout handling --- commit e24cf7acae572e3936b6d8fbcb1d3942444ce068 Author: Qing Luo <[email protected]> sctp: sm_statefuns: Fix zero window detection in T3 timeout handling This drops the asoc->state == SCTP_STATE_SHUTDOWN_PENDING qualifier from the zero-window branch of sctp_sf_do_6_3_3_rtx(), so the T5 shutdown guard timer is now started whenever asoc->peer.zero_window_announced is set, in any state that dispatches the T3-rtx timeout. No Link: tags were present in the commit header. > diff --git a/net/sctp/sm_statefuns.c b/net/sctp/sm_statefuns.c > index 708fa07d5fffc..b01db33bbc360 100644 > --- a/net/sctp/sm_statefuns.c > +++ b/net/sctp/sm_statefuns.c > @@ -5842,8 +5842,7 @@ enum sctp_disposition sctp_sf_do_6_3_3_rtx(struct net *net, > SCTP_INC_STATS(net, SCTP_MIB_T3_RTX_EXPIREDS); > > if (asoc->overall_error_count >= asoc->max_retrans) { > - if (asoc->peer.zero_window_announced && > - asoc->state == SCTP_STATE_SHUTDOWN_PENDING) { > + if (asoc->peer.zero_window_announced) { [Severity: High] [Finding: main-1-0] [Sources: opus-5] Is the T5 timer actually handled in the states this now arms it in? sctp_sf_do_6_3_3_rtx() runs in COOKIE_ECHOED, ESTABLISHED, SHUTDOWN_PENDING and SHUTDOWN_RECEIVED per the T3-rtx table in net/sctp/sm_statetable.c: #define TYPE_SCTP_EVENT_TIMEOUT_T3_RTX { \ ... /* SCTP_STATE_ESTABLISHED */ \ TYPE_SCTP_FUNC(sctp_sf_do_6_3_3_rtx), \ But the T5 table in the same file dispatches the expiry to sctp_sf_timer_ignore() for exactly the extra states this patch adds: #define TYPE_SCTP_EVENT_TIMEOUT_T5_SHUTDOWN_GUARD { \ ... /* SCTP_STATE_COOKIE_ECHOED */ \ TYPE_SCTP_FUNC(sctp_sf_timer_ignore), \ /* SCTP_STATE_ESTABLISHED */ \ TYPE_SCTP_FUNC(sctp_sf_timer_ignore), \ /* SCTP_STATE_SHUTDOWN_PENDING */ \ TYPE_SCTP_FUNC(sctp_sf_t5_timer_expire), \ ... /* SCTP_STATE_SHUTDOWN_RECEIVED */ \ TYPE_SCTP_FUNC(sctp_sf_timer_ignore), \ So in ESTABLISHED the "one last chance ... before aborting" described by the retained comment never happens, since sctp_sf_timer_ignore() only returns SCTP_DISPOSITION_CONSUME. Does this remove the Association.Max.Retrans enforcement for those states? The else branch, which is the only place that does SCTP_CMD_SET_SK_ERR(ETIMEDOUT) + SCTP_CMD_ASSOC_FAILED and returns SCTP_DISPOSITION_DELETE_TCB, is now skipped whenever zero_window_announced is set. Each later T3-rtx expiry still sees overall_error_count >= max_retrans, so it only queues STRIKE + RETRAN and re-arms the (no longer pending) T5, because sctp_cmd_interpreter() in net/sctp/sm_sideeffect.c re-arms on SCTP_CMD_TIMER_START_ONCE whenever the timer is not pending: case SCTP_CMD_TIMER_START_ONCE: timer = &asoc->timers[cmd->obj.to]; if (timer_pending(timer)) break; fallthrough; Can the sender then retransmit indefinitely with no error reported to the application, keeping the association, its outqueue chunks, sndbuf charge, transports and socket pinned? Note the flag that now solely decides abort vs. no abort comes straight from the remote peer, in sctp_outq_sack(): asoc->peer.zero_window_announced = !sack_a_rwnd; so a peer that announces a_rwnd == 0 and then goes silent controls the trigger. The heartbeat path sctp_sf_sendbeat_8_3() is the only remaining teardown for this condition, and it looks deferrable: every retransmission refreshes transport->last_time_sent in net/sctp/outqueue.c, while sctp_generate_heartbeat_event() keeps rescheduling instead of feeding the state machine: elapsed = jiffies - transport->last_time_sent; timeout = sctp_transport_timeout(transport); if (elapsed < timeout) { elapsed = timeout - elapsed; if (!mod_timer(&transport->hb_timer, jiffies + elapsed)) sctp_transport_hold(transport); goto out_unlock; } Since sctp_transport_timeout() is rto/2 + hbinterval, does any configuration with hbinterval > rto/2 (for example the default 30 s hbinterval with net.sctp.rto_max lowered, or hbinterval raised via net.sctp.hb_interval or SCTP_PEER_ADDR_PARAMS) push the heartbeat event out forever so no abort ever occurs? [Severity: High] [Finding: main-1-2] [Sources: opus-5] Should the matching cancel site be updated too? The only place that stops T5 on forward progress is still state-qualified, in sctp_check_transmitted() in net/sctp/outqueue.c: if (asoc->state == SCTP_STATE_SHUTDOWN_PENDING && timer_delete(&asoc->timers[SCTP_EVENT_TIMEOUT_T5_SHUTDOWN_GUARD])) sctp_association_put(asoc); A T5 armed while ESTABLISHED is therefore not disarmed when the peer reopens its window and acknowledges data: sctp_check_transmitted() clears transport->error_count and asoc->overall_error_count but skips the timer_delete() because the state is not SHUTDOWN_PENDING. The remaining SCTP_CMD_TIMER_STOP(T5) sites in net/sctp/sm_statefuns.c are ABORT/close handlers, and the T5 SCTP_CMD_TIMER_RESTART lives only in sctp_sf_do_9_2_start_shutdown(), which is not reached with a non-empty outqueue. The timer then stays pending with its ESTABLISHED-era deadline (asoc->timeouts[T5] is 5 * rto_max, 300 s by default). If the application later calls shutdown()/close() with data still queued, sctp_sf_do_9_2_prm_shutdown() moves to SHUTDOWN_PENDING without touching T5: sctp_add_cmd_sf(commands, SCTP_CMD_NEW_STATE, SCTP_STATE(SCTP_STATE_SHUTDOWN_PENDING)); disposition = SCTP_DISPOSITION_CONSUME; if (sctp_outq_is_empty(&asoc->outqueue)) { disposition = sctp_sf_do_9_2_start_shutdown(...); In SHUTDOWN_PENDING the stale expiry now reaches sctp_sf_t5_timer_expire(), which unconditionally aborts: reply = sctp_make_abort(asoc, NULL, 0); ... sctp_add_cmd_sf(commands, SCTP_CMD_SET_SK_ERR, SCTP_ERROR(ETIMEDOUT)); sctp_add_cmd_sf(commands, SCTP_CMD_ASSOC_FAILED, SCTP_PERR(SCTP_ERROR_NO_ERROR)); return SCTP_DISPOSITION_DELETE_TCB; Can this tear down a healthy, fully recovered association and discard queued user data with far fewer than Max.Retrans shutdown-phase retransmissions? Also, can the legitimate shutdown-guard arming still repair the deadline? SCTP_CMD_TIMER_START_ONCE short-circuits on timer_pending() and the SCTP_CMD_TIMER_START fallthrough uses timer_reduce(), which only moves an expiry earlier: if (timer_pending(timer)) break; fallthrough; case SCTP_CMD_TIMER_START: ... timer_reduce(timer, jiffies + timeout); For reference, the arming and the SHUTDOWN_PENDING-gated cancel were added together by f8d960524328 ("sctp: Enforce retransmission limit during shutdown"), so the two sites appear intended to be paired. [Severity: Medium] [Finding: main-1-1] [Sources: opus-5] A question about the changelog rationale as well. It states: "This caused premature ABORTs when the receiver's rwnd was closed but the association was still in ESTABLISHED state." In ESTABLISHED (state < SCTP_STATE_SHUTDOWN_PENDING) the counters that gate this ABORT are already reset on any sign of life from the peer. sctp_check_transmitted() in net/sctp/outqueue.c has a zero-window probe block: if (!q->asoc->peer.rwnd && !list_empty(&tlist) && (sack_ctsn+2 == q->asoc->next_tsn) && q->asoc->state < SCTP_STATE_SHUTDOWN_PENDING) { ... q->asoc->overall_error_count = 0; transport->error_count = 0; and sctp_cmd_transport_on() in net/sctp/sm_sideeffect.c clears the counter on every HEARTBEAT ACK: if (t->asoc->state < SCTP_STATE_SHUTDOWN_PENDING) t->asoc->overall_error_count = 0; Given those, can overall_error_count reach max_retrans in ESTABLISHED unless the peer acknowledges neither data nor heartbeats at all? If it cannot, is the ABORT being suppressed here the RFC 4960/9260 section 8.2 behaviour rather than a premature one? Two further points on the header: Fixes: 8a0d19c5ed41 ("sctp: start t5 timer only when peer rwnd is 0 and local state is SHUTDOWN_PENDING") That commit added the zero_window_announced conjunct. The asoc->state == SCTP_STATE_SHUTDOWN_PENDING condition being removed came from f8d960524328 ("sctp: Enforce retransmission limit during shutdown"), which deliberately confined the T5 guard to the shutdown phase. Should the Fixes: tag point there instead, and can the changelog address that commit's rationale? "According to RFC 2960 Section 6.1, zero window probing is a normal protocol behavior." RFC 2960 is obsoleted; RFC 4960/9260 would be the current reference. T5 is defined as the shutdown-sequence guard (RFC 4960 section 9.2, see the comment above sctp_sf_t5_timer_expire), so could the changelog also explain why reusing it as a generic zero-window guard in ESTABLISHED is appropriate, and update the "one last chance ... before aborting" comment that no longer describes what happens in that state? -- pw-bot: cr