[PATCH net v2 3/4] sctp: fix I-FORWARD-TSN handling of fragments on other streams

From: Tim Budding

Date: Thu Nov 05 2026 - 00:05:22 EST


sctp_report_iftsn() incorrectly frees queued I-DATA fragments in two
cases. First, it drops any queued fragments with TSN less than or equal
to the New Cumulative TSN. This prevents reconstruction of messages
still being received on other streams and can fully block an ordered
stream. Second, it calls sctp_intl_abort_pd() if the New Cumulative TSN
equals the highest TSN seen. This check exists for association resets
but can be inadvertently triggered by any I-FORWARD-TSN that arrives
before data with a higher TSN, causing data loss and, if a partial
delivery is in progress on an ordered stream, an aborted association.

Fix the first by freeing fragments from sctp_intl_skip() based on
stream, U bit, and MID instead of TSN. Fix the second by moving the call
to sctp_intl_abort_pd() from sctp_report_iftsn() to the corresponding
reset handlers.

Observed in real use as unexpected message loss. Reproduced on an
unmodified kernel with the sctp_ifwdtsn.sh selftest added later in this
series, which passes with this fix. Reproduction and fix done with AI
assistance.

Fixes: 47b20a88566f ("sctp: implement report_ftsn for sctp_stream_interleave")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Tim Budding <tim.budding@xxxxxxxxx>
---
net/sctp/stream.c | 2 ++
net/sctp/stream_interleave.c | 32 +++++++++++---------------------
2 files changed, 13 insertions(+), 21 deletions(-)

diff --git a/net/sctp/stream.c b/net/sctp/stream.c
index e4354d58f20b..d5e71a2027f5 100644
--- a/net/sctp/stream.c
+++ b/net/sctp/stream.c
@@ -749,6 +749,7 @@ struct sctp_chunk *sctp_process_strreset_tsnreq(
*/
max_tsn_seen = sctp_tsnmap_get_max_tsn_seen(&asoc->peer.tsn_map);
asoc->stream.si->report_ftsn(&asoc->ulpq, max_tsn_seen);
+ asoc->stream.si->abort_pd(&asoc->ulpq, GFP_ATOMIC);

/* G1: Compute an appropriate value for the Receiver's Next TSN -- the
* TSN that the peer should use to send the next DATA chunk. The
@@ -1028,6 +1029,7 @@ struct sctp_chunk *sctp_process_strreset_resp(
LIST_HEAD(temp);

asoc->stream.si->report_ftsn(&asoc->ulpq, mtsn);
+ asoc->stream.si->abort_pd(&asoc->ulpq, GFP_ATOMIC);

sctp_tsnmap_init(&asoc->peer.tsn_map,
SCTP_TSN_MAP_INITIAL,
diff --git a/net/sctp/stream_interleave.c b/net/sctp/stream_interleave.c
index e239da686f68..1113b3455369 100644
--- a/net/sctp/stream_interleave.c
+++ b/net/sctp/stream_interleave.c
@@ -1203,28 +1203,21 @@ static void sctp_report_fwdtsn(struct sctp_ulpq *ulpq, __u32 ftsn)
sctp_ulpq_abort_pd(ulpq, GFP_ATOMIC);
}

-static void sctp_intl_reasm_flushtsn(struct sctp_ulpq *ulpq, __u32 ftsn)
+static void sctp_intl_reasm_flush_mid(struct sk_buff_head *reasm, __u16 sid,
+ __u32 mid)
{
struct sk_buff *pos, *tmp;

- skb_queue_walk_safe(&ulpq->reasm, pos, tmp) {
+ skb_queue_walk_safe(reasm, pos, tmp) {
struct sctp_ulpevent *event = sctp_skb2event(pos);
- __u32 tsn = event->tsn;

- if (TSN_lte(tsn, ftsn)) {
- __skb_unlink(pos, &ulpq->reasm);
- sctp_ulpevent_free(event);
- }
- }
-
- skb_queue_walk_safe(&ulpq->reasm_uo, pos, tmp) {
- struct sctp_ulpevent *event = sctp_skb2event(pos);
- __u32 tsn = event->tsn;
+ if (event->stream < sid)
+ continue;
+ if (event->stream > sid || MID_lt(mid, event->mid))
+ break;

- if (TSN_lte(tsn, ftsn)) {
- __skb_unlink(pos, &ulpq->reasm_uo);
- sctp_ulpevent_free(event);
- }
+ __skb_unlink(pos, reasm);
+ sctp_ulpevent_free(event);
}
}

@@ -1232,11 +1225,6 @@ static void sctp_report_iftsn(struct sctp_ulpq *ulpq, __u32 ftsn)
{
/* Move the Cumulattive TSN Ack ahead. */
sctp_tsnmap_skip(&ulpq->asoc->peer.tsn_map, ftsn);
- /* purge the fragmentation queue */
- sctp_intl_reasm_flushtsn(ulpq, ftsn);
- /* abort only when it's for all data */
- if (ftsn == sctp_tsnmap_get_max_tsn_seen(&ulpq->asoc->peer.tsn_map))
- sctp_intl_abort_pd(ulpq, GFP_ATOMIC);
}

static void sctp_handle_fwdtsn(struct sctp_ulpq *ulpq, struct sctp_chunk *chunk)
@@ -1260,6 +1248,7 @@ static void sctp_intl_skip(struct sctp_ulpq *ulpq, __u16 sid, __u32 mid,
sctp_intl_stream_abort_pd(ulpq, sid, sin->mid_uo, 0x1,
GFP_ATOMIC);
}
+ sctp_intl_reasm_flush_mid(&ulpq->reasm_uo, sid, mid);
return;
}

@@ -1271,6 +1260,7 @@ static void sctp_intl_skip(struct sctp_ulpq *ulpq, __u16 sid, __u32 mid,
sctp_intl_stream_abort_pd(ulpq, sid, sin->mid, 0x0, GFP_ATOMIC);
}

+ sctp_intl_reasm_flush_mid(&ulpq->reasm, sid, mid);
sctp_mid_skip(stream, in, sid, mid);

sctp_intl_reap_ordered(ulpq, sid);
--
2.34.1