From 358355200929e2b56d719fa237f609261f468feb Mon Sep 17 00:00:00 2001 From: F6BVP Date: Sun, 13 Sep 2026 11:47:43 +0200 Subject: [PATCH] ax25: bound REJ-driven fast retransmits with the existing N2 counter REJ deliberately bypasses the T1/N2 backoff to trigger an immediate go-back-N retransmit -- that is the whole point of REJ, fast recovery from a single lost or out-of-order frame. But unlike every other recovery path in this same state machine (T1 timeout, state 4 enquiry/DM, state 1/2 SABM retries), the REJ case in ax25_std_state3_machine() never touches n2count, so it has no bound. If something keeps corrupting or reordering every retransmitted burst -- lossy RF, or packet reordering over a UDP-based transport such as axudp -- each attempt draws an immediate fresh REJ and the link retransmits at wire/CPU speed indefinitely, a "machine-gun" storm that never reaches the N2 give-up already relied on everywhere else in this file. Reported independently twice: Chris Maness (KQ6UP) on a plain AX.25 link with no ROSE involved at all, and observed live on a ROSE/FPAC White Pages link (f6bvp-8<->f6bvp-10, same LAN, so plain network reordering over the Internet is not the trigger there -- likely lossy retransmission of the same PDU) reaching ~5900 pkt/s. Both hit the same code path since ax25_std_in.c is shared regardless of what rides on top of AX.25. Count consecutive REJ-driven fast retransmits the same way a T1 timeout already does in this state, and once N2 of them have failed to get an in-sequence frame through, fall back to the paced state 4 enquiry -- which will genuinely disconnect after N2 further tries if the link stays bad, exactly like every other recovery path already does. A single frame accepted in sequence resets the counter, so an isolated REJ during otherwise healthy traffic never gets close to N2. Deployed on three Raspberry Pi nodes (f6bvp-12, f6bvp-8, f6bvp-10) since 2026-09-13, running live production AX.25/ROSE/NET-ROM traffic since, with no recurrence of the machine-gun storm and no other AX.25 regression observed. Signed-off-by: Bernard Pidoux --- net/ax25/ax25_std_in.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/net/ax25/ax25_std_in.c b/net/ax25/ax25_std_in.c index ba17619..f280683 100644 --- a/net/ax25/ax25_std_in.c +++ b/net/ax25/ax25_std_in.c @@ -196,6 +196,38 @@ static int ax25_std_state3_machine(ax25_cb *ax25, struct sk_buff *skb, int frame if (ax25_validate_nr(ax25, nr)) { ax25_frames_acked(ax25, nr); ax25_calculate_rtt(ax25); + /* + * REJ triggers an immediate go-back-N retransmit, + * deliberately bypassing the T1/N2 backoff that + * paces every other recovery path in this state + * machine -- that is the whole point of REJ, fast + * recovery from a single lost/out-of-order frame. + * But unlike every other recovery path here, this + * one never touches n2count, so it has no bound: if + * something keeps corrupting or reordering each + * retransmitted burst (lossy RF, or reordering over + * a UDP-based link such as axudp), every attempt + * draws an immediate fresh REJ and the link is + * retransmitted at wire/CPU speed indefinitely + * ("machine-gun" storm), never reaching the N2 give + * up that bounds every other recovery attempt. + * Count consecutive REJ-driven fast retransmits the + * same way a T1 timeout would in this state, and + * once N2 of them have failed to make it through, + * fall back to the normal paced state 4 enquiry + * (which will genuinely give up and disconnect after + * N2 further tries if the link stays bad). A single + * frame accepted in sequence resets the counter (see + * the AX25_I case below), so an isolated REJ during + * otherwise healthy traffic never gets close to N2. + */ + if (ax25->n2count == ax25->n2) { + ax25->n2count = 1; + ax25_std_transmit_enquiry(ax25); + ax25->state = AX25_STATE_4; + break; + } + ax25->n2count++; ax25_stop_t1timer(ax25); ax25_start_t3timer(ax25); ax25_requeue_frames(ax25); @@ -226,6 +258,10 @@ static int ax25_std_state3_machine(ax25_cb *ax25, struct sk_buff *skb, int frame if (ax25->condition & AX25_COND_OWN_RX_BUSY) ax25->vr = ns; /* ax25->vr - 1 */ ax25->condition &= ~AX25_COND_REJECT; + /* Genuine forward progress: a frame made it through in + * sequence, so this is not part of a REJ storm. + */ + ax25->n2count = 0; if (pf) { ax25_std_enquiry_response(ax25); } else {