* [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving
@ 2026-11-05 5:02 Tim Budding
2026-11-05 5:02 ` [PATCH net v2 1/4] sctp: abort unordered partial delivery when its message is skipped Tim Budding
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Tim Budding @ 2026-11-05 5:02 UTC (permalink / raw)
To: netdev, linux-sctp
Cc: lucien.xin, marcelo.leitner, davem, edumazet, kuba, pabeni,
horms, shuah, linux-kselftest, linux-kernel, Tim Budding
With user message interleaving (RFC 8260), fragments of messages on
different streams share the TSN space. When a PR-SCTP message is
abandoned, the receiver still frees queued fragments by the
I-FORWARD-TSN's New Cumulative TSN, which loses fragments of messages
still being received on other streams:
TSN stream MID
0 0 0 B 64KB reliable message on stream 0
...
5 0 0
6 1 0 BE PR-SCTP message on stream 1, lost, abandoned
7 0 0
...
I-FORWARD-TSN new_cum=6 skip=[sid=1 mid=0]
The stream 0 fragments with TSNs 0-5 are freed with the abandoned
message, are never retransmitted, and stream 0 stays blocked.
Patch 3 frees fragments by the skip entries instead of by TSN, and no
longer treats an I-FORWARD-TSN that reaches the highest TSN seen as a
reset. Such an I-FORWARD-TSN is then handled by sctp_intl_skip() alone,
which has two partial delivery bugs of its own: an unordered partial
delivery is not aborted when its own message is skipped, and
PARTIAL_DELIVERY_ABORTED reports the highest skipped MID rather than
the one being delivered. Patches 1 and 2 fix these first, so the three
fixes should be backported together and in order. Patch 4 adds a
selftest.
Patch 3 has a cost, as discussed on v1 [2]: an unordered fragment of an
abandoned message that first arrives after the last I-FORWARD-TSN
naming that message is no longer freed by a later, unrelated
I-FORWARD-TSN. It keeps its rwnd and receive buffer charge until a
later unordered skip on its stream, a reset, or teardown. Dropping such
fragments on arrival needs new per-stream state and is left to a
follow-up [3].
Changes since v1 [1]:
- Fold "sctp: don't treat an I-FORWARD-TSN as an association reset"
into the purge patch, now patch 3, as Xin suggested [3].
- New patch 2, from Sashiko's review of v1 patch 3.
- Move the unordered partial delivery fix first.
- Selftest: add leading_edge_nopd, reset_pending_srv/cli and
pdapi/pdapi_uo; wait for conditions rather than fixed times,
including all data being acked before the queue check.
Tested with the new selftest, sctp_vrf.sh and the lksctp-tools
functional tests, also on a kernel with KASAN, lockdep and kmemleak.
W=1 and sparse show no new warnings.
Initial bug observed in use, root cause identified with AI-assisted code
inspection. Related bugs found and fixes implemented with AI assistance.
All reviewed and tested by me.
[1] https://lore.kernel.org/all/20261024120627.837568-1-tim.budding@gmail.com/
[2] https://lore.kernel.org/all/20261026052706.187296-1-tim.budding@gmail.com/
[3] https://lore.kernel.org/all/CADvbK_du=-oT19kKBKYSjFKYscnQSH1mekRKMXgmLPMXTEH_=w@mail.gmail.com/
Tim Budding (4):
sctp: abort unordered partial delivery when its message is skipped
sctp: report the partially delivered MID in PARTIAL_DELIVERY_ABORTED
sctp: fix I-FORWARD-TSN handling of fragments on other streams
selftests: net: add SCTP I-FORWARD-TSN tests with message interleaving
net/sctp/stream.c | 2 +
net/sctp/stream_interleave.c | 38 +-
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 2 +
tools/testing/selftests/net/sctp_ifwdtsn.c | 699 ++++++++++++++++++++
tools/testing/selftests/net/sctp_ifwdtsn.sh | 233 +++++++
6 files changed, 951 insertions(+), 24 deletions(-)
create mode 100644 tools/testing/selftests/net/sctp_ifwdtsn.c
create mode 100755 tools/testing/selftests/net/sctp_ifwdtsn.sh
base-commit: 6baae996ecf6a3f57b08ea6f6786e29af9260f51
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v2 1/4] sctp: abort unordered partial delivery when its message is skipped
2026-11-05 5:02 [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving Tim Budding
@ 2026-11-05 5:02 ` Tim Budding
2026-11-05 5:02 ` [PATCH net v2 2/4] sctp: report the partially delivered MID in PARTIAL_DELIVERY_ABORTED Tim Budding
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Tim Budding @ 2026-11-05 5:02 UTC (permalink / raw)
To: netdev, linux-sctp
Cc: lucien.xin, marcelo.leitner, davem, edumazet, kuba, pabeni,
horms, shuah, linux-kselftest, linux-kernel, Tim Budding, stable
When an I-FORWARD-TSN skips unordered messages on a stream,
sctp_intl_skip() only aborts the stream's unordered partial delivery if
the MID being delivered is lower than the skipped MID. If the skipped
message is the one being partially delivered, partial delivery stays in
progress: no PARTIAL_DELIVERY_ABORTED is sent, and the next unordered
message on the stream is delivered as the rest of the abandoned one.
Abort the partial delivery when the MID being delivered is at or below
the skipped MID, as the ordered case already does.
Reproduced on an unmodified kernel with the uo_pd test of the
sctp_ifwdtsn.sh selftest added later in this series; its partial
delivery checks pass with this fix. Reproduction and fix done with AI
assistance.
Fixes: de60fe910543 ("sctp: implement handle_ftsn for sctp_stream_interleave")
Cc: stable@vger.kernel.org
Signed-off-by: Tim Budding <tim.budding@gmail.com>
---
net/sctp/stream_interleave.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/sctp/stream_interleave.c b/net/sctp/stream_interleave.c
index 840f24045ae2..869940001588 100644
--- a/net/sctp/stream_interleave.c
+++ b/net/sctp/stream_interleave.c
@@ -1255,7 +1255,7 @@ static void sctp_intl_skip(struct sctp_ulpq *ulpq, __u16 sid, __u32 mid,
struct sctp_stream *stream = &ulpq->asoc->stream;
if (flags & SCTP_FTSN_U_BIT) {
- if (sin->pd_mode_uo && MID_lt(sin->mid_uo, mid)) {
+ if (sin->pd_mode_uo && !MID_lt(mid, sin->mid_uo)) {
sin->pd_mode_uo = 0;
sctp_intl_stream_abort_pd(ulpq, sid, mid, 0x1,
GFP_ATOMIC);
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v2 2/4] sctp: report the partially delivered MID in PARTIAL_DELIVERY_ABORTED
2026-11-05 5:02 [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving Tim Budding
2026-11-05 5:02 ` [PATCH net v2 1/4] sctp: abort unordered partial delivery when its message is skipped Tim Budding
@ 2026-11-05 5:02 ` Tim Budding
2026-11-05 5:02 ` [PATCH net v2 3/4] sctp: fix I-FORWARD-TSN handling of fragments on other streams Tim Budding
2026-11-05 5:02 ` [PATCH net v2 4/4] selftests: net: add SCTP I-FORWARD-TSN tests with message interleaving Tim Budding
3 siblings, 0 replies; 5+ messages in thread
From: Tim Budding @ 2026-11-05 5:02 UTC (permalink / raw)
To: netdev, linux-sctp
Cc: lucien.xin, marcelo.leitner, davem, edumazet, kuba, pabeni,
horms, shuah, linux-kselftest, linux-kernel, Tim Budding, stable
When an I-FORWARD-TSN skips the message being partially delivered on a
stream, sctp_intl_skip() passes the MID from the I-FORWARD-TSN to the
PARTIAL_DELIVERY_ABORTED notification as pdapi_seq. That MID is the
highest one skipped on the stream, so when more than one message is
skipped, the notification names a later message instead of the one
whose partial delivery was aborted.
Report the MID being partially delivered, sin->mid_uo or sin->mid,
instead.
Reproduced on an unmodified kernel with the pdapi and pdapi_uo tests of
the sctp_ifwdtsn.sh selftest added later in this series, which pass
with this fix. Reproduction and fix done with AI assistance.
Fixes: de60fe910543 ("sctp: implement handle_ftsn for sctp_stream_interleave")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/179293012867.3134.16497481637722705271@kernel.org/
Cc: stable@vger.kernel.org
Signed-off-by: Tim Budding <tim.budding@gmail.com>
---
net/sctp/stream_interleave.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/net/sctp/stream_interleave.c b/net/sctp/stream_interleave.c
index 869940001588..e239da686f68 100644
--- a/net/sctp/stream_interleave.c
+++ b/net/sctp/stream_interleave.c
@@ -1257,7 +1257,7 @@ static void sctp_intl_skip(struct sctp_ulpq *ulpq, __u16 sid, __u32 mid,
if (flags & SCTP_FTSN_U_BIT) {
if (sin->pd_mode_uo && !MID_lt(mid, sin->mid_uo)) {
sin->pd_mode_uo = 0;
- sctp_intl_stream_abort_pd(ulpq, sid, mid, 0x1,
+ sctp_intl_stream_abort_pd(ulpq, sid, sin->mid_uo, 0x1,
GFP_ATOMIC);
}
return;
@@ -1268,7 +1268,7 @@ static void sctp_intl_skip(struct sctp_ulpq *ulpq, __u16 sid, __u32 mid,
if (sin->pd_mode) {
sin->pd_mode = 0;
- sctp_intl_stream_abort_pd(ulpq, sid, mid, 0x0, GFP_ATOMIC);
+ sctp_intl_stream_abort_pd(ulpq, sid, sin->mid, 0x0, GFP_ATOMIC);
}
sctp_mid_skip(stream, in, sid, mid);
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v2 3/4] sctp: fix I-FORWARD-TSN handling of fragments on other streams
2026-11-05 5:02 [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving Tim Budding
2026-11-05 5:02 ` [PATCH net v2 1/4] sctp: abort unordered partial delivery when its message is skipped Tim Budding
2026-11-05 5:02 ` [PATCH net v2 2/4] sctp: report the partially delivered MID in PARTIAL_DELIVERY_ABORTED Tim Budding
@ 2026-11-05 5:02 ` Tim Budding
2026-11-05 5:02 ` [PATCH net v2 4/4] selftests: net: add SCTP I-FORWARD-TSN tests with message interleaving Tim Budding
3 siblings, 0 replies; 5+ messages in thread
From: Tim Budding @ 2026-11-05 5:02 UTC (permalink / raw)
To: netdev, linux-sctp
Cc: lucien.xin, marcelo.leitner, davem, edumazet, kuba, pabeni,
horms, shuah, linux-kselftest, linux-kernel, Tim Budding, stable
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@vger.kernel.org
Signed-off-by: Tim Budding <tim.budding@gmail.com>
---
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
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v2 4/4] selftests: net: add SCTP I-FORWARD-TSN tests with message interleaving
2026-11-05 5:02 [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving Tim Budding
` (2 preceding siblings ...)
2026-11-05 5:02 ` [PATCH net v2 3/4] sctp: fix I-FORWARD-TSN handling of fragments on other streams Tim Budding
@ 2026-11-05 5:02 ` Tim Budding
3 siblings, 0 replies; 5+ messages in thread
From: Tim Budding @ 2026-11-05 5:02 UTC (permalink / raw)
To: netdev, linux-sctp
Cc: lucien.xin, marcelo.leitner, davem, edumazet, kuba, pabeni,
horms, shuah, linux-kselftest, linux-kernel, Tim Budding
Add tests for the receiver side of I-FORWARD-TSN with user message
interleaving (RFC 8260) and partial reliability. Most tests interleave
a 64KB reliable message on stream 0 with a PR-SCTP message on stream 1
that nft drops, so the sender abandons it, and check that the large
message is delivered intact and nothing is left queued. Others cover
partial delivery aborts, including the MID reported in
PARTIAL_DELIVERY_ABORTED, and SSN/TSN association resets requested by
either side.
Signed-off-by: Tim Budding <tim.budding@gmail.com>
---
tools/testing/selftests/net/.gitignore | 1 +
tools/testing/selftests/net/Makefile | 2 +
tools/testing/selftests/net/sctp_ifwdtsn.c | 699 ++++++++++++++++++++
tools/testing/selftests/net/sctp_ifwdtsn.sh | 233 +++++++
4 files changed, 935 insertions(+)
create mode 100644 tools/testing/selftests/net/sctp_ifwdtsn.c
create mode 100755 tools/testing/selftests/net/sctp_ifwdtsn.sh
diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore
index 21412510ac26..ca451bfeda75 100644
--- a/tools/testing/selftests/net/.gitignore
+++ b/tools/testing/selftests/net/.gitignore
@@ -34,6 +34,7 @@ reuseport_bpf_numa
reuseport_dualstack
rxtimestamp
sctp_hello
+sctp_ifwdtsn
sk_bind_sendto_listen
sk_connect_zero_addr
sk_so_peek_off
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 9180b9bec515..85f51a30c6a1 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -90,6 +90,7 @@ TEST_PROGS := \
run_afpackettests \
run_netsocktests \
rxtimestamp.sh \
+ sctp_ifwdtsn.sh \
sctp_vrf.sh \
skf_net_off.sh \
srv6_encap_lookup_l3vpn_test.sh \
@@ -163,6 +164,7 @@ TEST_GEN_FILES := \
reuseport_addr_any \
rxtimestamp \
sctp_hello \
+ sctp_ifwdtsn \
skf_net_off \
so_netns_cookie \
so_rcv_listener \
diff --git a/tools/testing/selftests/net/sctp_ifwdtsn.c b/tools/testing/selftests/net/sctp_ifwdtsn.c
new file mode 100644
index 000000000000..23ae508d8523
--- /dev/null
+++ b/tools/testing/selftests/net/sctp_ifwdtsn.c
@@ -0,0 +1,699 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Helper for sctp_ifwdtsn.sh: I-FORWARD-TSN handling with SCTP user message
+ * interleaving (RFC 8260) and partial reliability (RFC 3758).
+ *
+ * Runs both endpoints of one association: the server socket is created in
+ * the current network namespace, the client socket in the namespace given
+ * on the command line. The caller sets up the link and any nft rule that
+ * drops chunks. Messages sent by the client:
+ *
+ * big: 64KB on stream 0, reliable, fragmented into ~66 I-DATA chunks
+ * small: on stream 1, PR-SCTP "RTX" policy allowing one retransmission;
+ * the nft rule drops (part of) it, so it gets abandoned
+ * marker: short ordered message on stream 0
+ *
+ * The reset_pending tests instead leave a partly received PR-SCTP message
+ * on each stream, the one on stream 1 in partial delivery, before the
+ * reset; the pdapi tests abandon two messages on stream 1, the first one
+ * in partial delivery.
+ *
+ * Usage: sctp_ifwdtsn <client netns> <test>
+ * Exit status: 0 pass, 1 fail, 4 skip.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <linux/sctp.h>
+
+#include "kselftest.h"
+
+#ifndef SOL_SCTP
+#define SOL_SCTP 132
+#endif
+
+#define SRV_ADDR "10.0.0.1"
+#define CLI_ADDR "10.0.0.2"
+#define SRV_PORT 5000
+#define BIG_LEN (64 * 1024)
+#define BUF_LEN (BIG_LEN + 1024)
+#define MAXSEG 1000
+#define TIMEOUT_MS 5000
+#define MARKER "marker"
+#define UO_MARKER "uo-marker"
+#define PD_LEN 20000 /* message in partial delivery on stream 1 */
+
+struct test {
+ const char *name;
+ bool big_uo; /* send big unordered */
+ int small_len; /* 0: don't send the small message */
+ bool small_uo; /* send small unordered */
+ int pd_point; /* server SCTP_PARTIAL_DELIVERY_POINT */
+ bool expect_pd_abort; /* PD on stream 1 must be aborted */
+ bool uo_marker; /* then send an unordered marker on 1 */
+ bool reset; /* SSN/TSN association reset after big */
+ int reset_pending; /* reset with data pending, requested by
+ * 1: the server, 2: the client
+ */
+ bool pdapi; /* two messages skipped on stream 1 */
+};
+
+static const struct test tests[] = {
+ { .name = "ordered", .small_len = 3000 },
+ { .name = "unordered", .big_uo = true, .small_len = 3000 },
+ { .name = "leading_edge", .small_len = 1000, .pd_point = 2000 },
+ { .name = "leading_edge_nopd", .small_len = 1000 },
+ { .name = "uo_pd", .small_len = 20000, .small_uo = true,
+ .pd_point = 2000, .expect_pd_abort = true, .uo_marker = true },
+ { .name = "reset", .reset = true },
+ { .name = "reset_pending_srv", .reset = true, .reset_pending = 1,
+ .pd_point = 3000, .expect_pd_abort = true },
+ { .name = "reset_pending_cli", .reset = true, .reset_pending = 2,
+ .pd_point = 3000, .expect_pd_abort = true },
+ { .name = "pdapi", .pdapi = true, .pd_point = 2000,
+ .expect_pd_abort = true },
+ { .name = "pdapi_uo", .pdapi = true, .small_uo = true, .pd_point = 2000,
+ .expect_pd_abort = true },
+};
+
+static const struct test *t;
+
+static void die(const char *what)
+{
+ fprintf(stderr, "%s: %s\n", what, strerror(errno));
+ exit(KSFT_FAIL);
+}
+
+static void setopt(int fd, int opt, const void *val, socklen_t len,
+ const char *name)
+{
+ if (setsockopt(fd, SOL_SCTP, opt, val, len) < 0) {
+ fprintf(stderr, "setsockopt(%s): %s\n", name, strerror(errno));
+ /* Interleaving disabled, or option not supported. */
+ exit(errno == EPERM || errno == ENOPROTOOPT ?
+ KSFT_SKIP : KSFT_FAIL);
+ }
+}
+
+static void set_assoc_opt(int fd, int opt, unsigned int val, const char *name)
+{
+ struct sctp_assoc_value av = {
+ .assoc_id = SCTP_FUTURE_ASSOC,
+ .assoc_value = val,
+ };
+
+ setopt(fd, opt, &av, sizeof(av), name);
+}
+
+static void subscribe(int fd, int type)
+{
+ struct sctp_event ev = {
+ .se_assoc_id = SCTP_FUTURE_ASSOC,
+ .se_type = type,
+ .se_on = 1,
+ };
+
+ setopt(fd, SCTP_EVENT, &ev, sizeof(ev), "SCTP_EVENT");
+}
+
+static int sctp_socket(void)
+{
+ struct sctp_initmsg im = {
+ .sinit_num_ostreams = 2,
+ .sinit_max_instreams = 2,
+ };
+ struct sctp_paddrparams pp = {
+ .spp_assoc_id = SCTP_FUTURE_ASSOC,
+ .spp_flags = SPP_HB_DISABLE,
+ };
+ struct sctp_rtoinfo rto = {
+ .srto_assoc_id = SCTP_FUTURE_ASSOC,
+ .srto_initial = 300,
+ .srto_min = 300,
+ .srto_max = 1000,
+ };
+ int fd, val;
+
+ fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
+ if (fd < 0) {
+ perror("socket");
+ exit(errno == EPROTONOSUPPORT ? KSFT_SKIP : KSFT_FAIL);
+ }
+ val = 1;
+ setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
+
+ setopt(fd, SCTP_INITMSG, &im, sizeof(im), "SCTP_INITMSG");
+ setopt(fd, SCTP_RTOINFO, &rto, sizeof(rto), "SCTP_RTOINFO");
+ /* No HEARTBEATs: the nft rules only look at the first chunk of a
+ * packet, and a HEARTBEAT bundled in front of DATA would bypass them.
+ */
+ setopt(fd, SCTP_PEER_ADDR_PARAMS, &pp, sizeof(pp),
+ "SCTP_PEER_ADDR_PARAMS");
+ set_assoc_opt(fd, SCTP_PR_SUPPORTED, 1, "SCTP_PR_SUPPORTED");
+ val = 2;
+ setopt(fd, SCTP_FRAGMENT_INTERLEAVE, &val, sizeof(val),
+ "SCTP_FRAGMENT_INTERLEAVE");
+ set_assoc_opt(fd, SCTP_INTERLEAVING_SUPPORTED, 1,
+ "SCTP_INTERLEAVING_SUPPORTED");
+ if (t->reset) {
+ set_assoc_opt(fd, SCTP_RECONFIG_SUPPORTED, 1,
+ "SCTP_RECONFIG_SUPPORTED");
+ set_assoc_opt(fd, SCTP_ENABLE_STREAM_RESET,
+ SCTP_ENABLE_RESET_ASSOC_REQ,
+ "SCTP_ENABLE_STREAM_RESET");
+ subscribe(fd, SCTP_ASSOC_RESET_EVENT);
+ }
+ val = 1;
+ setopt(fd, SCTP_RECVRCVINFO, &val, sizeof(val), "SCTP_RECVRCVINFO");
+
+ return fd;
+}
+
+static void send_msg(int fd, int sid, bool uo, const void *buf, size_t len,
+ bool pr)
+{
+ union {
+ char buf[CMSG_SPACE(sizeof(struct sctp_sndinfo)) +
+ CMSG_SPACE(sizeof(struct sctp_prinfo))];
+ struct cmsghdr align;
+ } cbuf = {};
+ struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf.buf,
+ .msg_controllen = CMSG_SPACE(sizeof(struct sctp_sndinfo)),
+ };
+ struct sctp_sndinfo *si;
+ struct sctp_prinfo *pi;
+ struct cmsghdr *cm;
+
+ cm = CMSG_FIRSTHDR(&msg);
+ cm->cmsg_level = IPPROTO_SCTP;
+ cm->cmsg_type = SCTP_SNDINFO;
+ cm->cmsg_len = CMSG_LEN(sizeof(*si));
+ si = (struct sctp_sndinfo *)CMSG_DATA(cm);
+ si->snd_sid = sid;
+ si->snd_flags = uo ? SCTP_UNORDERED : 0;
+
+ if (pr) {
+ /* Abandon once the chunk would need a second retransmission:
+ * only a chunk that is lost again after being retransmitted
+ * qualifies, so abandonment does not depend on timing.
+ */
+ msg.msg_controllen += CMSG_SPACE(sizeof(*pi));
+ cm = CMSG_NXTHDR(&msg, cm);
+ cm->cmsg_level = IPPROTO_SCTP;
+ cm->cmsg_type = SCTP_PRINFO;
+ cm->cmsg_len = CMSG_LEN(sizeof(*pi));
+ pi = (struct sctp_prinfo *)CMSG_DATA(cm);
+ pi->pr_policy = SCTP_PR_SCTP_RTX;
+ pi->pr_value = 1;
+ }
+
+ if (sendmsg(fd, &msg, 0) != (ssize_t)len)
+ die("sendmsg");
+}
+
+static unsigned char pattern(size_t i)
+{
+ return (i * 7 + (i >> 8)) & 0xff;
+}
+
+static long long now_ms(void)
+{
+ struct timespec ts;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000;
+}
+
+/* Wait until the client has abandoned @n sent messages. */
+static bool wait_abandoned(int fd, unsigned int n)
+{
+ long long end = now_ms() + TIMEOUT_MS;
+ struct sctp_prstatus ps;
+ socklen_t len;
+
+ while (now_ms() < end) {
+ memset(&ps, 0, sizeof(ps));
+ ps.sprstat_policy = SCTP_PR_SCTP_RTX;
+ len = sizeof(ps);
+ if (getsockopt(fd, SOL_SCTP, SCTP_PR_ASSOC_STATUS, &ps,
+ &len) < 0)
+ die("SCTP_PR_ASSOC_STATUS");
+ if (ps.sprstat_abandoned_sent >= n)
+ return true;
+ usleep(10000);
+ }
+ return false;
+}
+
+/* Wait for a notification of @type on @fd, discarding anything else. */
+static bool wait_event(int fd, int type)
+{
+ long long end = now_ms() + TIMEOUT_MS;
+ union {
+ char data[256];
+ union sctp_notification sn;
+ } buf;
+
+ for (;;) {
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ struct iovec iov = { buf.data, sizeof(buf.data) };
+ struct msghdr msg = { .msg_iov = &iov, .msg_iovlen = 1 };
+ union sctp_notification *sn = &buf.sn;
+ long long left = end - now_ms();
+
+ if (left <= 0 || poll(&pfd, 1, left) <= 0)
+ break;
+ if (recvmsg(fd, &msg, 0) <= 0)
+ break;
+ if (!(msg.msg_flags & MSG_NOTIFICATION) ||
+ sn->sn_header.sn_type != type)
+ continue;
+ if (type == SCTP_ASSOC_RESET_EVENT &&
+ sn->sn_assocreset_event.assocreset_flags &
+ (SCTP_ASSOC_RESET_DENIED | SCTP_ASSOC_RESET_FAILED)) {
+ printf("association reset denied or failed\n");
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+/* Request an SSN/TSN association reset. The sender refuses while it still
+ * has data queued or in flight, so retry until that has been acked.
+ */
+static void reset_assoc(int fd)
+{
+ long long end = now_ms() + TIMEOUT_MS;
+ sctp_assoc_t id = 0;
+
+ while (setsockopt(fd, SOL_SCTP, SCTP_RESET_ASSOC, &id, sizeof(id))) {
+ if (errno != EAGAIN || now_ms() >= end)
+ die("SCTP_RESET_ASSOC");
+ usleep(10000);
+ }
+}
+
+/* Wait until all data sent on @fd has been acked. The receiver's
+ * cumulative TSN only moves past an abandoned message once it has
+ * processed the I-FORWARD-TSN for it.
+ */
+static bool wait_acked(int fd)
+{
+ long long end = now_ms() + TIMEOUT_MS;
+ struct sctp_status st;
+ socklen_t len;
+
+ while (now_ms() < end) {
+ memset(&st, 0, sizeof(st));
+ len = sizeof(st);
+ /* Fails once the association has been aborted. */
+ if (getsockopt(fd, SOL_SCTP, SCTP_STATUS, &st, &len) < 0)
+ return false;
+ if (!st.sstat_unackdata)
+ return true;
+ usleep(10000);
+ }
+ return false;
+}
+
+/* Bytes held by the server association (/proc/net/sctp/assocs RX_QUEUE). */
+static long assoc_rx_queue(void)
+{
+ /* 7 columns, TX_QUEUE, RX_QUEUE, UID, INODE, LPORT */
+ const char *fmt = "%*s %*s %*s %*s %*s %*s %*s %*u %lu %*s %*s %d";
+ unsigned long rxq;
+ char line[512];
+ long ret = -1;
+ FILE *f;
+ int lport;
+
+ f = fopen("/proc/net/sctp/assocs", "r");
+ if (!f)
+ return -1;
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, fmt, &rxq, &lport) == 2 && lport == SRV_PORT)
+ ret = rxq;
+ }
+ fclose(f);
+ return ret;
+}
+
+struct server_state {
+ unsigned char *buf[2]; /* message being received, per stream */
+ size_t len[2];
+ bool big, marker, uo_marker, reset_event;
+ int pd_aborts[2];
+ unsigned int pd_seq[2]; /* pdapi_seq of the last abort */
+ int errors;
+};
+
+static void check_msg(struct server_state *s, int sid)
+{
+ unsigned char *m = s->buf[sid];
+ size_t len = s->len[sid];
+ size_t i;
+
+ s->len[sid] = 0;
+ if (sid == 0 && len == sizeof(MARKER) && !memcmp(m, MARKER, len)) {
+ s->marker = true;
+ return;
+ }
+ if (sid == 1 && len == sizeof(UO_MARKER) &&
+ !memcmp(m, UO_MARKER, len)) {
+ s->uo_marker = true;
+ return;
+ }
+ if (sid == 0 && len == BIG_LEN) {
+ for (i = 0; i < BIG_LEN && m[i] == pattern(i); i++)
+ ;
+ if (i == BIG_LEN) {
+ s->big = true;
+ return;
+ }
+ printf("big message corrupted at offset %zu\n", i);
+ } else if (sid == 1 && len > sizeof(UO_MARKER) &&
+ !memcmp(m + len - sizeof(UO_MARKER), UO_MARKER,
+ sizeof(UO_MARKER))) {
+ printf("unordered marker appended to the abandoned message\n");
+ } else if (sid == 1 && t->small_len) {
+ printf("abandoned message delivered (%zu bytes)\n", len);
+ } else {
+ printf("unexpected %zu byte message on stream %d\n", len, sid);
+ }
+ s->errors++;
+}
+
+/* Receive until @done(s) is true or the timeout expires. */
+static bool server_recv(int fd, struct server_state *s,
+ bool (*done)(const struct server_state *s))
+{
+ long long end = now_ms() + TIMEOUT_MS;
+ static union {
+ char buf[CMSG_SPACE(sizeof(struct sctp_rcvinfo))];
+ struct cmsghdr align;
+ } cbuf;
+ static union {
+ unsigned char data[BUF_LEN];
+ union sctp_notification sn;
+ } buf;
+
+ while (!done(s)) {
+ struct pollfd pfd = { .fd = fd, .events = POLLIN };
+ struct iovec iov = { buf.data, sizeof(buf.data) };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf.buf,
+ .msg_controllen = sizeof(cbuf.buf),
+ };
+ union sctp_notification *sn = &buf.sn;
+ struct sctp_rcvinfo *ri = NULL;
+ struct cmsghdr *cm;
+ long long left;
+ ssize_t n;
+ int sid;
+
+ left = end - now_ms();
+ if (left <= 0 || poll(&pfd, 1, left) <= 0) {
+ printf("timeout waiting for data\n");
+ return false;
+ }
+ n = recvmsg(fd, &msg, 0);
+ if (n <= 0) {
+ printf("recvmsg: %s\n", n ? strerror(errno) : "EOF");
+ return false;
+ }
+
+ if (msg.msg_flags & MSG_NOTIFICATION) {
+ int type = sn->sn_header.sn_type;
+
+ if (type == SCTP_PARTIAL_DELIVERY_EVENT) {
+ struct sctp_pdapi_event *pe = &sn->sn_pdapi_event;
+
+ sid = pe->pdapi_stream;
+ if (sid > 1 || pe->pdapi_indication !=
+ SCTP_PARTIAL_DELIVERY_ABORTED) {
+ printf("unexpected pdapi event %u on stream %d\n",
+ pe->pdapi_indication, sid);
+ return false;
+ }
+ s->pd_aborts[sid]++;
+ s->pd_seq[sid] = pe->pdapi_seq;
+ s->len[sid] = 0;
+ } else if (type == SCTP_ASSOC_RESET_EVENT) {
+ s->reset_event = true;
+ }
+ continue;
+ }
+
+ for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm))
+ if (cm->cmsg_level == IPPROTO_SCTP &&
+ cm->cmsg_type == SCTP_RCVINFO)
+ ri = (struct sctp_rcvinfo *)CMSG_DATA(cm);
+ sid = ri ? ri->rcv_sid : 0;
+ if (sid > 1 || s->len[sid] + n > BUF_LEN) {
+ printf("unexpected data on stream %d\n", sid);
+ return false;
+ }
+ memcpy(s->buf[sid] + s->len[sid], buf.data, n);
+ s->len[sid] += n;
+ if (msg.msg_flags & MSG_EOR)
+ check_msg(s, sid);
+ }
+ return true;
+}
+
+static bool got_reset(const struct server_state *s)
+{
+ return s->reset_event || s->errors;
+}
+
+static bool got_big(const struct server_state *s)
+{
+ return s->big || s->errors;
+}
+
+static bool got_all(const struct server_state *s)
+{
+ return ((s->big || t->pdapi) && s->marker &&
+ (!t->uo_marker || s->uo_marker) &&
+ (!t->expect_pd_abort || s->pd_aborts[1])) || s->errors;
+}
+
+/* Let the nft rules that match the client address in set "hold" pass. */
+static bool release_hold(void)
+{
+ if (system("nft delete element ip sctp_test hold { " CLI_ADDR " }")) {
+ printf("failed to release the nft hold\n");
+ return false;
+ }
+ return true;
+}
+
+static int client_socket(const char *netns)
+{
+ int self, other, fd;
+ char path[256];
+
+ snprintf(path, sizeof(path), "/run/netns/%s", netns);
+ self = open("/proc/self/ns/net", O_RDONLY);
+ other = open(path, O_RDONLY);
+ if (self < 0 || other < 0)
+ die("open netns");
+ if (setns(other, CLONE_NEWNET))
+ die("setns");
+ fd = sctp_socket();
+ if (setns(self, CLONE_NEWNET))
+ die("setns");
+ close(other);
+ close(self);
+ return fd;
+}
+
+int main(int argc, char **argv)
+{
+ struct sockaddr_in sa = {
+ .sin_family = AF_INET,
+ .sin_port = htons(SRV_PORT),
+ };
+ struct server_state s = {};
+ unsigned char *big, *small;
+ int lfd, sfd, cfd, i;
+ bool ok;
+ long rxq;
+
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <client netns> <test>\n", argv[0]);
+ return KSFT_FAIL;
+ }
+ for (i = 0; i < (int)ARRAY_SIZE(tests); i++)
+ if (!strcmp(argv[2], tests[i].name))
+ t = &tests[i];
+ if (!t) {
+ fprintf(stderr, "unknown test %s\n", argv[2]);
+ return KSFT_FAIL;
+ }
+ inet_pton(AF_INET, SRV_ADDR, &sa.sin_addr);
+
+ lfd = sctp_socket();
+ if (t->pd_point) {
+ setopt(lfd, SCTP_PARTIAL_DELIVERY_POINT, &t->pd_point,
+ sizeof(t->pd_point), "SCTP_PARTIAL_DELIVERY_POINT");
+ subscribe(lfd, SCTP_PARTIAL_DELIVERY_EVENT);
+ }
+ if (bind(lfd, (struct sockaddr *)&sa, sizeof(sa)) || listen(lfd, 1))
+ die("bind/listen");
+
+ cfd = client_socket(argv[1]);
+ set_assoc_opt(cfd, SCTP_MAXSEG, MAXSEG, "SCTP_MAXSEG");
+ /* Round robin, so that the small message is sent between fragments
+ * of the big one.
+ */
+ set_assoc_opt(cfd, SCTP_STREAM_SCHEDULER, SCTP_SS_RR,
+ "SCTP_STREAM_SCHEDULER");
+ if (t->pdapi) {
+ /* Send the marker right away. Held back by Nagle, it could
+ * go out bundled behind an I-FORWARD-TSN, and nft would
+ * drop both.
+ */
+ i = 1;
+ setopt(cfd, SCTP_NODELAY, &i, sizeof(i), "SCTP_NODELAY");
+ }
+ if (connect(cfd, (struct sockaddr *)&sa, sizeof(sa)))
+ die("connect");
+ sfd = accept(lfd, NULL, NULL);
+ if (sfd < 0)
+ die("accept");
+
+ big = malloc(BIG_LEN);
+ small = malloc(PD_LEN);
+ s.buf[0] = malloc(BUF_LEN);
+ s.buf[1] = malloc(BUF_LEN);
+ if (!big || !small || !s.buf[0] || !s.buf[1])
+ die("malloc");
+ for (i = 0; i < BIG_LEN; i++)
+ big[i] = pattern(i);
+ memset(small, 's', PD_LEN);
+
+ if (t->reset_pending) {
+ /* A message partly received on stream 0, not in partial
+ * delivery, and one in partial delivery on stream 1. Until
+ * the hold is released, nft drops a fragment of each and
+ * every I-FORWARD-TSN, so both are still pending on the
+ * server when the reset is done.
+ */
+ send_msg(cfd, 0, false, small, 2500, true);
+ send_msg(cfd, 1, false, small, PD_LEN, true);
+ if (!wait_abandoned(cfd, 2)) {
+ printf("client: messages were not abandoned\n");
+ return KSFT_FAIL;
+ }
+ reset_assoc(t->reset_pending == 1 ? sfd : cfd);
+ if (!wait_event(cfd, SCTP_ASSOC_RESET_EVENT)) {
+ printf("client: no association reset event\n");
+ return KSFT_FAIL;
+ }
+ /* Once the server has seen the reset too, every packet the
+ * client sent before it has arrived.
+ */
+ if (!server_recv(sfd, &s, got_reset) || s.errors ||
+ !release_hold())
+ return KSFT_FAIL;
+ /* MIDs start again from 0, so big reuses the MID of the
+ * message left on stream 0 and must not be mixed with it.
+ */
+ send_msg(cfd, 0, false, big, BIG_LEN, false);
+ } else if (t->pdapi) {
+ /* Two messages on stream 1, the first one in partial
+ * delivery. nft drops a fragment of each and holds back
+ * I-FORWARD-TSNs until the marker, sent once both are
+ * abandoned, arrives. The client then sends one
+ * I-FORWARD-TSN naming both messages, below the marker's
+ * TSN.
+ */
+ send_msg(cfd, 1, t->small_uo, small, PD_LEN, true);
+ send_msg(cfd, 1, t->small_uo, small, 3000, true);
+ if (!wait_abandoned(cfd, 2)) {
+ printf("client: messages were not abandoned\n");
+ return KSFT_FAIL;
+ }
+ } else {
+ send_msg(cfd, 0, t->big_uo, big, BIG_LEN, false);
+ if (t->small_len)
+ send_msg(cfd, 1, t->small_uo, small, t->small_len,
+ true);
+ }
+
+ if (t->reset && !t->reset_pending) {
+ ok = server_recv(sfd, &s, got_big);
+ if (ok)
+ reset_assoc(sfd);
+ /* Only send the marker once the reset is done. */
+ if (ok && !wait_event(cfd, SCTP_ASSOC_RESET_EVENT)) {
+ printf("client: no association reset event\n");
+ return KSFT_FAIL;
+ }
+ }
+ send_msg(cfd, 0, false, MARKER, sizeof(MARKER), false);
+
+ if (t->small_len && !wait_abandoned(cfd, 1)) {
+ printf("client: small message was not abandoned\n");
+ return KSFT_FAIL;
+ }
+ if (t->uo_marker)
+ send_msg(cfd, 1, true, UO_MARKER, sizeof(UO_MARKER), false);
+
+ ok = server_recv(sfd, &s, got_all);
+ if (!wait_acked(cfd)) {
+ printf("client: data not acked\n");
+ ok = false;
+ }
+ rxq = assoc_rx_queue();
+
+ if (!s.big && !t->pdapi)
+ printf("big message not delivered intact\n");
+ if (!s.marker)
+ printf("marker not delivered\n");
+ if (t->uo_marker && !s.uo_marker)
+ printf("unordered marker not delivered as its own message\n");
+ if (s.pd_aborts[0] || (s.pd_aborts[1] && !t->expect_pd_abort)) {
+ printf("unexpected partial delivery abort\n");
+ ok = false;
+ }
+ if (t->expect_pd_abort && !s.pd_aborts[1]) {
+ printf("partial delivery on stream 1 not aborted\n");
+ ok = false;
+ } else if (t->expect_pd_abort && (s.pd_aborts[1] > 1 || s.pd_seq[1])) {
+ printf("%d partial delivery aborts, last for MID %u, expected one for MID 0\n",
+ s.pd_aborts[1], s.pd_seq[1]);
+ ok = false;
+ }
+ if (t->reset && !s.reset_event) {
+ printf("server: no association reset event\n");
+ ok = false;
+ }
+ if (rxq < 0) {
+ printf("association is gone\n");
+ ok = false;
+ } else if (rxq > 0) {
+ printf("%ld bytes left queued on the association\n", rxq);
+ ok = false;
+ }
+
+ return ok && !s.errors ? KSFT_PASS : KSFT_FAIL;
+}
diff --git a/tools/testing/selftests/net/sctp_ifwdtsn.sh b/tools/testing/selftests/net/sctp_ifwdtsn.sh
new file mode 100755
index 000000000000..e745d67b3b79
--- /dev/null
+++ b/tools/testing/selftests/net/sctp_ifwdtsn.sh
@@ -0,0 +1,233 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# SCTP I-FORWARD-TSN handling with user message interleaving (RFC 8260).
+#
+# TOPO: CLI_NS (veth, 10.0.0.2) <---> (veth, 10.0.0.1) SRV_NS
+#
+# A large reliable message on stream 0 is sent interleaved with a PR-SCTP
+# message on stream 1 that an nft rule in SRV_NS drops, so the sender
+# abandons it and sends an I-FORWARD-TSN. The netem delay keeps the sender
+# window-limited, so the two messages' fragments interleave. Most of the
+# checks are done by sctp_ifwdtsn, see there.
+
+source lib.sh
+
+ALL_TESTS="
+ test_ordered
+ test_unordered
+ test_leading_edge
+ test_leading_edge_nopd
+ test_uo_pd
+ test_reset
+ test_reset_pending_srv
+ test_reset_pending_cli
+ test_pdapi
+ test_pdapi_uo
+"
+
+# nft matches on the first chunk of a packet: chunk type at byte 12 of the
+# SCTP header, flags at byte 13, I-DATA stream id at byte 20, MID at byte
+# 24 and FSN at byte 28 (the PPID in a B fragment). SCTP_MAXSEG limits
+# fragments to 1000 bytes and GSO is limited to the MTU, so no two
+# full-size fragments share a packet.
+NFT_SCTP="meta l4proto sctp"
+NFT_CHUNK_IDATA="@th,96,8 64"
+NFT_CHUNK_IFWDTSN="@th,96,8 194"
+NFT_FLAG_B="@th,104,8 & 0x02 == 0x02"
+NFT_FLAG_E="@th,104,8 & 0x01 == 0x01"
+NFT_SID0="@th,160,16 0"
+NFT_SID1="@th,160,16 1"
+NFT_MID0="@th,192,32 0"
+NFT_MID1="@th,192,32 1"
+NFT_FSN5="@th,224,32 5"
+
+# Run a setup command. The config file enables everything used here, so a
+# failure fails the test, with the command and its output as the reason.
+setup_cmd()
+{
+ local out
+
+ out=$("$@" 2>&1) && return
+ check_err 1 "$*: $out"
+ return 1
+}
+
+setup()
+{
+ if ! setup_ns CLI_NS SRV_NS; then
+ check_err 1 "setup_ns failed"
+ return 1
+ fi
+ defer cleanup_all_ns
+
+ setup_cmd ip -n "$SRV_NS" link add veth0 type veth peer name veth0 \
+ netns "$CLI_NS" || return
+ setup_cmd ip -n "$SRV_NS" addr add 10.0.0.1/24 dev veth0 || return
+ setup_cmd ip -n "$CLI_NS" addr add 10.0.0.2/24 dev veth0 || return
+ for ns in "$SRV_NS" "$CLI_NS"; do
+ setup_cmd ip -n "$ns" link set veth0 gso_max_size 1500 up ||
+ return
+ setup_cmd tc -n "$ns" qdisc add dev veth0 root netem \
+ delay 50ms || return
+ setup_cmd ip netns exec "$ns" sysctl -wq \
+ net.sctp.intl_enable=1 net.sctp.prsctp_enable=1 \
+ net.sctp.reconf_enable=1 || return
+ done
+}
+
+# $1: nft rules for the server's input hook (may be empty)
+nft_rules()
+{
+ setup_cmd ip netns exec "$SRV_NS" nft -f - <<-EOF
+ table ip sctp_test {
+ set blk {
+ type ipv4_addr
+ flags dynamic
+ }
+ set hold {
+ type ipv4_addr
+ flags dynamic
+ elements = { 10.0.0.2 }
+ }
+ chain input {
+ type filter hook input priority 0;
+ $1
+ }
+ }
+ EOF
+}
+
+# $1: test name for sctp_ifwdtsn, $2: description, $3: nft rules (optional)
+run_helper()
+{
+ RET=0
+ retmsg=
+ if ! setup || { [ -n "$3" ] && ! nft_rules "$3"; }; then
+ log_test "$2"
+ return
+ fi
+ ip netns exec "$SRV_NS" ./sctp_ifwdtsn "$CLI_NS" "$1"
+ RET=$?
+ [ "$RET" -eq "$ksft_skip" ] || [ "$RET" -eq 0 ] || RET=$ksft_fail
+ log_test "$2"
+}
+
+# Fragments of the abandoned message must not take fragments of a live
+# message below the New Cumulative TSN with them, and the abandoned
+# message's own received fragments must be freed.
+test_ordered()
+{
+ run_helper ordered \
+ "abandoned message does not purge ordered message" \
+ "$NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 $NFT_FLAG_B drop"
+}
+
+test_unordered()
+{
+ run_helper unordered \
+ "abandoned message does not purge unordered message" \
+ "$NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 $NFT_FLAG_B drop"
+}
+
+# Drop the abandoned message and every packet after it until one starting
+# with the I-FORWARD-TSN arrives, so that its New Cumulative TSN is the
+# highest TSN received. This must not abort the partial delivery of the
+# message on stream 0.
+test_leading_edge()
+{
+ run_helper leading_edge \
+ "I-FORWARD-TSN at the highest TSN is not a reset" "
+ $NFT_SCTP $NFT_CHUNK_IFWDTSN delete @blk { ip saddr }
+ $NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 add @blk { ip saddr } drop
+ $NFT_SCTP ip saddr @blk drop"
+}
+
+# The same without partial delivery: the fragments of the message on
+# stream 0 must not be freed.
+test_leading_edge_nopd()
+{
+ run_helper leading_edge_nopd \
+ "I-FORWARD-TSN at the highest TSN keeps other messages" "
+ $NFT_SCTP $NFT_CHUNK_IFWDTSN delete @blk { ip saddr }
+ $NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 add @blk { ip saddr } drop
+ $NFT_SCTP ip saddr @blk drop"
+}
+
+# Abandon an unordered message during its partial delivery: the partial
+# delivery must be aborted, and the next unordered message on the stream
+# must not be appended to it.
+test_uo_pd()
+{
+ run_helper uo_pd "abandoned unordered partial delivery is aborted" \
+ "$NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 $NFT_FSN5 drop"
+}
+
+# An SSN/TSN association reset with interleaving completes and leaves a
+# working association.
+test_reset()
+{
+ run_helper reset "association reset with interleaving"
+}
+
+# Drop a fragment of the message on each stream and every
+# I-FORWARD-TSN, so that both messages are left on the server when the
+# reset is done. The reset must abort the partial delivery on stream 1
+# and free both. sctp_ifwdtsn releases the hold after the reset, before
+# it sends a new message on stream 0 that reuses MID 0.
+NFT_PENDING="
+ $NFT_SCTP ip saddr @hold $NFT_CHUNK_IFWDTSN drop
+ $NFT_SCTP ip saddr @hold $NFT_CHUNK_IDATA $NFT_SID0 $NFT_FLAG_E drop
+ $NFT_SCTP ip saddr @hold $NFT_CHUNK_IDATA $NFT_SID1 $NFT_FSN5 drop"
+
+test_reset_pending_srv()
+{
+ run_helper reset_pending_srv \
+ "association reset by the server frees pending data" \
+ "$NFT_PENDING"
+}
+
+test_reset_pending_cli()
+{
+ run_helper reset_pending_cli \
+ "association reset by the client frees pending data" \
+ "$NFT_PENDING"
+}
+
+# Abandon two messages on a stream, the first one in partial delivery,
+# and let one I-FORWARD-TSN below the highest TSN received skip both. The
+# partial delivery abort must name the first message. I-FORWARD-TSNs are
+# dropped until the marker on stream 0, sent once both messages are
+# abandoned, arrives; any later one was sent after it.
+NFT_PDAPI="
+ $NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID0 delete @hold { ip saddr }
+ $NFT_SCTP ip saddr @hold $NFT_CHUNK_IFWDTSN drop
+ $NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 $NFT_MID0 $NFT_FSN5 drop
+ $NFT_SCTP $NFT_CHUNK_IDATA $NFT_SID1 $NFT_MID1 $NFT_FLAG_E drop"
+
+test_pdapi()
+{
+ run_helper pdapi "ordered partial delivery abort names its message" \
+ "$NFT_PDAPI"
+}
+
+test_pdapi_uo()
+{
+ run_helper pdapi_uo \
+ "unordered partial delivery abort names its message" \
+ "$NFT_PDAPI"
+}
+
+require_command nft
+require_command tc
+modprobe -q sctp
+if [ ! -d /proc/net/sctp ]; then
+ echo "SKIP: SCTP not available"
+ exit "$ksft_skip"
+fi
+
+trap defer_scopes_cleanup EXIT
+
+tests_run
+
+exit "$EXIT_STATUS"
--
2.34.1
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-05 5:02 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-05 5:02 [PATCH net v2 0/4] sctp: fix I-FORWARD-TSN handling with user message interleaving Tim Budding
2026-11-05 5:02 ` [PATCH net v2 1/4] sctp: abort unordered partial delivery when its message is skipped Tim Budding
2026-11-05 5:02 ` [PATCH net v2 2/4] sctp: report the partially delivered MID in PARTIAL_DELIVERY_ABORTED Tim Budding
2026-11-05 5:02 ` [PATCH net v2 3/4] sctp: fix I-FORWARD-TSN handling of fragments on other streams Tim Budding
2026-11-05 5:02 ` [PATCH net v2 4/4] selftests: net: add SCTP I-FORWARD-TSN tests with message interleaving Tim Budding
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®