[PATCH can-next v1 7/8] selftests: can: cover J1939 transport abort and timeout paths

From: Oleksij Rempel

Date: Wed Oct 14 2026 - 12:30:11 EST


Extend the multi-frame tests with the transport edge cases the round-trip
tests do not reach: a per-CTS block limit smaller than the message, the
RTS/CTS and rx timeouts that must end a stalled session with a Connection
Abort, and a received Connection Abort that cancels an incoming session.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@xxxxxxxxxxxxxx>
---
.../selftests/net/can/test_j1939_multi.c | 187 +++++++++++++++++-
1 file changed, 186 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/net/can/test_j1939_multi.c b/tools/testing/selftests/net/can/test_j1939_multi.c
index 1f27ee58f48c..069a9c8690b5 100644
--- a/tools/testing/selftests/net/can/test_j1939_multi.c
+++ b/tools/testing/selftests/net/can/test_j1939_multi.c
@@ -5,7 +5,18 @@
* A CAN_RAW socket plays the transport peer and drives the full handshake
* against the kernel J1939 stack over the wire, in both directions: for TP the
* raw side answers RTS/CTS and exchanges DT frames with EOMA; ETP does the same
- * with a payload spanning several CTS blocks (DPO windows).
+ * with a payload spanning several CTS blocks (DPO windows). The tests also
+ * cover the CTS block limit and the abort and timeout paths. Those edge cases
+ * are TP only; ETP has no equivalents here yet.
+ *
+ * Timing limitation: the abort/timeout tests wait with wide receive windows
+ * rather than tight bounds, because timing over vcan under QEMU is not tightly
+ * bounded. They catch a gross timer regression (e.g. a doubled timeout) but not
+ * a subtle shift; see the per-test comments.
+ *
+ * Coverage limitation: a received Connection Abort is exercised RX-side only;
+ * there is no kernel-as-transmitter received-abort test, matching the known
+ * net-next limitation that a TX session ignores an incoming Connection Abort.
*
* The vcan interface is set up by the caller (see test_j1939_multi.sh); its name
* is passed via the CANIF environment variable.
@@ -39,6 +50,14 @@
#define ETP_CMD_DPO 0x16
#define ETP_CMD_EOMA 0x17

+/* TP.Abort reason codes (not durations), from the kernel's enum j1939_xtp_abort */
+#define ABORT_REASON_TIMEOUT 3 /* J1939_XTP_ABORT_TIMEOUT */
+#define ABORT_REASON_BUSY 1 /* J1939_XTP_ABORT_BUSY */
+
+/* TP responder T2: wait for the next DT after CTS (ISO 11783-3:1998 §3.10.2.4). */
+#define TP_T2_MS 1250
+#define TP_RX_ABORT_WAIT_MS (TP_T2_MS * 2) /* generous; floor check is the real test */
+
#define TP_SIZE 200
#define ETP_SIZE 6000 /* > TP max 1785 (255*7); several blocks */

@@ -313,6 +332,69 @@ TEST_F(j1939_multi, tp_tx_to_raw)
EXPECT_EQ(memcmp(payload, rx_buf, sizeof(payload)), 0);
}

+/*
+ * TP RX, per-CTS block limit: the RTS advertises a "max packets per CTS"
+ * (RTS data byte 4, 0-based; the spec's byte 5) smaller than the total packet
+ * count. The kernel must never request more than that many packets in a
+ * single CTS.
+ */
+TEST_F(j1939_multi, tp_rx_cts_block_limit)
+{
+ const unsigned int limit = 3;
+ unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+ struct can_frame frame;
+ int max_want;
+
+ ASSERT_GT(num_pkts, limit)
+ TH_LOG("test needs more packets than the CTS limit");
+
+ /* advertise a max packets/CTS (limit) smaller than the total */
+ ASSERT_EQ(tp_rx_roundtrip(self, limit, &max_want, &frame), 0)
+ TH_LOG("TP RX round-trip against the raw peer failed");
+
+ EXPECT_LE(max_want, (int)limit)
+ TH_LOG("kernel CTS requested %d packets, sender limit was %u",
+ max_want, limit);
+}
+
+/*
+ * TP TX, RTS->CTS timeout: the J1939 socket starts a TP transmit; the raw peer
+ * receives the RTS but never answers with a CTS. The kernel must give up (spec
+ * T3 = 1250 ms, SAE J1939-21:2001 §5.12.3) and send a Connection Abort.
+ *
+ * The abort must arrive within 2000 ms (a doubled T3 would miss this window) but
+ * no sooner than 800 ms, safely below the 1250 ms timer, so an instant-fire
+ * regression (an abort emitted immediately on a zeroed timer) is caught too. The
+ * bounds are wide, not tight, to stay reliable over vcan, so a subtle shift
+ * (e.g. 1250->1800 ms) still escapes.
+ */
+TEST_F(j1939_multi, tp_tx_rts_cts_timeout)
+{
+ uint8_t payload[TP_SIZE];
+ struct timespec t0, t1;
+ struct can_frame frame;
+ long elapsed_ms;
+
+ fill_pattern(payload, sizeof(payload));
+
+ ASSERT_EQ(tx_start_session(&self->j1939, self->ifindex, payload,
+ sizeof(payload), self->raw, J1939_PF(TP_CTL),
+ TP_CMD_RTS, &frame), 0);
+
+ /* Deliberately send no CTS; expect the kernel to abort within 2000 ms. */
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+ ASSERT_EQ(recv_ctl_cmd(self->raw, J1939_PF(TP_CTL), SA_PEER, &frame,
+ 2000, TP_CMD_ABORT), 0)
+ TH_LOG("kernel did not send a Connection Abort within 2000 ms");
+ clock_gettime(CLOCK_MONOTONIC, &t1);
+ elapsed_ms = timespec_diff_ms(&t0, &t1);
+ /* must not fire instantly: the T3 timer has to run before the abort */
+ ASSERT_GE(elapsed_ms, 800)
+ TH_LOG("abort fired after only %ld ms, timer did not run", elapsed_ms);
+ /* the T3 expiry must be reported as a timeout abort */
+ EXPECT_EQ(frame.data[1], ABORT_REASON_TIMEOUT);
+}
+
/*
* ETP RX: the CAN_RAW peer transmits an ETP message to the J1939 socket.
* The kernel requests blocks with ETP.CTS; raw answers each with a DPO and
@@ -450,4 +532,107 @@ TEST_F(j1939_multi, etp_tx_to_raw)
free(rx_buf);
}

+/* Send a TP.CM_Abort control frame. */
+static int send_abort(int raw, uint8_t da, uint8_t sa, uint8_t reason)
+{
+ uint8_t dat[8];
+
+ ctl_dat_init(dat, TP_CMD_ABORT);
+ dat[1] = reason;
+ return send_ctl_frame(raw, PRIO, TP_CTL, da, sa, dat, PGN_DATA);
+}
+
+/* Open a TP RX j1939 socket, send a full RTS, consume the first CTS. */
+static int tp_rx_start_session(int *j1939, int raw, int ifindex,
+ unsigned int num_pkts, struct can_frame *cts)
+{
+ if (tp_rx_open_and_send_rts(j1939, raw, ifindex, num_pkts,
+ num_pkts /* no per-CTS cap */))
+ return -1;
+ return recv_ctl_cmd(raw, J1939_PF(TP_CTL), SA_PEER, cts,
+ J1939_RECV_TIMEOUT_MS, TP_CMD_CTS);
+}
+
+/*
+ * TP RX timeout: the CAN_RAW peer opens a session with RTS but never sends the
+ * data; the kernel's rx timer must abort the session and emit a TP.CM_Abort.
+ * The wait uses wide bounds (not a tight window) to stay reliable over vcan: the
+ * lower bound catches an instant-fire regression; the upper bound is a generous
+ * T2 multiple, so timer shifts within it escape.
+ */
+TEST_F(j1939_multi, tp_rx_timeout)
+{
+ unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+ struct timespec t0, t1;
+ struct can_frame frame;
+ long elapsed_ms;
+
+ ASSERT_EQ(tp_rx_start_session(&self->j1939, self->raw, self->ifindex,
+ num_pkts, &frame), 0)
+ TH_LOG("failed to open and start the TP rx session");
+
+ /*
+ * No data follows the CTS; the kernel's rx timer (T2, see TP_T2_MS) must
+ * fire and abort; the wait is a generous TP_T2_MS * 2, the floor below is
+ * the real check.
+ */
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+ ASSERT_EQ(recv_ctl_cmd(self->raw, J1939_PF(TP_CTL), SA_PEER, &frame,
+ TP_RX_ABORT_WAIT_MS, TP_CMD_ABORT), 0)
+ TH_LOG("kernel did not abort the stalled rx session");
+ clock_gettime(CLOCK_MONOTONIC, &t1);
+ elapsed_ms = timespec_diff_ms(&t0, &t1);
+ /*
+ * Must not fire instantly: the T2 timer (1250 ms) has to run before the
+ * abort, so 800 ms is a floor safely below it.
+ */
+ ASSERT_GE(elapsed_ms, 800)
+ TH_LOG("abort fired after only %ld ms, timer did not run", elapsed_ms);
+ EXPECT_EQ(frame.data[1], ABORT_REASON_TIMEOUT);
+}
+
+/*
+ * TP RX abort: the CAN_RAW peer aborts an open session; the kernel must cancel
+ * it and deliver nothing to the J1939 socket.
+ */
+TEST_F(j1939_multi, tp_rx_abort_from_raw)
+{
+ uint8_t payload[TP_SIZE];
+ unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+ struct can_frame frame;
+ struct pollfd pfd;
+ unsigned int i;
+
+ fill_pattern(payload, sizeof(payload));
+
+ ASSERT_EQ(tp_rx_start_session(&self->j1939, self->raw, self->ifindex,
+ num_pkts, &frame), 0)
+ TH_LOG("failed to open and start the TP rx session");
+
+ ASSERT_EQ(send_abort(self->raw, SA_LOCAL, SA_PEER, ABORT_REASON_BUSY), 0);
+ /* let the kernel process the abort first; 100 ms is a generous
+ * empirical margin, not a spec value.
+ */
+ usleep(100000);
+
+ /*
+ * Send the full DT burst anyway. A kernel that honoured the abort has
+ * torn the session down and drops these; one that ignored it would
+ * reassemble them and deliver, so POLLIN below means the abort was not
+ * honoured.
+ */
+ for (i = 1; i <= num_pkts; i++)
+ ASSERT_EQ(send_dt(self->raw, PRIO, TP_DAT, SA_LOCAL, SA_PEER,
+ i, payload, (i - 1) * 7, TP_SIZE), 0);
+
+ pfd.fd = self->j1939;
+ pfd.events = POLLIN;
+ /*
+ * Short negative-check window: nothing should arrive after the abort,
+ * so this is deliberately shorter than J1939_RECV_TIMEOUT_MS.
+ */
+ EXPECT_EQ(poll(&pfd, 1, J1939_NEG_RECV_TIMEOUT_MS), 0)
+ TH_LOG("socket delivered data despite the abort");
+}
+
J1939_TEST_MAIN();
--
2.47.3