Linux CAN drivers development
 help / color / mirror / Atom feed
* [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests
@ 2026-10-14 15:44 Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 1/8] selftests: can: add J1939 test helper header and library Oleksij Rempel
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

net/can/j1939 has had no in-tree selftests. This series adds a kselftest
suite for the J1939 socket layer and the TP/ETP transport, run on vcan with
a CAN_RAW socket as an independent on-the-wire peer.

The whole suite runs on vcan under QEMU and passes. Two limits span the
series and are left for follow-up: the transport timers are checked only
with wide windows, not tight bounds, and the ETP abort/timeout edge cases
are not covered yet (the abort/timeout tests are TP only).

Oleksij Rempel (8):
  selftests: can: add J1939 test helper header and library
  selftests: can: add J1939 base functional tests
  selftests: can: add J1939 socket option and metadata tests
  selftests: can: add J1939 addressing and concurrency tests
  selftests: can: add J1939 wire-format tests against CAN_RAW
  selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
  selftests: can: cover J1939 transport abort and timeout paths
  selftests: can: check SO_J1939_PROMISC receive effect

 tools/testing/selftests/net/can/.gitignore    |   5 +
 tools/testing/selftests/net/can/Makefile      |  20 +-
 tools/testing/selftests/net/can/config        |   2 +
 tools/testing/selftests/net/can/j1939_lib.sh  |  74 ++
 tools/testing/selftests/net/can/j1939_tests.h | 460 +++++++++++++
 .../selftests/net/can/test_j1939_addr.c       | 336 +++++++++
 .../selftests/net/can/test_j1939_addr.sh      |   5 +
 .../selftests/net/can/test_j1939_base.c       | 160 +++++
 .../selftests/net/can/test_j1939_base.sh      |   5 +
 .../selftests/net/can/test_j1939_multi.c      | 638 ++++++++++++++++++
 .../selftests/net/can/test_j1939_multi.sh     |   5 +
 .../selftests/net/can/test_j1939_opts.c       | 347 ++++++++++
 .../selftests/net/can/test_j1939_opts.sh      |   5 +
 .../selftests/net/can/test_j1939_wire.c       | 170 +++++
 .../selftests/net/can/test_j1939_wire.sh      |   5 +
 15 files changed, 2235 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/net/can/j1939_lib.sh
 create mode 100644 tools/testing/selftests/net/can/j1939_tests.h
 create mode 100644 tools/testing/selftests/net/can/test_j1939_addr.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_addr.sh
 create mode 100644 tools/testing/selftests/net/can/test_j1939_base.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_base.sh
 create mode 100644 tools/testing/selftests/net/can/test_j1939_multi.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_multi.sh
 create mode 100644 tools/testing/selftests/net/can/test_j1939_opts.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_opts.sh
 create mode 100644 tools/testing/selftests/net/can/test_j1939_wire.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_wire.sh

--
2.47.3


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 1/8] selftests: can: add J1939 test helper header and library
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 2/8] selftests: can: add J1939 base functional tests Oleksij Rempel
                   ` (6 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add two shared pieces used by all of the J1939 selftests that follow:

 - j1939_tests.h: the common C helpers, as static inline functions and
   macros grouped and documented by purpose, so each test .c keeps to the
   behaviour it checks rather than the socket and wire plumbing.
 - j1939_lib.sh: the shared vcan setup/cleanup and test runner; a wrapper
   just sets J1939_TEST_PROG and sources it.

They remove boilerplate the tests would otherwise repeat: resolving the
test interface, opening and connecting CAN_J1939 sockets, opening a
CAN_RAW socket, poll()+recv() and send/verify helpers, CAN-ID and
PGN/NAME encoding, the raw TP/ETP frame building blocks, and vcan
bring-up in every shell wrapper.

No tests use them yet; the following patches add the tests on top.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/Makefile      |   2 +
 tools/testing/selftests/net/can/j1939_lib.sh  |  74 +++
 tools/testing/selftests/net/can/j1939_tests.h | 460 ++++++++++++++++++
 3 files changed, 536 insertions(+)
 create mode 100644 tools/testing/selftests/net/can/j1939_lib.sh
 create mode 100644 tools/testing/selftests/net/can/j1939_tests.h

diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 5b82e60a03e7..e2092c289109 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -6,6 +6,8 @@ CFLAGS += -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INC
 
 TEST_PROGS := test_raw_filter.sh
 
+TEST_FILES := j1939_lib.sh
+
 TEST_GEN_FILES := test_raw_filter
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/j1939_lib.sh b/tools/testing/selftests/net/can/j1939_lib.sh
new file mode 100644
index 000000000000..7b7fff228b16
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_lib.sh
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Shared bring-up for the J1939 selftest wrappers. A wrapper sets
+# J1939_TEST_PROG to its test binary and sources this file:
+#
+#	J1939_TEST_PROG=test_j1939_base
+#	source "$(dirname "$0")/j1939_lib.sh"
+#
+# It sets up (and tears down) the CAN interface named by CANIF (a vcan by
+# default), runs the test binary through the kselftest net lib and reports the
+# result.
+
+ALL_TESTS="j1939_run"
+
+net_dir=$(dirname "$0")/..
+source "$net_dir/lib.sh"
+
+export CANIF=${CANIF:-"vcan0"}
+# 250 kbit/s: ISO 11783-2:2002 §3.6 (nominal 4 us bit time); same value in SAE
+# J1939-11:2006 §3.12. J1939-15:1999 §3.14 Table 1 defers to J1939-11.
+# Only the real-CAN path uses this; the default vcan ignores bitrate.
+BITRATE=${BITRATE:-250000}
+
+# Only undo what this script actually did: delete a vcan we created, and bring
+# down an interface only if we brought it up. A failed setup() (e.g. CANIF
+# already exists) must not disturb a pre-existing interface.
+created=0
+brought_up=0
+
+setup()
+{
+	if [[ $CANIF == vcan* ]]; then
+		ip link add name "$CANIF" type vcan || exit $ksft_skip
+		created=1
+	else
+		ip link set dev "$CANIF" type can bitrate "$BITRATE" || exit $ksft_skip
+	fi
+	ip link set dev "$CANIF" up || exit $ksft_skip
+	brought_up=1
+}
+
+cleanup()
+{
+	if [ "$brought_up" = 1 ]; then
+		ip link set dev "$CANIF" down 2>/dev/null
+	fi
+	if [ "$created" = 1 ]; then
+		ip link delete "$CANIF" 2>/dev/null
+	fi
+}
+
+j1939_run()
+{
+	local rc
+
+	./"$J1939_TEST_PROG"
+	rc=$?
+	# The test binary exits ksft_skip when CANIF is missing or the kernel
+	# lacks CAN_J1939; keep that a skip instead of letting check_err turn
+	# any non-zero into a failure.
+	if ((rc == ksft_skip)); then
+		ret_set_ksft_status $ksft_skip "$J1939_TEST_PROG"
+	else
+		check_err $rc
+	fi
+	log_test "$J1939_TEST_PROG"
+}
+
+trap cleanup EXIT INT TERM
+setup
+
+tests_run
+
+exit $EXIT_STATUS
diff --git a/tools/testing/selftests/net/can/j1939_tests.h b/tools/testing/selftests/net/can/j1939_tests.h
new file mode 100644
index 000000000000..dd70a7d60ec7
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -0,0 +1,460 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) */
+/*
+ * Shared helpers for the J1939 selftests.
+ *
+ * The individual tests are standalone kselftest binaries, so the common
+ * boilerplate (interface lookup, socket open/connect, timed receive, CAN-ID
+ * encoding and the CANIF/main() plumbing) lives here as static inline helpers.
+ *
+ * The including .c file must include "kselftest_harness.h" before this header
+ * (the J1939_TEST_MAIN() macro uses KSFT_SKIP and test_harness_run()).
+ */
+#ifndef __SELFTESTS_NET_CAN_J1939_TESTS_H__
+#define __SELFTESTS_NET_CAN_J1939_TESTS_H__
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <poll.h>
+#include <stdint.h>
+#include <errno.h>
+#include <time.h>
+
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <net/if.h>
+#include <linux/if.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <linux/can/j1939.h>
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(a)		(sizeof(a) / sizeof((a)[0]))
+#endif
+
+/* Harness patience for an expected frame (not a protocol timer). */
+#define J1939_RECV_TIMEOUT_MS	2000
+
+/*
+ * Timeout for a check that expects no frame. Long enough that a real
+ * local frame would have arrived, so a timeout means "nothing came".
+ * Much shorter than J1939_RECV_TIMEOUT_MS, to keep the test fast.
+ */
+#define J1939_NEG_RECV_TIMEOUT_MS	300
+
+/* kernel's default J1939 TX priority (net/can/j1939 seeds 6). */
+#define J1939_PRIO_DEFAULT	6
+
+/* J1939 PGN helpers: PF >= 0xf0 selects PDU2 (broadcast, no destination). */
+#define J1939_PF(pgn)		(((pgn) >> 8) & 0xff)
+#define J1939_PS(pgn)		((pgn) & 0xff)
+#define J1939_IS_PDU1(pgn)	(J1939_PF(pgn) < 0xf0)
+
+/* Decoders for the fields of a received 29-bit CAN id (mask with CAN_EFF_MASK). */
+#define J1939_ID_PRIO(id)	(((id) >> 26) & 0x7)
+#define J1939_ID_PF(id)		(((id) >> 16) & 0xff)
+#define J1939_ID_PS(id)		(((id) >> 8) & 0xff)
+#define J1939_ID_SA(id)		((id) & 0xff)
+
+/* Number of 7-byte TP/ETP DT packets needed for len bytes, rounded up. */
+#define J1939_NUM_PKTS(len)	(((len) + 6) / 7)
+
+/*
+ * Size of a CAN_RAW receive buffer large enough for a full ETP burst.
+ * A generous round-number headroom, not a computed minimum; applied
+ * best-effort via SO_RCVBUFFORCE.
+ */
+#define J1939_RAW_BIG_RCVBUF	(8 * 1024 * 1024)
+
+/* Flags for j1939_open_flags(). */
+#define J1939_OPEN_BCAST	(1 << 0)
+
+/* Set by J1939_TEST_MAIN() from the CANIF environment variable. */
+static char CANIF[IFNAMSIZ];
+
+/* Resolve the ifindex of the test interface named by CANIF. */
+static inline int j1939_if_index(void)
+{
+	struct ifreq ifr = {};
+	int s, idx = -1;
+
+	s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
+	if (s < 0)
+		return -1;
+	strncpy(ifr.ifr_name, CANIF, sizeof(ifr.ifr_name) - 1);
+	if (ioctl(s, SIOCGIFINDEX, &ifr) >= 0)
+		idx = ifr.ifr_ifindex;
+	close(s);
+	return idx;
+}
+
+/*
+ * FIXTURE_SETUP helper: resolve CANIF's ifindex into self->ifindex and abort the
+ * test if it cannot be found. Used as a statement: J1939_SETUP_IFINDEX(self);
+ */
+#define J1939_SETUP_IFINDEX(self)					       \
+	do {								       \
+		(self)->ifindex = j1939_if_index();			       \
+		ASSERT_GE((self)->ifindex, 0)				       \
+			TH_LOG("failed to resolve ifindex for '%s': %d",       \
+			       CANIF, errno);				       \
+	} while (0)
+
+/* Open a J1939 socket bound to (name, sa, pgn); flags may set J1939_OPEN_BCAST. */
+static inline int j1939_open_flags(int ifindex, name_t name, uint8_t sa,
+				   pgn_t pgn, int flags)
+{
+	struct sockaddr_can addr = {
+		.can_family = AF_CAN,
+		.can_ifindex = ifindex,
+	};
+	int s, one = 1;
+
+	s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
+	if (s < 0)
+		return -1;
+	if ((flags & J1939_OPEN_BCAST) &&
+	    setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one))) {
+		close(s);
+		return -1;
+	}
+	addr.can_addr.j1939.name = name;
+	addr.can_addr.j1939.addr = sa;
+	addr.can_addr.j1939.pgn = pgn;
+	if (bind(s, (struct sockaddr *)&addr, sizeof(addr))) {
+		close(s);
+		return -1;
+	}
+	return s;
+}
+
+/* Open a J1939 socket bound to (sa, pgn) with no NAME and no broadcast. */
+static inline int j1939_open(int ifindex, uint8_t sa, pgn_t pgn)
+{
+	return j1939_open_flags(ifindex, J1939_NO_NAME, sa, pgn, 0);
+}
+
+/* Connect a J1939 socket to the peer (name, da, pgn). */
+static inline int j1939_connect_name(int s, int ifindex, name_t name,
+				     uint8_t da, pgn_t pgn)
+{
+	struct sockaddr_can addr = {
+		.can_family = AF_CAN,
+		.can_ifindex = ifindex,
+	};
+
+	addr.can_addr.j1939.name = name;
+	addr.can_addr.j1939.addr = da;
+	addr.can_addr.j1939.pgn = pgn;
+	return connect(s, (struct sockaddr *)&addr, sizeof(addr));
+}
+
+static inline int j1939_connect(int s, int ifindex, uint8_t da, pgn_t pgn)
+{
+	return j1939_connect_name(s, ifindex, J1939_NO_NAME, da, pgn);
+}
+
+/* Fill a sockaddr_can describing a J1939 (name, da, pgn) destination. */
+static inline void j1939_fill_sockaddr(struct sockaddr_can *addr, int ifindex,
+				       name_t name, uint8_t da, pgn_t pgn)
+{
+	memset(addr, 0, sizeof(*addr));
+	addr->can_family = AF_CAN;
+	addr->can_ifindex = ifindex;
+	addr->can_addr.j1939.name = name;
+	addr->can_addr.j1939.addr = da;
+	addr->can_addr.j1939.pgn = pgn;
+}
+
+/* sendto() len bytes from buf to the J1939 (name, da, pgn) destination. */
+static inline int j1939_sendto(int s, const void *buf, size_t len, int ifindex,
+			       name_t name, uint8_t da, pgn_t pgn)
+{
+	struct sockaddr_can dst;
+
+	j1939_fill_sockaddr(&dst, ifindex, name, da, pgn);
+	return sendto(s, buf, len, 0, (struct sockaddr *)&dst, sizeof(dst));
+}
+
+/*
+ * Open a CAN_RAW socket on ifindex. rcvbuf > 0 requests that SO_RCVBUFFORCE
+ * size (best-effort: a failure is logged, not fatal).
+ */
+static inline int open_raw_rcvbuf(int ifindex, int rcvbuf)
+{
+	struct sockaddr_can addr = {
+		.can_family = AF_CAN,
+		.can_ifindex = ifindex,
+	};
+	int s;
+
+	s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+	if (s < 0)
+		return -1;
+	if (rcvbuf > 0 &&
+	    setsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf)))
+		fprintf(stderr, "%s: SO_RCVBUFFORCE(%d) failed: %d\n",
+			__func__, rcvbuf, errno);
+	if (bind(s, (struct sockaddr *)&addr, sizeof(addr))) {
+		close(s);
+		return -1;
+	}
+	return s;
+}
+
+static inline int open_raw(int ifindex)
+{
+	return open_raw_rcvbuf(ifindex, 0);
+}
+
+/*
+ * FIXTURE_SETUP helper: open a CAN_RAW socket on self->ifindex into self->raw
+ * with the given SO_RCVBUFFORCE size (0 for the default) and abort the test if
+ * it cannot be opened. Used as a statement: J1939_SETUP_RAW(self, rcvbuf);
+ */
+#define J1939_SETUP_RAW(self, rcvbuf)					       \
+	do {								       \
+		(self)->raw = open_raw_rcvbuf((self)->ifindex, (rcvbuf));      \
+		ASSERT_GE((self)->raw, 0)				       \
+			TH_LOG("failed to open raw socket: %d", errno);	       \
+	} while (0)
+
+/* poll() + recv(): returns the recv() result, or -1 on timeout/error. */
+static inline int recv_timeout(int s, void *buf, size_t len, int timeout_ms)
+{
+	struct pollfd pfd = { .fd = s, .events = POLLIN };
+
+	if (poll(&pfd, 1, timeout_ms) <= 0)
+		return -1;
+	return recv(s, buf, len, 0);
+}
+
+/* Send len bytes on tx and verify they arrive byte-for-byte on rx. 0 / -1. */
+static inline int send_recv_verify(int tx, int rx, const void *buf, size_t len,
+				   int timeout_ms)
+{
+	uint8_t *rx_buf = malloc(len);
+	int rc = -1;
+
+	if (!rx_buf)
+		return -1;
+	if (send(tx, buf, len, 0) == (ssize_t)len &&
+	    recv_timeout(rx, rx_buf, len, timeout_ms) == (int)len &&
+	    memcmp(buf, rx_buf, len) == 0)
+		rc = 0;
+	free(rx_buf);
+	return rc;
+}
+
+/* Encode a J1939 single frame the way the kernel does on transmit. */
+static inline canid_t j1939_canid(uint8_t prio, pgn_t pgn, uint8_t da,
+				  uint8_t sa)
+{
+	canid_t id = CAN_EFF_FLAG | ((canid_t)prio << 26) | (pgn << 8) | sa;
+
+	if (J1939_IS_PDU1(pgn))
+		id |= (canid_t)da << 8;
+	return id;
+}
+
+/* Start an 8-byte TP/ETP control frame: unused CM bytes are 0xff, byte 0 = cmd. */
+static inline void ctl_dat_init(uint8_t *dat, uint8_t cmd)
+{
+	memset(dat, 0xff, 8);
+	dat[0] = cmd;
+}
+
+/* Embed a 24-bit PGN into a TP/ETP control frame (bytes 5..7, little endian). */
+static inline void put_pgn(uint8_t *dat, pgn_t pgn)
+{
+	dat[5] = pgn;
+	dat[6] = pgn >> 8;
+	dat[7] = pgn >> 16;
+}
+
+/* Serialize a 64-bit NAME little-endian into an 8-byte buffer (host-agnostic). */
+static inline void put_name(uint8_t *dat, name_t name)
+{
+	int i;
+
+	for (i = 0; i < 8; i++)
+		dat[i] = (uint8_t)(name >> (8 * i));
+}
+
+/* Little-endian codecs for the TP/ETP CM multi-byte fields (host-agnostic). */
+static inline void put_le16(uint8_t *p, uint16_t v)
+{
+	p[0] = v;
+	p[1] = v >> 8;
+}
+
+static inline void put_le24(uint8_t *p, uint32_t v)
+{
+	p[0] = v;
+	p[1] = v >> 8;
+	p[2] = v >> 16;
+}
+
+static inline void put_le32(uint8_t *p, uint32_t v)
+{
+	p[0] = v;
+	p[1] = v >> 8;
+	p[2] = v >> 16;
+	p[3] = v >> 24;
+}
+
+static inline uint16_t get_le16(const uint8_t *p)
+{
+	return (uint16_t)(p[0] | (p[1] << 8));
+}
+
+static inline uint32_t get_le24(const uint8_t *p)
+{
+	return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16);
+}
+
+static inline uint32_t get_le32(const uint8_t *p)
+{
+	return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
+	       ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+}
+
+/* Send one 8-byte J1939-encoded frame (any PGN) with the given priority. */
+static inline int send_raw8(int raw, uint8_t prio, pgn_t pgn, uint8_t da,
+			    uint8_t sa, const uint8_t dat[8])
+{
+	struct can_frame f = {};
+
+	f.can_id = j1939_canid(prio, pgn, da, sa);
+	f.can_dlc = 8;
+	memcpy(f.data, dat, 8);
+	return write(raw, &f, sizeof(f)) == (int)sizeof(f) ? 0 : -1;
+}
+
+/* Send a TP/ETP data (DT) frame carrying seq + up to 7 payload bytes. */
+static inline int send_dt(int raw, uint8_t prio, pgn_t dat_pgn, uint8_t da,
+			  uint8_t sa, uint8_t seq, const uint8_t *payload,
+			  size_t off, size_t total)
+{
+	struct can_frame f = {};
+	size_t n;
+
+	/* off >= total would make the size_t subtraction total - off underflow. */
+	if (off >= total)
+		return -1;
+	n = total - off;
+	if (n > 7)
+		n = 7;
+	f.can_id = j1939_canid(prio, dat_pgn, da, sa);
+	f.can_dlc = 8;
+	memset(f.data, 0xff, 8);
+	f.data[0] = seq;
+	memcpy(&f.data[1], payload + off, n);
+	return write(raw, &f, sizeof(f)) == (int)sizeof(f) ? 0 : -1;
+}
+
+/* Elapsed milliseconds between two CLOCK_MONOTONIC samples. */
+static inline long timespec_diff_ms(const struct timespec *a, const struct timespec *b)
+{
+	return (b->tv_sec - a->tv_sec) * 1000 + (b->tv_nsec - a->tv_nsec) / 1000000;
+}
+
+/* Poll+recv on raw until match() accepts a frame, else timeout. 0/-1. */
+static inline int recv_match(int raw, int timeout_ms, struct can_frame *frame,
+			     bool (*match)(canid_t id, const void *ctx),
+			     const void *ctx)
+{
+	struct pollfd pfd = { .fd = raw, .events = POLLIN };
+	struct timespec now, deadline;
+	int remaining = timeout_ms;
+
+	/*
+	 * Non-matching frames (other traffic on the bus) must not refresh the
+	 * timeout, or a busy bus would keep the loop alive forever. Poll against
+	 * a fixed deadline and shrink the budget after every frame.
+	 */
+	clock_gettime(CLOCK_MONOTONIC, &deadline);
+	deadline.tv_sec += timeout_ms / 1000;
+	deadline.tv_nsec += (long)(timeout_ms % 1000) * 1000000;
+	if (deadline.tv_nsec >= 1000000000) {
+		deadline.tv_sec++;
+		deadline.tv_nsec -= 1000000000;
+	}
+	while (remaining > 0 && poll(&pfd, 1, remaining) > 0) {
+		if (recv(raw, frame, sizeof(*frame), 0) < 0)
+			return -1;
+		if (match(frame->can_id & CAN_EFF_MASK, ctx))
+			return 0;
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		remaining = timespec_diff_ms(&now, &deadline);
+	}
+	return -1;
+}
+
+struct j1939_peer_match { uint8_t want_pf, want_da; };
+static inline bool match_peer_dest(canid_t id, const void *ctx)
+{
+	const struct j1939_peer_match *m = ctx;
+
+	return J1939_ID_PF(id) == m->want_pf &&
+	       J1939_ID_PS(id) == m->want_da;		/* destination */
+}
+
+/* Receive the next frame whose CAN-id PF == want_pf addressed to want_da (PS). */
+static inline int recv_for_peer(int raw, uint8_t want_pf, uint8_t want_da,
+				struct can_frame *frame, int timeout_ms)
+{
+	struct j1939_peer_match m = { .want_pf = want_pf, .want_da = want_da };
+
+	return recv_match(raw, timeout_ms, frame, match_peer_dest, &m);
+}
+
+/* recv_for_peer() + check the control command byte; 0 on match, -1 otherwise. */
+static inline int recv_ctl_cmd(int raw, uint8_t want_pf, uint8_t want_da,
+			       struct can_frame *frame, int timeout_ms,
+			       uint8_t want_cmd)
+{
+	if (recv_for_peer(raw, want_pf, want_da, frame, timeout_ms))
+		return -1;
+	return frame->data[0] == want_cmd ? 0 : -1;
+}
+
+/* Fill buf with a deterministic, position-dependent byte pattern. */
+static inline void fill_pattern(uint8_t *buf, size_t len)
+{
+	size_t i;
+
+	for (i = 0; i < len; i++)
+		buf[i] = (uint8_t)(i ^ (i >> 8) ^ 0x3c);
+}
+
+/*
+ * Define main(): require CANIF in the environment and a kernel that supports
+ * CAN_J1939, then run the harness. Skip (rather than fail) when either is
+ * missing, per the kselftest skip contract.
+ */
+#define J1939_TEST_MAIN()						       \
+int main(int argc, char **argv)						       \
+{									       \
+	char *ifname = getenv("CANIF");					       \
+	int probe;							       \
+									       \
+	if (!ifname) {							       \
+		fprintf(stderr,						       \
+			"CANIF environment variable must contain the test interface\n"); \
+		return KSFT_SKIP;					       \
+	}								       \
+	strncpy(CANIF, ifname, sizeof(CANIF) - 1);			       \
+									       \
+	probe = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);			       \
+	if (probe < 0) {						       \
+		if (errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT)	       \
+			return KSFT_SKIP;				       \
+	} else {							       \
+		close(probe);						       \
+	}								       \
+	return test_harness_run(argc, argv);				       \
+}
+
+#endif /* __SELFTESTS_NET_CAN_J1939_TESTS_H__ */
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 2/8] selftests: can: add J1939 base functional tests
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 1/8] selftests: can: add J1939 test helper header and library Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 3/8] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add a functional selftest for the SAE J1939 socket interface. It runs two
CAN_J1939 sockets (a sender and a receiver) over a vcan interface and
checks payload integrity across the transport-size transitions: single
frame (PDU1), Transport Protocol, Extended Transport Protocol, the exact
size boundaries around those transitions, and single-frame and BAM
broadcast (PDU2).

Declare j1939_tests.h as a local header dependency and enable
CONFIG_CAN_J1939 in the test config.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/.gitignore    |   1 +
 tools/testing/selftests/net/can/Makefile      |  10 +-
 tools/testing/selftests/net/can/config        |   1 +
 .../selftests/net/can/test_j1939_base.c       | 160 ++++++++++++++++++
 .../selftests/net/can/test_j1939_base.sh      |   5 +
 5 files changed, 175 insertions(+), 2 deletions(-)
 create mode 100644 tools/testing/selftests/net/can/test_j1939_base.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_base.sh

diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 764a53fc837f..0ab563fe9ddf 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -1,2 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0-only
 test_raw_filter
+test_j1939_base
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index e2092c289109..cf4bd2461a6c 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -4,10 +4,16 @@ top_srcdir = ../../../../..
 
 CFLAGS += -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INCLUDES)
 
-TEST_PROGS := test_raw_filter.sh
+LOCAL_HDRS := j1939_tests.h
+
+TEST_PROGS := \
+	test_raw_filter.sh \
+	test_j1939_base.sh \
 
 TEST_FILES := j1939_lib.sh
 
-TEST_GEN_FILES := test_raw_filter
+TEST_GEN_FILES := \
+	test_raw_filter \
+	test_j1939_base \
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/config b/tools/testing/selftests/net/can/config
index 188f79796670..52a186bd1055 100644
--- a/tools/testing/selftests/net/can/config
+++ b/tools/testing/selftests/net/can/config
@@ -1,3 +1,4 @@
 CONFIG_CAN=m
 CONFIG_CAN_DEV=m
 CONFIG_CAN_VCAN=m
+CONFIG_CAN_J1939=m
diff --git a/tools/testing/selftests/net/can/test_j1939_base.c b/tools/testing/selftests/net/can/test_j1939_base.c
new file mode 100644
index 000000000000..59f1448b7fb2
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_base.c
@@ -0,0 +1,160 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 base functional selftests.
+ *
+ * Run two SOCK_DGRAM/CAN_J1939 sockets (a sender and a receiver) over a vcan
+ * interface and verify payload integrity across the transport-size transitions:
+ * a single frame (PDU1), Transport Protocol, Extended Transport Protocol, the
+ * exact boundaries between them, and single-frame and BAM broadcast (PDU2).
+ *
+ * The vcan interface is set up by the caller (see test_j1939_base.sh); its name
+ * is passed via the CANIF environment variable.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+#define SA_TX			0x80
+#define SA_RX			0x90
+#define PGN_PDU1		0x0ab00	/* PF 0xab (< 240): destination specific */
+#define PGN_PDU2		0x0feee	/* PF 0xfe (>= 240): broadcast */
+
+FIXTURE(j1939_base)
+{
+	int ifindex;
+	int tx;
+	int rx;
+};
+
+FIXTURE_SETUP(j1939_base)
+{
+	self->tx = -1;
+	self->rx = -1;
+	J1939_SETUP_IFINDEX(self);
+}
+
+FIXTURE_TEARDOWN(j1939_base)
+{
+	if (self->tx >= 0)
+		close(self->tx);
+	if (self->rx >= 0)
+		close(self->rx);
+}
+
+/*
+ * Set up a receiver bound to (rx_addr, pgn) and a sender bound to SA_TX and
+ * connected to (tx_dst, pgn). Any pair opened by a previous call is closed
+ * first. Returns 0 on success, -1 otherwise.
+ */
+static int pair_up(FIXTURE_DATA(j1939_base) *self, uint8_t rx_addr,
+		   uint8_t tx_dst, pgn_t pgn, int bcast)
+{
+	int flags = bcast ? J1939_OPEN_BCAST : 0;
+
+	if (self->tx >= 0) {
+		close(self->tx);
+		self->tx = -1;
+	}
+	if (self->rx >= 0) {
+		close(self->rx);
+		self->rx = -1;
+	}
+
+	self->rx = j1939_open_flags(self->ifindex, J1939_NO_NAME, rx_addr, pgn,
+				   flags);
+	if (self->rx < 0) {
+		fprintf(stderr, "# open rx failed: %d\n", errno);
+		return -1;
+	}
+
+	self->tx = j1939_open_flags(self->ifindex, J1939_NO_NAME, SA_TX,
+				   J1939_NO_PGN, flags);
+	if (self->tx < 0) {
+		fprintf(stderr, "# open tx failed: %d\n", errno);
+		return -1;
+	}
+
+	if (j1939_connect(self->tx, self->ifindex, tx_dst, pgn)) {
+		fprintf(stderr, "# connect tx failed: %d\n", errno);
+		return -1;
+	}
+
+	return 0;
+}
+
+/*
+ * Send a deterministic payload of the given size on tx and verify it is
+ * received byte for byte on rx. Returns 0 on success, -1 otherwise.
+ */
+static int roundtrip(FIXTURE_DATA(j1939_base) *self, size_t size)
+{
+	uint8_t *tx_buf;
+	int rc;
+
+	tx_buf = malloc(size);
+	if (!tx_buf) {
+		fprintf(stderr, "# alloc failed\n");
+		return -1;
+	}
+	fill_pattern(tx_buf, size);
+
+	rc = send_recv_verify(self->tx, self->rx, tx_buf, size,
+			      J1939_RECV_TIMEOUT_MS);
+	if (rc)
+		fprintf(stderr, "# round-trip failed at size %zu, errno %d\n",
+			size, errno);
+
+	free(tx_buf);
+	return rc;
+}
+
+/* single frame (<= 8 bytes), unicast PDU1. */
+TEST_F(j1939_base, single_frame_pdu1)
+{
+	ASSERT_EQ(pair_up(self, SA_RX, SA_RX, PGN_PDU1, 0), 0);
+	EXPECT_EQ(roundtrip(self, 8), 0);
+}
+
+/* Transport Protocol (9 .. 1785 bytes), unicast. */
+TEST_F(j1939_base, tp_roundtrip)
+{
+	ASSERT_EQ(pair_up(self, SA_RX, SA_RX, PGN_PDU1, 0), 0);
+	EXPECT_EQ(roundtrip(self, 128), 0);
+}
+
+/* Extended Transport Protocol (> 1785 bytes), unicast. */
+TEST_F(j1939_base, etp_roundtrip)
+{
+	ASSERT_EQ(pair_up(self, SA_RX, SA_RX, PGN_PDU1, 0), 0);
+	EXPECT_EQ(roundtrip(self, 2000), 0);
+}
+
+/* exact size boundaries around single / TP / ETP transitions. */
+TEST_F(j1939_base, size_boundaries)
+{
+	static const size_t sizes[] = { 8, 9, 1785, 1786 };
+	size_t k;
+
+	for (k = 0; k < ARRAY_SIZE(sizes); k++) {
+		ASSERT_EQ(pair_up(self, SA_RX, SA_RX, PGN_PDU1, 0), 0);
+		EXPECT_EQ(roundtrip(self, sizes[k]), 0)
+			TH_LOG("failed at boundary size %zu", sizes[k]);
+	}
+}
+
+/* single-frame broadcast, PDU2. */
+TEST_F(j1939_base, single_frame_broadcast_pdu2)
+{
+	ASSERT_EQ(pair_up(self, SA_RX, J1939_NO_ADDR, PGN_PDU2, 1), 0);
+	EXPECT_EQ(roundtrip(self, 8), 0);
+}
+
+/* BAM (broadcast transport). */
+TEST_F(j1939_base, bam_broadcast)
+{
+	ASSERT_EQ(pair_up(self, SA_RX, J1939_NO_ADDR, PGN_PDU2, 1), 0);
+	EXPECT_EQ(roundtrip(self, 128), 0);
+}
+
+J1939_TEST_MAIN();
diff --git a/tools/testing/selftests/net/can/test_j1939_base.sh b/tools/testing/selftests/net/can/test_j1939_base.sh
new file mode 100755
index 000000000000..3cd2b14a74bb
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_base.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+J1939_TEST_PROG=test_j1939_base
+source "$(dirname "$0")/j1939_lib.sh"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 3/8] selftests: can: add J1939 socket option and metadata tests
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 1/8] selftests: can: add J1939 test helper header and library Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 2/8] selftests: can: add J1939 base functional tests Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 4/8] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add a selftest covering the J1939 socket options and send metadata: the
PGN filter (SO_J1939_FILTER), the send priority (SO_J1939_SEND_PRIO,
observed on the wire via a CAN_RAW peer), the promisc flag, destination
addressing via sendto(), a wrong-family bind, and the TX-completion
error-queue notification (SO_J1939_ERRQUEUE / SO_TIMESTAMPING), including
that it is labelled SCM_TSTAMP_ACK. setsockopt values are also read back
with getsockopt.

This is the first J1939 test to open a CAN_RAW socket, so enable
CONFIG_CAN_RAW in the test config.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/.gitignore    |   1 +
 tools/testing/selftests/net/can/Makefile      |   2 +
 tools/testing/selftests/net/can/config        |   1 +
 .../selftests/net/can/test_j1939_opts.c       | 305 ++++++++++++++++++
 .../selftests/net/can/test_j1939_opts.sh      |   5 +
 5 files changed, 314 insertions(+)
 create mode 100644 tools/testing/selftests/net/can/test_j1939_opts.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_opts.sh

diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 0ab563fe9ddf..427eed83e49e 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -1,3 +1,4 @@
 # SPDX-License-Identifier: GPL-2.0-only
 test_raw_filter
 test_j1939_base
+test_j1939_opts
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index cf4bd2461a6c..24a57edb912c 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -9,11 +9,13 @@ LOCAL_HDRS := j1939_tests.h
 TEST_PROGS := \
 	test_raw_filter.sh \
 	test_j1939_base.sh \
+	test_j1939_opts.sh \
 
 TEST_FILES := j1939_lib.sh
 
 TEST_GEN_FILES := \
 	test_raw_filter \
 	test_j1939_base \
+	test_j1939_opts \
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/config b/tools/testing/selftests/net/can/config
index 52a186bd1055..908342600101 100644
--- a/tools/testing/selftests/net/can/config
+++ b/tools/testing/selftests/net/can/config
@@ -2,3 +2,4 @@ CONFIG_CAN=m
 CONFIG_CAN_DEV=m
 CONFIG_CAN_VCAN=m
 CONFIG_CAN_J1939=m
+CONFIG_CAN_RAW=m
diff --git a/tools/testing/selftests/net/can/test_j1939_opts.c b/tools/testing/selftests/net/can/test_j1939_opts.c
new file mode 100644
index 000000000000..f761a42db213
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_opts.c
@@ -0,0 +1,305 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 socket option and metadata selftests.
+ *
+ * On top of the basic transport, cover the user-visible J1939 socket options and
+ * addressing: the SO_J1939_FILTER PGN whitelist, the SO_J1939_SEND_PRIO send
+ * priority seen in the CAN id (via a CAN_RAW peer, including the default of 6),
+ * unconnected sendto(2), a wrong-family bind rejection, and SO_J1939_ERRQUEUE
+ * TX-completion notifications.
+ *
+ * The vcan interface is set up by the caller (see test_j1939_opts.sh); its name
+ * is passed via the CANIF environment variable.
+ */
+#include <errno.h>
+#include <time.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+#include <linux/net_tstamp.h>
+#include <linux/errqueue.h>
+
+#define SA_TX			0x80
+#define SA_RX			0x90
+#define PGN_A			0x0ab00
+#define PGN_B			0x0cd00
+#define PRIO_SET		3
+/*
+ * Worst-case error-queue drain budget: each iteration can block up to
+ * J1939_RECV_TIMEOUT_MS, so this bounds the drain time. A generous cap; only one
+ * ACK is expected.
+ */
+#define ERRQUEUE_POLL_RETRIES	8
+
+/* Read the next CAN frame from a raw socket and return its J1939 priority. */
+static int raw_prio(int raw, int timeout_ms)
+{
+	struct can_frame frame;
+
+	if (recv_timeout(raw, &frame, sizeof(frame), timeout_ms) < 0)
+		return -1;
+	return (int)J1939_ID_PRIO(frame.can_id & CAN_EFF_MASK);
+}
+
+/* Set an int J1939 sockopt, read it back, and return the value (or -1). */
+static int sockopt_roundtrip(int s, int optname, int val)
+{
+	int got = -1;
+	socklen_t len = sizeof(got);
+
+	if (setsockopt(s, SOL_CAN_J1939, optname, &val, sizeof(val)) < 0)
+		return -1;
+	if (getsockopt(s, SOL_CAN_J1939, optname, &got, &len) < 0)
+		return -1;
+	return got;
+}
+
+FIXTURE(j1939_opts)
+{
+	int ifindex;
+	int tx;
+	int rx;
+	int raw;
+};
+
+FIXTURE_SETUP(j1939_opts)
+{
+	self->tx = -1;
+	self->rx = -1;
+	self->raw = -1;
+
+	J1939_SETUP_IFINDEX(self);
+
+	J1939_SETUP_RAW(self, 0);
+}
+
+FIXTURE_TEARDOWN(j1939_opts)
+{
+	if (self->tx >= 0)
+		close(self->tx);
+	if (self->rx >= 0)
+		close(self->rx);
+	if (self->raw >= 0)
+		close(self->raw);
+}
+
+/* SO_J1939_FILTER whitelists PGN_A; PGN_A is received, PGN_B is dropped. */
+TEST_F(j1939_opts, filter_pgn)
+{
+	struct j1939_filter filt = {
+		.pgn = PGN_A,
+		.pgn_mask = J1939_PGN_MAX,
+	};
+	uint8_t tx_buf[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
+	uint8_t rx_buf[8];
+	int ret;
+
+	self->rx = j1939_open(self->ifindex, SA_RX, J1939_NO_PGN);
+	ASSERT_GE(self->rx, 0);
+	ASSERT_EQ(setsockopt(self->rx, SOL_CAN_J1939, SO_J1939_FILTER,
+			     &filt, sizeof(filt)), 0)
+		TH_LOG("SO_J1939_FILTER failed: %d", errno);
+
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+
+	/* PGN_A must pass the filter */
+	ret = j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			   J1939_NO_NAME, SA_RX, PGN_A);
+	ASSERT_EQ(ret, (int)sizeof(tx_buf))
+		TH_LOG("sendto PGN_A failed: %d", errno);
+	EXPECT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_RECV_TIMEOUT_MS), (int)sizeof(tx_buf))
+		TH_LOG("PGN_A was not received despite matching filter");
+
+	/* PGN_B must be dropped by the filter */
+	ret = j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			   J1939_NO_NAME, SA_RX, PGN_B);
+	ASSERT_EQ(ret, (int)sizeof(tx_buf))
+		TH_LOG("sendto PGN_B failed: %d", errno);
+	EXPECT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_NEG_RECV_TIMEOUT_MS), -1)
+		TH_LOG("PGN_B was received but should have been filtered out");
+}
+
+/* without SO_J1939_SEND_PRIO the J1939 default priority (6) is used. */
+TEST_F(j1939_opts, send_prio_default)
+{
+	uint8_t buf[8] = {};
+
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+
+	ASSERT_EQ(j1939_sendto(self->tx, buf, sizeof(buf), self->ifindex,
+			       J1939_NO_NAME, SA_RX, PGN_A),
+		  (int)sizeof(buf));
+
+	EXPECT_EQ(raw_prio(self->raw, J1939_RECV_TIMEOUT_MS), J1939_PRIO_DEFAULT);
+}
+
+/* SO_J1939_SEND_PRIO changes the priority carried in the CAN id. */
+TEST_F(j1939_opts, send_prio_set)
+{
+	int prio = PRIO_SET;
+	uint8_t buf[8] = {};
+
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	ASSERT_EQ(setsockopt(self->tx, SOL_CAN_J1939, SO_J1939_SEND_PRIO,
+			     &prio, sizeof(prio)), 0)
+		TH_LOG("SO_J1939_SEND_PRIO failed: %d", errno);
+
+	ASSERT_EQ(j1939_sendto(self->tx, buf, sizeof(buf), self->ifindex,
+			       J1939_NO_NAME, SA_RX, PGN_A),
+		  (int)sizeof(buf));
+
+	EXPECT_EQ(raw_prio(self->raw, J1939_RECV_TIMEOUT_MS), PRIO_SET);
+}
+
+/* an unconnected socket can address a peer via sendto(2). */
+TEST_F(j1939_opts, sendto_unconnected)
+{
+	uint8_t tx_buf[8] = { 9, 8, 7, 6, 5, 4, 3, 2 };
+	uint8_t rx_buf[8];
+
+	self->rx = j1939_open(self->ifindex, SA_RX, PGN_A);
+	ASSERT_GE(self->rx, 0);
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+
+	ASSERT_EQ(j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			       J1939_NO_NAME, SA_RX, PGN_A),
+		  (int)sizeof(tx_buf));
+
+	ASSERT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_RECV_TIMEOUT_MS), (int)sizeof(tx_buf));
+	EXPECT_EQ(memcmp(tx_buf, rx_buf, sizeof(tx_buf)), 0);
+}
+
+/* bind(2) with a wrong address family is rejected. */
+TEST_F(j1939_opts, bind_wrong_family)
+{
+	struct sockaddr_can addr = {
+		.can_family = AF_INET,
+		.can_ifindex = self->ifindex,
+	};
+	int s;
+
+	s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
+	ASSERT_GE(s, 0);
+	EXPECT_LT(bind(s, (struct sockaddr *)&addr, sizeof(addr)), 0)
+		TH_LOG("bind with AF_INET unexpectedly succeeded");
+	close(s);
+}
+
+/* SO_J1939_ERRQUEUE delivers TX completion notifications for a TP send. */
+TEST_F(j1939_opts, errqueue_tx_completion)
+{
+	int enable = 1;
+	int ts = SOF_TIMESTAMPING_TX_ACK | SOF_TIMESTAMPING_TX_SCHED |
+		 SOF_TIMESTAMPING_OPT_ID;
+	uint8_t tx_buf[128], rx_buf[128];
+	int got_ack = 0, i;
+
+	memset(tx_buf, 0x5a, sizeof(tx_buf));
+
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	/*
+	 * setsockopt(SO_J1939_ERRQUEUE) returns the flag value (1) on success
+	 * rather than 0, so only check for a non-negative result.
+	 */
+	ASSERT_GE(setsockopt(self->tx, SOL_CAN_J1939, SO_J1939_ERRQUEUE,
+			     &enable, sizeof(enable)), 0)
+		TH_LOG("SO_J1939_ERRQUEUE failed: %d", errno);
+	ASSERT_EQ(setsockopt(self->tx, SOL_SOCKET, SO_TIMESTAMPING,
+			     &ts, sizeof(ts)), 0)
+		TH_LOG("SO_TIMESTAMPING failed: %d", errno);
+
+	/* A receiver lets the TP session complete and be acknowledged. */
+	self->rx = j1939_open(self->ifindex, SA_RX, PGN_A);
+	ASSERT_GE(self->rx, 0);
+
+	ASSERT_EQ(j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			       J1939_NO_NAME, SA_RX, PGN_A),
+		  (int)sizeof(tx_buf));
+
+	ASSERT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_RECV_TIMEOUT_MS), (int)sizeof(tx_buf));
+
+	/*
+	 * Collect the TX-completion (ACK) from the error queue. Allow a few extra
+	 * iterations because other control messages may arrive before the ACK.
+	 */
+	for (i = 0; i < ERRQUEUE_POLL_RETRIES && !got_ack; i++) {
+		struct pollfd pfd = { .fd = self->tx, .events = POLLERR };
+		union {
+			char buf[CMSG_SPACE(sizeof(struct sock_extended_err)) +
+				 CMSG_SPACE(sizeof(struct scm_timestamping))];
+			struct cmsghdr align;	/* portable cmsg alignment */
+		} ctrl;
+		char data[128];
+		struct iovec iov = { .iov_base = data, .iov_len = sizeof(data) };
+		struct msghdr msg = {
+			.msg_iov = &iov,
+			.msg_iovlen = 1,
+			.msg_control = ctrl.buf,
+			.msg_controllen = sizeof(ctrl.buf),
+		};
+		struct cmsghdr *cmsg;
+
+		if (poll(&pfd, 1, J1939_RECV_TIMEOUT_MS) <= 0)
+			break;
+		if (recvmsg(self->tx, &msg, MSG_ERRQUEUE) < 0)
+			break;
+
+		for (cmsg = CMSG_FIRSTHDR(&msg); cmsg;
+		     cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+			struct sock_extended_err *serr;
+
+			if (cmsg->cmsg_level != SOL_CAN_J1939 ||
+			    cmsg->cmsg_type != SCM_J1939_ERRQUEUE)
+				continue;
+			serr = (struct sock_extended_err *)CMSG_DATA(cmsg);
+			if (serr->ee_origin != SO_EE_ORIGIN_TIMESTAMPING)
+				continue;
+			/* The TX-completion notification must be SCM_TSTAMP_ACK. */
+			if (serr->ee_info == SCM_TSTAMP_ACK)
+				got_ack = 1;
+		}
+	}
+
+	EXPECT_EQ(got_ack, 1)
+		TH_LOG("no SCM_TSTAMP_ACK TX completion on the error queue");
+}
+
+/* getsockopt reads back SO_J1939_SEND_PRIO. */
+TEST_F(j1939_opts, getsockopt_send_prio)
+{
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	EXPECT_EQ(sockopt_roundtrip(self->tx, SO_J1939_SEND_PRIO, PRIO_SET),
+		  PRIO_SET)
+		TH_LOG("SO_J1939_SEND_PRIO round-trip failed: %d", errno);
+}
+
+/* getsockopt reads back the SO_J1939_PROMISC flag. */
+TEST_F(j1939_opts, getsockopt_promisc)
+{
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	EXPECT_EQ(sockopt_roundtrip(self->tx, SO_J1939_PROMISC, 1), 1)
+		TH_LOG("SO_J1939_PROMISC round-trip failed: %d", errno);
+}
+
+/* getsockopt reads back the SO_J1939_ERRQUEUE flag. */
+TEST_F(j1939_opts, getsockopt_errqueue)
+{
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	EXPECT_EQ(sockopt_roundtrip(self->tx, SO_J1939_ERRQUEUE, 1), 1)
+		TH_LOG("SO_J1939_ERRQUEUE round-trip failed: %d", errno);
+}
+
+J1939_TEST_MAIN();
diff --git a/tools/testing/selftests/net/can/test_j1939_opts.sh b/tools/testing/selftests/net/can/test_j1939_opts.sh
new file mode 100755
index 000000000000..5d76c56f24a4
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_opts.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+J1939_TEST_PROG=test_j1939_opts
+source "$(dirname "$0")/j1939_lib.sh"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 4/8] selftests: can: add J1939 addressing and concurrency tests
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
                   ` (2 preceding siblings ...)
  2026-10-14 15:44 ` [PATCH can-next v1 3/8] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add a selftest for J1939 NAME-based addressing and address claiming:
binding and reading back a NAME, the source address seen on the wire,
NAME-addressed transfers (with a forged Address Claimed), the Address
Claimed a socket emits, and address-claim conflicts where the contender
with the higher NAME loses and the one with the lower NAME keeps the
address. It also runs several concurrent transport sessions as a stress
check.

The NAME is serialized onto the wire byte-wise (put_name) so the tests
behave the same on big- and little-endian hosts.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/.gitignore    |   1 +
 tools/testing/selftests/net/can/Makefile      |   2 +
 .../selftests/net/can/test_j1939_addr.c       | 336 ++++++++++++++++++
 .../selftests/net/can/test_j1939_addr.sh      |   5 +
 4 files changed, 344 insertions(+)
 create mode 100644 tools/testing/selftests/net/can/test_j1939_addr.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_addr.sh

diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 427eed83e49e..7ca63a19472c 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -2,3 +2,4 @@
 test_raw_filter
 test_j1939_base
 test_j1939_opts
+test_j1939_addr
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 24a57edb912c..6e4176a1b985 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -10,6 +10,7 @@ TEST_PROGS := \
 	test_raw_filter.sh \
 	test_j1939_base.sh \
 	test_j1939_opts.sh \
+	test_j1939_addr.sh \
 
 TEST_FILES := j1939_lib.sh
 
@@ -17,5 +18,6 @@ TEST_GEN_FILES := \
 	test_raw_filter \
 	test_j1939_base \
 	test_j1939_opts \
+	test_j1939_addr \
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/test_j1939_addr.c b/tools/testing/selftests/net/can/test_j1939_addr.c
new file mode 100644
index 000000000000..8b1b093f0839
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_addr.c
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 addressing and concurrency selftests.
+ *
+ * Cover NAME-based addressing and address claiming: binding a NAME and reading
+ * it back, the bound source address appearing in the CAN id, a sender reaching a
+ * peer by NAME, address-claim contention where the lower NAME wins, and a stress
+ * run of many concurrent transport sessions.
+ *
+ * The vcan interface is set up by the caller (see test_j1939_addr.sh); its name
+ * is passed via the CANIF environment variable.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+#define SA_TX			0x80
+#define SA_RX			0x90
+#define NAME_TX			0x00000000c0ffee01ULL
+#define NAME_RX			0x00000000c0ffee02ULL
+#define PGN_TEST		0x0ab00
+#define STRESS_N		8	/* arbitrary test tuning */
+#define STRESS_SIZE		128	/* arbitrary test tuning */
+#define ACLAIM_SETTLE_US	300000	/* settle: empirical, not spec */
+
+/* Open a J1939 socket bound to (name, sa, pgn). */
+static int j1939_open_name(int ifindex, name_t name, uint8_t sa, pgn_t pgn)
+{
+	return j1939_open_flags(ifindex, name, sa, pgn, 0);
+}
+
+/*
+ * Inject an Address Claimed message (PGN 0xee00, global destination) for the
+ * given source address and NAME via a raw socket, so the kernel learns the
+ * NAME <-> address mapping. The kernel does not claim addresses itself.
+ */
+static int forge_address_claim(int raw, uint8_t sa, name_t name)
+{
+	uint8_t nb[8];
+
+	put_name(nb, name);
+	if (send_raw8(raw, J1939_PRIO_DEFAULT, J1939_PGN_ADDRESS_CLAIMED,
+		      J1939_NO_ADDR, sa, nb))
+		return -1;
+	/* give the kernel RX path a moment to process the claim */
+	usleep(ACLAIM_SETTLE_US);
+	return 0;
+}
+
+/* One independent transport-protocol round-trip; returns 0 on success. */
+static int stress_child(int ifindex, uint8_t sa_tx, uint8_t sa_rx)
+{
+	uint8_t tx_buf[STRESS_SIZE];
+	int tx, rx, rc = 1;
+
+	fill_pattern(tx_buf, sizeof(tx_buf));
+
+	rx = j1939_open_name(ifindex, J1939_NO_NAME, sa_rx, PGN_TEST);
+	tx = j1939_open_name(ifindex, J1939_NO_NAME, sa_tx, J1939_NO_PGN);
+	if (rx < 0 || tx < 0)
+		goto out;
+	if (j1939_connect_name(tx, ifindex, J1939_NO_NAME, sa_rx, PGN_TEST))
+		goto out;
+
+	if (send_recv_verify(tx, rx, tx_buf, sizeof(tx_buf),
+			     J1939_RECV_TIMEOUT_MS) == 0)
+		rc = 0;
+out:
+	if (rx >= 0)
+		close(rx);
+	if (tx >= 0)
+		close(tx);
+	return rc;
+}
+
+FIXTURE(j1939_addr)
+{
+	int ifindex;
+	int tx;
+	int rx;
+	int raw;
+};
+
+FIXTURE_SETUP(j1939_addr)
+{
+	self->tx = -1;
+	self->rx = -1;
+	self->raw = -1;
+
+	J1939_SETUP_IFINDEX(self);
+}
+
+FIXTURE_TEARDOWN(j1939_addr)
+{
+	if (self->tx >= 0)
+		close(self->tx);
+	if (self->rx >= 0)
+		close(self->rx);
+	if (self->raw >= 0)
+		close(self->raw);
+}
+
+/* bind with a NAME and read it back with getsockname(2). */
+TEST_F(j1939_addr, name_bind_getsockname)
+{
+	struct sockaddr_can addr = {};
+	socklen_t len = sizeof(addr);
+
+	self->tx = j1939_open_name(self->ifindex, NAME_TX, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0)
+		TH_LOG("bind with NAME failed: %d", errno);
+
+	ASSERT_EQ(getsockname(self->tx, (struct sockaddr *)&addr, &len), 0);
+	EXPECT_EQ(addr.can_addr.j1939.name, NAME_TX);
+	EXPECT_EQ(addr.can_addr.j1939.addr, SA_TX);
+}
+
+/* the bound source address is used as source address on the wire. */
+TEST_F(j1939_addr, source_sa_on_wire)
+{
+	uint8_t buf[8] = {};
+	struct can_frame frame;
+	int ret;
+
+	self->raw = open_raw(self->ifindex);
+	ASSERT_GE(self->raw, 0);
+
+	/* static source address: no address claim needed to transmit */
+	self->tx = j1939_open_name(self->ifindex, J1939_NO_NAME, SA_TX,
+			      J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	ASSERT_EQ(j1939_connect_name(self->tx, self->ifindex, J1939_NO_NAME,
+				SA_RX, PGN_TEST), 0);
+
+	ASSERT_EQ(send(self->tx, buf, sizeof(buf), 0), (int)sizeof(buf));
+
+	ret = recv_timeout(self->raw, &frame, sizeof(frame), J1939_RECV_TIMEOUT_MS);
+	ASSERT_GT(ret, 0);
+	EXPECT_EQ((int)J1939_ID_SA(frame.can_id), SA_TX)
+		TH_LOG("source address on wire is 0x%02x, expected 0x%02x",
+		       (unsigned int)J1939_ID_SA(frame.can_id), SA_TX);
+}
+
+/*
+ * NAME based addressing reaches the socket bound to that NAME.
+ *
+ * The peer must have claimed its address before it is reachable by NAME, so
+ * inject an Address Claimed message for (SA_RX, NAME_RX) first. The sender
+ * uses a static source address and resolves the destination by NAME.
+ */
+TEST_F(j1939_addr, name_addressing)
+{
+	uint8_t tx_buf[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
+	uint8_t rx_buf[8];
+
+	/* static receiver on SA_RX; the forged claim maps NAME_RX -> SA_RX */
+	self->rx = j1939_open_name(self->ifindex, J1939_NO_NAME, SA_RX, PGN_TEST);
+	ASSERT_GE(self->rx, 0);
+
+	self->raw = open_raw(self->ifindex);
+	ASSERT_GE(self->raw, 0);
+	ASSERT_EQ(forge_address_claim(self->raw, SA_RX, NAME_RX), 0)
+		TH_LOG("failed to inject address claim: %d", errno);
+
+	self->tx = j1939_open_name(self->ifindex, J1939_NO_NAME, SA_TX,
+			      J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+	ASSERT_EQ(j1939_connect_name(self->tx, self->ifindex, NAME_RX,
+				J1939_NO_ADDR, PGN_TEST), 0)
+		TH_LOG("connect to peer NAME failed: %d", errno);
+
+	ASSERT_EQ(send(self->tx, tx_buf, sizeof(tx_buf), 0),
+		  (int)sizeof(tx_buf))
+		TH_LOG("send to peer NAME failed: %d", errno);
+	ASSERT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_RECV_TIMEOUT_MS), (int)sizeof(tx_buf));
+	EXPECT_EQ(memcmp(tx_buf, rx_buf, sizeof(tx_buf)), 0);
+}
+
+/* many concurrent transport-protocol sessions run without corruption. */
+TEST_F(j1939_addr, stress_concurrent_sessions)
+{
+	pid_t pids[STRESS_N];
+	int i, failed = 0;
+
+	for (i = 0; i < STRESS_N; i++) {
+		pids[i] = fork();
+		ASSERT_GE(pids[i], 0);
+		if (pids[i] == 0)
+			_exit(stress_child(self->ifindex, 0x40 + i, 0x20 + i));
+	}
+
+	for (i = 0; i < STRESS_N; i++) {
+		int status;
+
+		if (waitpid(pids[i], &status, 0) < 0 ||
+		    !WIFEXITED(status) || WEXITSTATUS(status) != 0)
+			failed++;
+	}
+
+	EXPECT_EQ(failed, 0)
+		TH_LOG("%d of %d concurrent sessions failed", failed, STRESS_N);
+}
+
+#define SA_CLAIM		0x50
+#define SA_DST			0x60
+#define NAME_CLAIM		0x00000000c0ffee10ULL
+#define NAME_LOWER		0x00000000c0ffee00ULL	/* higher priority */
+#define NAME_HIGHER		0x00000000c0ffee20ULL	/* lower priority */
+
+/*
+ * Claim an address the way j1939acd would: bind a NAME to an address and send
+ * an Address Claimed message, then wait for the claim to settle. Returns the
+ * (still open) socket on success.
+ */
+static int claim_addr(int ifindex, name_t name, uint8_t sa)
+{
+	uint8_t nb[8];
+	int s;
+
+	s = j1939_open_flags(ifindex, name, sa, J1939_NO_PGN, J1939_OPEN_BCAST);
+	if (s < 0)
+		return -1;
+
+	put_name(nb, name);
+	if (j1939_sendto(s, nb, 8, ifindex, J1939_NO_NAME, J1939_NO_ADDR,
+			 J1939_PGN_ADDRESS_CLAIMED) != 8) {
+		close(s);
+		return -1;
+	}
+
+	usleep(ACLAIM_SETTLE_US);	/* settling time */
+	return s;
+}
+
+/* Send a single data frame from a socket to (SA_DST, PGN_TEST). */
+static int send_data(int s, int ifindex)
+{
+	uint8_t data[8] = {};
+
+	return j1939_sendto(s, data, sizeof(data), ifindex, J1939_NO_NAME,
+			    SA_DST, PGN_TEST);
+}
+
+static bool match_addr_claimed(canid_t id, const void *ctx)
+{
+	(void)ctx;
+	return J1939_ID_PF(id) == J1939_PF(J1939_PGN_ADDRESS_CLAIMED) &&
+	       J1939_ID_SA(id) == SA_CLAIM;
+}
+
+/* sending an Address Claimed message puts it on the wire and claims. */
+TEST_F(j1939_addr, address_claim_send)
+{
+	struct can_frame frame;
+
+	self->raw = open_raw(self->ifindex);
+	ASSERT_GE(self->raw, 0);
+
+	self->tx = claim_addr(self->ifindex, NAME_CLAIM, SA_CLAIM);
+	ASSERT_GE(self->tx, 0)
+		TH_LOG("address claim failed: %d", errno);
+
+	/* the Address Claimed frame must have appeared on the wire */
+	EXPECT_EQ(recv_match(self->raw, ACLAIM_SETTLE_US / 1000, &frame,
+			     match_addr_claimed, NULL), 0)
+		TH_LOG("no Address Claimed frame seen on the wire");
+
+	/* with the address claimed, the socket can transmit */
+	EXPECT_EQ(send_data(self->tx, self->ifindex), 8)
+		TH_LOG("transmit from claimed address failed: %d", errno);
+}
+
+/*
+ * Shared address-claim arbitration setup for the win/lose conflict tests.
+ * Claim SA_CLAIM with NAME_CLAIM, confirm we own it, then let a contender with
+ * @contender claim the same SA. On success stores the result of transmitting
+ * after the contention in *tx_ret (errno is left as that send set it) and
+ * returns 0; returns -1 if any setup step failed. Each caller asserts its own
+ * win/lose outcome on *tx_ret.
+ */
+static int ac_conflict_probe(FIXTURE_DATA(j1939_addr) *self, name_t contender,
+			     int *tx_ret)
+{
+	self->raw = open_raw(self->ifindex);
+	if (self->raw < 0)
+		return -1;
+	self->tx = claim_addr(self->ifindex, NAME_CLAIM, SA_CLAIM);
+	if (self->tx < 0)
+		return -1;
+
+	/* baseline: we own the address and can transmit */
+	if (send_data(self->tx, self->ifindex) != 8)
+		return -1;
+
+	/* let the contender claim our SA */
+	if (forge_address_claim(self->raw, SA_CLAIM, contender))
+		return -1;
+
+	*tx_ret = send_data(self->tx, self->ifindex);
+	return 0;
+}
+
+/* a higher-priority competing claim takes the address away from us. */
+TEST_F(j1939_addr, ac_conflict_lose)
+{
+	int tx_ret;
+
+	/* a contender with a lower NAME (higher priority) claims our SA */
+	ASSERT_EQ(ac_conflict_probe(self, NAME_LOWER, &tx_ret), 0);
+
+	/* we must have lost the address */
+	EXPECT_LT(tx_ret, 0)
+		TH_LOG("transmit still succeeded after losing the address");
+	EXPECT_EQ(errno, EADDRNOTAVAIL);
+}
+
+/* a lower-priority competing claim does not take our address. */
+TEST_F(j1939_addr, ac_conflict_defend)
+{
+	int tx_ret;
+
+	/* a contender with a higher NAME (lower priority) must not win */
+	ASSERT_EQ(ac_conflict_probe(self, NAME_HIGHER, &tx_ret), 0);
+
+	EXPECT_EQ(tx_ret, 8)
+		TH_LOG("lost the address to a lower-priority claim");
+}
+
+J1939_TEST_MAIN();
diff --git a/tools/testing/selftests/net/can/test_j1939_addr.sh b/tools/testing/selftests/net/can/test_j1939_addr.sh
new file mode 100755
index 000000000000..e611fe211c6d
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_addr.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+J1939_TEST_PROG=test_j1939_addr
+source "$(dirname "$0")/j1939_lib.sh"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
                   ` (3 preceding siblings ...)
  2026-10-14 15:44 ` [PATCH can-next v1 4/8] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:52   ` sashiko-bot
  2026-10-14 15:44 ` [PATCH can-next v1 6/8] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
                   ` (2 subsequent siblings)
  7 siblings, 1 reply; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add a test that pairs a J1939 socket with a CAN_RAW socket so the actual
CAN frames on the wire are checked in both directions: a J1939 send
produces the expected CAN id (priority, PGN, source and destination) and
payload, and an injected CAN frame is parsed and delivered to the J1939
socket. The base test only exercises J1939-socket-to-J1939-socket
transfers, which stay on the local delivery path inside one netdevice, so
the on-wire format is otherwise unchecked. Both PDU1 (destination
specific) and PDU2 (broadcast) single frames are covered.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/.gitignore    |   1 +
 tools/testing/selftests/net/can/Makefile      |   2 +
 .../selftests/net/can/test_j1939_wire.c       | 170 ++++++++++++++++++
 .../selftests/net/can/test_j1939_wire.sh      |   5 +
 4 files changed, 178 insertions(+)
 create mode 100644 tools/testing/selftests/net/can/test_j1939_wire.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_wire.sh

diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 7ca63a19472c..3f31fa5b7438 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -3,3 +3,4 @@ test_raw_filter
 test_j1939_base
 test_j1939_opts
 test_j1939_addr
+test_j1939_wire
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 6e4176a1b985..d80fa114a0cd 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -11,6 +11,7 @@ TEST_PROGS := \
 	test_j1939_base.sh \
 	test_j1939_opts.sh \
 	test_j1939_addr.sh \
+	test_j1939_wire.sh \
 
 TEST_FILES := j1939_lib.sh
 
@@ -19,5 +20,6 @@ TEST_GEN_FILES := \
 	test_j1939_base \
 	test_j1939_opts \
 	test_j1939_addr \
+	test_j1939_wire \
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/test_j1939_wire.c b/tools/testing/selftests/net/can/test_j1939_wire.c
new file mode 100644
index 000000000000..8f5113394f99
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.c
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 wire-format selftests: a J1939 socket against CAN_RAW.
+ *
+ * Where test_j1939_base keeps transfers on the local path between two J1939
+ * sockets, these pair a J1939 socket with a CAN_RAW socket and check the actual
+ * CAN frames on the wire both ways: a J1939 send produces the expected CAN id
+ * (priority, PGN, source and destination) and payload, and an injected CAN frame
+ * is parsed and delivered to the J1939 socket. Both destination-specific (PDU1)
+ * and broadcast (PDU2) single frames are covered.
+ *
+ * The vcan interface is set up by the caller (see test_j1939_wire.sh); its name
+ * is passed via the CANIF environment variable.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+#define SA_LOCAL		0x90	/* the J1939 socket under test */
+#define SA_PEER			0x20	/* the CAN_RAW peer */
+#define PGN_PDU1		0x0ab00	/* PF 0xab (< 240): destination specific */
+#define PGN_PDU2		0x0feee	/* PF 0xfe (>= 240): broadcast */
+#define PRIO			J1939_PRIO_DEFAULT
+
+FIXTURE(j1939_wire)
+{
+	int ifindex;
+	int j1939;
+	int raw;
+};
+
+FIXTURE_SETUP(j1939_wire)
+{
+	self->j1939 = -1;
+	self->raw = -1;
+
+	J1939_SETUP_IFINDEX(self);
+
+	J1939_SETUP_RAW(self, 0);
+}
+
+FIXTURE_TEARDOWN(j1939_wire)
+{
+	if (self->j1939 >= 0)
+		close(self->j1939);
+	if (self->raw >= 0)
+		close(self->raw);
+}
+
+/*
+ * Inject one CAN frame from the raw peer and confirm the J1939 socket receives
+ * the identical 8-byte payload. Opens the J1939 RX socket into self->j1939 (the
+ * fixture teardown closes it). @bcast selects a broadcast (PDU2) socket and
+ * frame; @pgn and @da address the injected frame. Returns 0 on success, -1
+ * otherwise; the caller ASSERTs the result.
+ */
+static int rx_single_frame_check(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+				 pgn_t pgn, uint8_t da, const uint8_t payload[8])
+{
+	uint8_t rx_buf[8];
+
+	if (bcast)
+		self->j1939 = j1939_open_flags(self->ifindex, J1939_NO_NAME,
+					       SA_LOCAL, pgn, J1939_OPEN_BCAST);
+	else
+		self->j1939 = j1939_open(self->ifindex, SA_LOCAL, pgn);
+	if (self->j1939 < 0)
+		return -1;
+
+	if (send_raw8(self->raw, PRIO, pgn, da, SA_PEER, payload))
+		return -1;
+
+	if (recv_timeout(self->j1939, rx_buf, sizeof(rx_buf),
+			 J1939_RECV_TIMEOUT_MS) != (int)sizeof(rx_buf))
+		return -1;
+
+	return memcmp(rx_buf, payload, sizeof(rx_buf)) == 0 ? 0 : -1;
+}
+
+/*
+ * Send one 8-byte payload from the J1939 socket to the raw peer and return the
+ * CAN frame seen on the wire via *frame, so each caller can assert the id fields
+ * it cares about. Opens the J1939 TX socket into self->j1939 (the fixture
+ * teardown closes it) and connects it to (@da, @pgn); @bcast opens a broadcast
+ * (PDU2) socket. Returns 0 on success, -1 otherwise; the caller ASSERTs the
+ * result.
+ */
+static int tx_to_raw_send_recv(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+			       pgn_t pgn, uint8_t da, const uint8_t payload[8],
+			       struct can_frame *frame)
+{
+	if (bcast)
+		self->j1939 = j1939_open_flags(self->ifindex, J1939_NO_NAME,
+					       SA_LOCAL, J1939_NO_PGN,
+					       J1939_OPEN_BCAST);
+	else
+		self->j1939 = j1939_open(self->ifindex, SA_LOCAL, J1939_NO_PGN);
+	if (self->j1939 < 0)
+		return -1;
+
+	if (j1939_connect(self->j1939, self->ifindex, da, pgn))
+		return -1;
+
+	if (send(self->j1939, payload, 8, 0) != 8)
+		return -1;
+
+	if (recv_timeout(self->raw, frame, sizeof(*frame),
+			 J1939_RECV_TIMEOUT_MS) <= 0)
+		return -1;
+
+	return 0;
+}
+
+/* J1939 TX -> CAN_RAW: destination-specific (PDU1) single frame. */
+TEST_F(j1939_wire, tx_pdu1_to_raw)
+{
+	uint8_t payload[8] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 };
+	struct can_frame frame;
+	canid_t id;
+
+	ASSERT_EQ(tx_to_raw_send_recv(self, false, PGN_PDU1, SA_PEER, payload,
+				      &frame), 0);
+	id = frame.can_id & CAN_EFF_MASK;
+
+	EXPECT_EQ(J1939_ID_PRIO(id), PRIO);
+	EXPECT_EQ(J1939_ID_SA(id), SA_LOCAL);			/* source address */
+	EXPECT_EQ(J1939_ID_PS(id), SA_PEER);		/* destination (PS) */
+	EXPECT_EQ(J1939_ID_PF(id), J1939_PF(PGN_PDU1));
+	ASSERT_EQ(frame.can_dlc, sizeof(payload));
+	EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
+}
+
+/* CAN_RAW -> J1939 RX: destination-specific (PDU1) single frame. */
+TEST_F(j1939_wire, rx_pdu1_from_raw)
+{
+	uint8_t payload[8] = { 0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04 };
+
+	ASSERT_EQ(rx_single_frame_check(self, false, PGN_PDU1, SA_LOCAL,
+					payload), 0);
+}
+
+/* J1939 TX -> CAN_RAW: broadcast (PDU2) single frame. */
+TEST_F(j1939_wire, tx_pdu2_to_raw)
+{
+	uint8_t payload[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
+	struct can_frame frame;
+	canid_t id;
+
+	ASSERT_EQ(tx_to_raw_send_recv(self, true, PGN_PDU2, J1939_NO_ADDR,
+				      payload, &frame), 0);
+	id = frame.can_id & CAN_EFF_MASK;
+
+	EXPECT_EQ(J1939_ID_PRIO(id), PRIO);
+	EXPECT_EQ(J1939_ID_SA(id), SA_LOCAL);			/* source address */
+	EXPECT_EQ((id >> 8) & J1939_PGN_MAX, PGN_PDU2);	/* full broadcast PGN */
+	ASSERT_EQ(frame.can_dlc, sizeof(payload));
+	EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
+}
+
+/* CAN_RAW -> J1939 RX: broadcast (PDU2) single frame. */
+TEST_F(j1939_wire, rx_pdu2_from_raw)
+{
+	uint8_t payload[8] = { 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7 };
+
+	ASSERT_EQ(rx_single_frame_check(self, true, PGN_PDU2, J1939_NO_ADDR,
+					payload), 0);
+}
+
+J1939_TEST_MAIN();
diff --git a/tools/testing/selftests/net/can/test_j1939_wire.sh b/tools/testing/selftests/net/can/test_j1939_wire.sh
new file mode 100755
index 000000000000..77a1bddb5854
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+J1939_TEST_PROG=test_j1939_wire
+source "$(dirname "$0")/j1939_lib.sh"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 6/8] selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
                   ` (4 preceding siblings ...)
  2026-10-14 15:44 ` [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 7/8] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 8/8] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add multi-frame transport tests that pair a J1939 socket with a CAN_RAW
peer. They drive the Transport Protocol (TP) and Extended Transport
Protocol (ETP) round-trips in both directions. The raw peer answers the
kernel's RTS/CTS/DPO sequencing, and the reassembled payload is checked
byte for byte.

The ETP payload spans several 255-packet blocks. In the receive
direction, the test checks the kernel-emitted EOMA on the wire (command
and size, plus the packet count for TP). In the transmit direction the
kernel sends the RTS, so the test also checks its ETP RTS message-size
field.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 tools/testing/selftests/net/can/.gitignore    |   1 +
 tools/testing/selftests/net/can/Makefile      |   2 +
 .../selftests/net/can/test_j1939_multi.c      | 453 ++++++++++++++++++
 .../selftests/net/can/test_j1939_multi.sh     |   5 +
 4 files changed, 461 insertions(+)
 create mode 100644 tools/testing/selftests/net/can/test_j1939_multi.c
 create mode 100755 tools/testing/selftests/net/can/test_j1939_multi.sh

diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 3f31fa5b7438..b6bf4c613ba3 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -4,3 +4,4 @@ test_j1939_base
 test_j1939_opts
 test_j1939_addr
 test_j1939_wire
+test_j1939_multi
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index d80fa114a0cd..559cca255875 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -12,6 +12,7 @@ TEST_PROGS := \
 	test_j1939_opts.sh \
 	test_j1939_addr.sh \
 	test_j1939_wire.sh \
+	test_j1939_multi.sh \
 
 TEST_FILES := j1939_lib.sh
 
@@ -21,5 +22,6 @@ TEST_GEN_FILES := \
 	test_j1939_opts \
 	test_j1939_addr \
 	test_j1939_wire \
+	test_j1939_multi \
 
 include ../../lib.mk
diff --git a/tools/testing/selftests/net/can/test_j1939_multi.c b/tools/testing/selftests/net/can/test_j1939_multi.c
new file mode 100644
index 000000000000..1f27ee58f48c
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_multi.c
@@ -0,0 +1,453 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 multi-frame wire selftests: TP and ETP against CAN_RAW.
+ *
+ * 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).
+ *
+ * The vcan interface is set up by the caller (see test_j1939_multi.sh); its name
+ * is passed via the CANIF environment variable.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+#define SA_LOCAL		0x90	/* the J1939 socket under test */
+#define SA_PEER			0x20	/* the CAN_RAW transport peer */
+#define PGN_DATA		0x0feef	/* transported message PGN */
+/*
+ * Priority for frames this test injects as the CAN_RAW peer; the stack
+ * does not check it. SAE J1939-21:2001 lists 7 as the TP.CM/TP.DT
+ * default; the kernel defaults its own J1939 TX to 6 (J1939_PRIO_DEFAULT).
+ */
+#define PRIO			7
+
+#define TP_CTL			0x0ec00
+#define TP_DAT			0x0eb00
+#define ETP_CTL			0x0c800
+#define ETP_DAT			0x0c700
+
+#define TP_CMD_RTS		0x10
+#define TP_CMD_CTS		0x11
+#define TP_CMD_EOMA		0x13
+#define TP_CMD_ABORT		0xff
+#define ETP_CMD_RTS		0x14
+#define ETP_CMD_CTS		0x15
+#define ETP_CMD_DPO		0x16
+#define ETP_CMD_EOMA		0x17
+
+#define TP_SIZE			200
+#define ETP_SIZE		6000		/* > TP max 1785 (255*7); several blocks */
+
+FIXTURE(j1939_multi)
+{
+	int ifindex;
+	int j1939;
+	int raw;
+};
+
+FIXTURE_SETUP(j1939_multi)
+{
+	self->j1939 = -1;
+	self->raw = -1;
+
+	J1939_SETUP_IFINDEX(self);
+
+	J1939_SETUP_RAW(self, J1939_RAW_BIG_RCVBUF);
+}
+
+FIXTURE_TEARDOWN(j1939_multi)
+{
+	if (self->j1939 >= 0)
+		close(self->j1939);
+	if (self->raw >= 0)
+		close(self->raw);
+}
+
+/*
+ * Finish a TP/ETP control frame by embedding its data PGN and sending it on the
+ * control PGN. dat must already hold the command and its command-specific bytes.
+ */
+static int send_ctl_frame(int raw, uint8_t prio, pgn_t ctl_pgn, uint8_t da,
+			  uint8_t sa, uint8_t *dat, pgn_t data_pgn)
+{
+	put_pgn(dat, data_pgn);
+	return send_raw8(raw, prio, ctl_pgn, da, sa, dat);
+}
+
+/* Build and send a TP.CM_RTS: message size, total packets and max packets/CTS. */
+static int send_tp_rts(int raw, uint8_t prio, pgn_t pgn, uint8_t da, uint8_t sa,
+		       uint16_t size, uint8_t total_pkts, uint8_t max_cts_pkts)
+{
+	uint8_t dat[8];
+
+	ctl_dat_init(dat, TP_CMD_RTS);
+	put_le16(&dat[1], size);
+	dat[3] = total_pkts;
+	dat[4] = max_cts_pkts;
+	return send_ctl_frame(raw, prio, TP_CTL, da, sa, dat, pgn);
+}
+
+/*
+ * Send a burst of @count DT frames for the block starting at the 1-based packet
+ * @next, each carrying 7 payload bytes from the matching absolute offset. The DT
+ * sequence byte differs by protocol: TP numbers packets absolutely across the
+ * whole message, ETP numbers them 1-based within each DPO block (@seq_relative
+ * selects the latter). Returns 0 on success, -1 on error.
+ */
+static int send_dt_burst(int raw, uint8_t prio, pgn_t dat_pgn, uint8_t da,
+			 uint8_t sa, unsigned int next, unsigned int count,
+			 const uint8_t *payload, size_t size, bool seq_relative)
+{
+	unsigned int i;
+
+	for (i = 0; i < count; i++) {
+		unsigned int abs_pkt = (next - 1) + i;
+		uint8_t seq = seq_relative ? (i + 1) : (abs_pkt + 1);
+
+		if (send_dt(raw, prio, dat_pgn, da, sa, seq, payload,
+			    (size_t)abs_pkt * 7, size))
+			return -1;
+	}
+	return 0;
+}
+
+/*
+ * Answer each kernel CTS with its DT burst until num_pkts are delivered.
+ * Returns the largest CTS packet count the kernel requested, or -1 on error.
+ */
+static int tp_serve_cts(int raw, uint8_t prio, uint8_t da, uint8_t sa,
+			const uint8_t *payload, size_t size, unsigned int num_pkts)
+{
+	unsigned int sent = 0, max_want = 0;
+	struct can_frame frame;
+
+	while (sent < num_pkts) {
+		unsigned int want, next;
+
+		if (recv_ctl_cmd(raw, J1939_PF(TP_CTL), sa, &frame,
+				 J1939_RECV_TIMEOUT_MS, TP_CMD_CTS))
+			return -1;
+		want = frame.data[1];
+		next = frame.data[2];		/* 1-based packet number */
+		if (want > max_want)
+			max_want = want;
+		if (send_dt_burst(raw, prio, TP_DAT, da, sa, next, want,
+				  payload, size, false))
+			return -1;
+		sent = next - 1 + want;
+	}
+	return (int)max_want;
+}
+
+/*
+ * Scatter a received DT frame's payload into buf at the offset implied by its
+ * sequence number. base_pkt is the 0-based absolute packet the 1-based seq is
+ * relative to (0 for TP, the DPO offset for ETP).
+ */
+static void unpack_dt_into(uint8_t *buf, size_t total, unsigned int base_pkt,
+			   const struct can_frame *frame)
+{
+	unsigned int abs_pkt = base_pkt + (frame->data[0] - 1);
+	size_t off = (size_t)abs_pkt * 7;
+	size_t n;
+
+	/*
+	 * A bogus sequence number (e.g. data[0] == 0 wrapping abs_pkt) can put
+	 * off past the buffer; total - off would then underflow size_t into a
+	 * huge length. Drop such frames instead of overrunning buf.
+	 */
+	if (off >= total)
+		return;
+	n = total - off;
+	if (n > 7)
+		n = 7;
+	memcpy(buf + off, &frame->data[1], n);
+}
+
+/* Open a TP RX j1939 socket and send the RTS that starts a receive session. */
+static int tp_rx_open_and_send_rts(int *j1939, int raw, int ifindex,
+				   unsigned int num_pkts,
+				   unsigned int max_cts_pkts)
+{
+	*j1939 = j1939_open(ifindex, SA_LOCAL, PGN_DATA);
+	if (*j1939 < 0)
+		return -1;
+	return send_tp_rts(raw, PRIO, PGN_DATA, SA_LOCAL, SA_PEER, TP_SIZE,
+			   num_pkts, max_cts_pkts);
+}
+
+/*
+ * Shared TP-RX round-trip: open the j1939 socket, drive a full TP receive from
+ * the raw peer advertising max_cts_pkts per CTS, and verify the reassembled
+ * payload against the sent pattern. The kernel's largest single-CTS packet
+ * request is returned via *max_want_out and its EOMA frame via *eoma_out, so
+ * each caller can assert the specific property it cares about. ASSERT_* cannot
+ * be used here (the harness jump target lives in the TEST body), so failures
+ * are reported through the return value. Returns 0 on success, -1 otherwise.
+ */
+static int tp_rx_roundtrip(FIXTURE_DATA(j1939_multi) *self,
+			   unsigned int max_cts_pkts, int *max_want_out,
+			   struct can_frame *eoma_out)
+{
+	uint8_t payload[TP_SIZE], rx_buf[TP_SIZE];
+	unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+	int max_want;
+
+	fill_pattern(payload, sizeof(payload));
+
+	if (tp_rx_open_and_send_rts(&self->j1939, self->raw, self->ifindex,
+				    num_pkts, max_cts_pkts))
+		return -1;
+
+	max_want = tp_serve_cts(self->raw, PRIO, SA_LOCAL, SA_PEER,
+				payload, TP_SIZE, num_pkts);
+	if (max_want < 0)
+		return -1;
+	*max_want_out = max_want;
+
+	if (recv_ctl_cmd(self->raw, J1939_PF(TP_CTL), SA_PEER, eoma_out,
+			 J1939_RECV_TIMEOUT_MS, TP_CMD_EOMA))
+		return -1;
+
+	if (recv_timeout(self->j1939, rx_buf, sizeof(rx_buf),
+			 J1939_RECV_TIMEOUT_MS) != (int)sizeof(rx_buf))
+		return -1;
+	if (memcmp(payload, rx_buf, sizeof(payload)) != 0)
+		return -1;
+
+	return 0;
+}
+
+/*
+ * TP RX: the CAN_RAW peer transmits a TP message to the J1939 socket.
+ * raw sends RTS, then answers each CTS from the kernel with DT frames.
+ */
+TEST_F(j1939_multi, tp_rx_from_raw)
+{
+	unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+	struct can_frame frame;
+	int max_want;
+
+	ASSERT_EQ(tp_rx_roundtrip(self, num_pkts, &max_want, &frame), 0)
+		TH_LOG("TP RX round-trip against the raw peer failed");
+
+	/*
+	 * The kernel (TP receiver) must acknowledge with a well-formed EOMA on
+	 * the wire: command, the 16-bit message size and the total packet count.
+	 */
+	EXPECT_EQ(get_le16(&frame.data[1]), TP_SIZE);
+	EXPECT_EQ(frame.data[3], num_pkts);
+}
+
+/*
+ * Start a TP/ETP transmit session from the J1939 socket: open an unbound
+ * socket, connect to the peer, send the payload and collect the kernel's RTS
+ * from the raw peer (ctl_pf/rts_cmd select TP or ETP). *j1939 is set even on
+ * open failure so the fixture teardown can close it. Returns 0 on success, -1
+ * otherwise; the caller ASSERTs the result.
+ */
+static int tx_start_session(int *j1939, int ifindex, const void *payload,
+			    size_t len, int raw, uint8_t ctl_pf,
+			    uint8_t rts_cmd, struct can_frame *rts)
+{
+	*j1939 = j1939_open(ifindex, SA_LOCAL, J1939_NO_PGN);
+	if (*j1939 < 0)
+		return -1;
+	if (j1939_connect(*j1939, ifindex, SA_PEER, PGN_DATA))
+		return -1;
+	if (send(*j1939, payload, len, 0) != (ssize_t)len)
+		return -1;
+	return recv_ctl_cmd(raw, ctl_pf, SA_PEER, rts,
+			    J1939_RECV_TIMEOUT_MS, rts_cmd);
+}
+
+/*
+ * TP TX: the J1939 socket transmits a TP message to the CAN_RAW peer.
+ * raw answers the kernel's RTS with a CTS, collects the DT frames and
+ * acknowledges with EOMA.
+ */
+TEST_F(j1939_multi, tp_tx_to_raw)
+{
+	uint8_t payload[TP_SIZE], rx_buf[TP_SIZE];
+	unsigned int num_pkts = J1939_NUM_PKTS(TP_SIZE);
+	struct can_frame frame;
+	uint8_t dat[8];
+	unsigned int got = 0;
+
+	fill_pattern(payload, sizeof(payload));
+	memset(rx_buf, 0, sizeof(rx_buf));
+
+	ASSERT_EQ(tx_start_session(&self->j1939, self->ifindex, payload,
+				   sizeof(payload), self->raw, J1939_PF(TP_CTL),
+				   TP_CMD_RTS, &frame), 0);
+	/* The kernel's TP RTS must carry the message size and packet count. */
+	EXPECT_EQ(get_le16(&frame.data[1]), TP_SIZE);
+	EXPECT_EQ(frame.data[3], num_pkts);
+
+	/* request all packets in one CTS */
+	ctl_dat_init(dat, TP_CMD_CTS);
+	dat[1] = num_pkts;
+	dat[2] = 1;
+	ASSERT_EQ(send_ctl_frame(self->raw, PRIO, TP_CTL, SA_LOCAL, SA_PEER,
+				 dat, PGN_DATA), 0);
+
+	while (got < num_pkts) {
+		ASSERT_EQ(recv_for_peer(self->raw, J1939_PF(TP_DAT), SA_PEER, &frame,
+					J1939_RECV_TIMEOUT_MS), 0)
+			TH_LOG("missing DT after %u/%u packets", got, num_pkts);
+		unpack_dt_into(rx_buf, TP_SIZE, 0, &frame);
+		got++;
+	}
+
+	/* acknowledge with EOMA */
+	ctl_dat_init(dat, TP_CMD_EOMA);
+	put_le16(&dat[1], TP_SIZE);
+	dat[3] = num_pkts;
+	ASSERT_EQ(send_ctl_frame(self->raw, PRIO, TP_CTL, SA_LOCAL, SA_PEER,
+				 dat, PGN_DATA), 0);
+
+	EXPECT_EQ(memcmp(payload, rx_buf, sizeof(payload)), 0);
+}
+
+/*
+ * 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
+ * the block's DT frames. The payload spans several 255-packet blocks.
+ */
+TEST_F(j1939_multi, etp_rx_from_raw)
+{
+	unsigned int num_pkts = J1939_NUM_PKTS(ETP_SIZE);
+	uint8_t *payload, *rx_buf;
+	struct can_frame frame;
+	unsigned int sent = 0;
+	uint8_t dat[8];
+
+	payload = malloc(ETP_SIZE);
+	rx_buf = malloc(ETP_SIZE);
+	ASSERT_NE(payload, NULL);
+	ASSERT_NE(rx_buf, NULL);
+	fill_pattern(payload, ETP_SIZE);
+
+	self->j1939 = j1939_open(self->ifindex, SA_LOCAL, PGN_DATA);
+	ASSERT_GE(self->j1939, 0);
+
+	ctl_dat_init(dat, ETP_CMD_RTS);
+	put_le32(&dat[1], ETP_SIZE);
+	ASSERT_EQ(send_ctl_frame(self->raw, PRIO, ETP_CTL, SA_LOCAL, SA_PEER,
+				 dat, PGN_DATA), 0);
+
+	while (sent < num_pkts) {
+		unsigned int num, next;
+
+		ASSERT_EQ(recv_ctl_cmd(self->raw, J1939_PF(ETP_CTL), SA_PEER,
+				       &frame, J1939_RECV_TIMEOUT_MS,
+				       ETP_CMD_CTS), 0)
+			TH_LOG("no ETP CTS after %u/%u packets", sent,
+			       num_pkts);
+		num = frame.data[1];
+		next = get_le24(&frame.data[2]);	/* 1-based */
+
+		ctl_dat_init(dat, ETP_CMD_DPO);
+		dat[1] = num;
+		put_le24(&dat[2], next - 1);
+		ASSERT_EQ(send_ctl_frame(self->raw, PRIO, ETP_CTL, SA_LOCAL,
+					 SA_PEER, dat, PGN_DATA), 0);
+
+		/* ETP numbers DT packets 1-based within each DPO block */
+		ASSERT_EQ(send_dt_burst(self->raw, PRIO, ETP_DAT, SA_LOCAL,
+					SA_PEER, next, num, payload, ETP_SIZE,
+					true), 0);
+		sent = (next - 1) + num;
+	}
+
+	/*
+	 * The kernel (ETP receiver) must acknowledge with a well-formed ETP EOMA
+	 * carrying the 32-bit total message size in bytes 1..4.
+	 */
+	ASSERT_EQ(recv_ctl_cmd(self->raw, J1939_PF(ETP_CTL), SA_PEER, &frame,
+			       J1939_RECV_TIMEOUT_MS, ETP_CMD_EOMA), 0)
+		TH_LOG("no well-formed ETP EOMA from kernel after last DT");
+	EXPECT_EQ(get_le32(&frame.data[1]), (uint32_t)ETP_SIZE);
+
+	ASSERT_EQ(recv_timeout(self->j1939, rx_buf, ETP_SIZE,
+			       J1939_RECV_TIMEOUT_MS), (int)ETP_SIZE);
+	EXPECT_EQ(memcmp(payload, rx_buf, ETP_SIZE), 0);
+
+	free(payload);
+	free(rx_buf);
+}
+
+/*
+ * ETP TX: the J1939 socket transmits an ETP message to the CAN_RAW peer.
+ * raw requests the data in 255-packet blocks (ETP.CTS); for each block the
+ * kernel sends a DPO followed by the DT frames, acknowledged by EOMA.
+ */
+TEST_F(j1939_multi, etp_tx_to_raw)
+{
+	unsigned int num_pkts = J1939_NUM_PKTS(ETP_SIZE);
+	const unsigned int block = 255;
+	uint8_t *payload, *rx_buf;
+	struct can_frame frame;
+	unsigned int got = 0;
+	uint8_t dat[8];
+
+	payload = malloc(ETP_SIZE);
+	rx_buf = malloc(ETP_SIZE);
+	ASSERT_NE(payload, NULL);
+	ASSERT_NE(rx_buf, NULL);
+	fill_pattern(payload, ETP_SIZE);
+	memset(rx_buf, 0, ETP_SIZE);
+
+	ASSERT_EQ(tx_start_session(&self->j1939, self->ifindex, payload,
+				   ETP_SIZE, self->raw, J1939_PF(ETP_CTL),
+				   ETP_CMD_RTS, &frame), 0);
+	/* The kernel's ETP RTS must carry the correct 32-bit message size. */
+	EXPECT_EQ(get_le32(&frame.data[1]), (uint32_t)ETP_SIZE);
+
+	while (got < num_pkts) {
+		unsigned int req = num_pkts - got;
+		unsigned int next = got + 1;		/* 1-based */
+		unsigned int i, dpo_num, dpo_off;
+
+		if (req > block)
+			req = block;
+
+		ctl_dat_init(dat, ETP_CMD_CTS);
+		dat[1] = req;
+		put_le24(&dat[2], next);
+		ASSERT_EQ(send_ctl_frame(self->raw, PRIO, ETP_CTL, SA_LOCAL,
+					 SA_PEER, dat, PGN_DATA), 0);
+
+		ASSERT_EQ(recv_ctl_cmd(self->raw, J1939_PF(ETP_CTL), SA_PEER,
+				       &frame, J1939_RECV_TIMEOUT_MS,
+				       ETP_CMD_DPO), 0)
+			TH_LOG("no DPO after %u/%u packets", got, num_pkts);
+		dpo_num = frame.data[1];
+		dpo_off = get_le24(&frame.data[2]);
+
+		for (i = 0; i < dpo_num; i++) {
+			ASSERT_EQ(recv_for_peer(self->raw, J1939_PF(ETP_DAT),
+						SA_PEER, &frame,
+						J1939_RECV_TIMEOUT_MS), 0)
+				TH_LOG("missing ETP DT %u in block", i);
+			unpack_dt_into(rx_buf, ETP_SIZE, dpo_off, &frame);
+		}
+		got += dpo_num;
+	}
+
+	ctl_dat_init(dat, ETP_CMD_EOMA);
+	put_le32(&dat[1], ETP_SIZE);
+	ASSERT_EQ(send_ctl_frame(self->raw, PRIO, ETP_CTL, SA_LOCAL, SA_PEER,
+				 dat, PGN_DATA), 0);
+
+	EXPECT_EQ(memcmp(payload, rx_buf, ETP_SIZE), 0);
+
+	free(payload);
+	free(rx_buf);
+}
+
+J1939_TEST_MAIN();
diff --git a/tools/testing/selftests/net/can/test_j1939_multi.sh b/tools/testing/selftests/net/can/test_j1939_multi.sh
new file mode 100755
index 000000000000..53f473588154
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_multi.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+J1939_TEST_PROG=test_j1939_multi
+source "$(dirname "$0")/j1939_lib.sh"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 7/8] selftests: can: cover J1939 transport abort and timeout paths
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
                   ` (5 preceding siblings ...)
  2026-10-14 15:44 ` [PATCH can-next v1 6/8] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  2026-10-14 15:44 ` [PATCH can-next v1 8/8] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

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@pengutronix.de>
---
 .../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


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH can-next v1 8/8] selftests: can: check SO_J1939_PROMISC receive effect
  2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
                   ` (6 preceding siblings ...)
  2026-10-14 15:44 ` [PATCH can-next v1 7/8] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
@ 2026-10-14 15:44 ` Oleksij Rempel
  7 siblings, 0 replies; 10+ messages in thread
From: Oleksij Rempel @ 2026-10-14 15:44 UTC (permalink / raw)
  To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde,
	Shuah Khan
  Cc: Oleksij Rempel, kernel, linux-can, linux-kernel, linux-kselftest,
	Alexander Hölzl, Quchaosheng, Tetsuo Handa

Add a test that exercises the SO_J1939_PROMISC receive effect. The
existing coverage only round-trips the flag through set/getsockopt, so a
kernel that stores it but never acts on it in the RX path stays green.

The test binds an rx socket to a specific (address, PGN), checks that a
frame to a different destination address and PGN is not delivered, then
enables SO_J1939_PROMISC and checks the same otherwise-unmatched frame is
now delivered unchanged.

Assisted-by: LLM
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
 .../selftests/net/can/test_j1939_opts.c       | 46 ++++++++++++++++++-
 1 file changed, 44 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/net/can/test_j1939_opts.c b/tools/testing/selftests/net/can/test_j1939_opts.c
index f761a42db213..3afda9b4e4a8 100644
--- a/tools/testing/selftests/net/can/test_j1939_opts.c
+++ b/tools/testing/selftests/net/can/test_j1939_opts.c
@@ -5,8 +5,8 @@
  * On top of the basic transport, cover the user-visible J1939 socket options and
  * addressing: the SO_J1939_FILTER PGN whitelist, the SO_J1939_SEND_PRIO send
  * priority seen in the CAN id (via a CAN_RAW peer, including the default of 6),
- * unconnected sendto(2), a wrong-family bind rejection, and SO_J1939_ERRQUEUE
- * TX-completion notifications.
+ * unconnected sendto(2), a wrong-family bind rejection, SO_J1939_ERRQUEUE
+ * TX-completion notifications, and the SO_J1939_PROMISC receive effect.
  *
  * The vcan interface is set up by the caller (see test_j1939_opts.sh); its name
  * is passed via the CANIF environment variable.
@@ -22,6 +22,7 @@
 
 #define SA_TX			0x80
 #define SA_RX			0x90
+#define SA_OTHER		0x50
 #define PGN_A			0x0ab00
 #define PGN_B			0x0cd00
 #define PRIO_SET		3
@@ -123,6 +124,47 @@ TEST_F(j1939_opts, filter_pgn)
 		TH_LOG("PGN_B was received but should have been filtered out");
 }
 
+/* SO_J1939_PROMISC delivers a frame that does not match the bound (addr,pgn). */
+TEST_F(j1939_opts, promisc_receive)
+{
+	uint8_t tx_buf[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
+	uint8_t rx_buf[8];
+	int enable = 1;
+	int ret;
+
+	self->rx = j1939_open(self->ifindex, SA_RX, PGN_A);
+	ASSERT_GE(self->rx, 0);
+	self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+	ASSERT_GE(self->tx, 0);
+
+	/*
+	 * This frame uses a different address and PGN than rx is bound to.
+	 * Without promisc, rx must not receive it.
+	 */
+	ret = j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			   J1939_NO_NAME, SA_OTHER, PGN_B);
+	ASSERT_EQ(ret, (int)sizeof(tx_buf))
+		TH_LOG("sendto unmatched frame failed: %d", errno);
+	EXPECT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_NEG_RECV_TIMEOUT_MS), -1)
+		TH_LOG("unmatched frame was received without promisc");
+
+	/* SO_J1939_PROMISC returns the flag value (1), not 0, on success. */
+	ASSERT_GE(setsockopt(self->rx, SOL_CAN_J1939, SO_J1939_PROMISC,
+			     &enable, sizeof(enable)), 0)
+		TH_LOG("SO_J1939_PROMISC failed: %d", errno);
+
+	/* The same otherwise-unmatched frame must now be delivered. */
+	ret = j1939_sendto(self->tx, tx_buf, sizeof(tx_buf), self->ifindex,
+			   J1939_NO_NAME, SA_OTHER, PGN_B);
+	ASSERT_EQ(ret, (int)sizeof(tx_buf))
+		TH_LOG("sendto unmatched frame failed: %d", errno);
+	ASSERT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+			       J1939_RECV_TIMEOUT_MS), (int)sizeof(tx_buf))
+		TH_LOG("unmatched frame not received despite promisc");
+	EXPECT_EQ(memcmp(tx_buf, rx_buf, sizeof(tx_buf)), 0);
+}
+
 /* without SO_J1939_SEND_PRIO the J1939 default priority (6) is used. */
 TEST_F(j1939_opts, send_prio_default)
 {
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW
  2026-10-14 15:44 ` [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
@ 2026-10-14 15:52   ` sashiko-bot
  0 siblings, 0 replies; 10+ messages in thread
From: sashiko-bot @ 2026-10-14 15:52 UTC (permalink / raw)
  To: Oleksij Rempel
  Cc: linux-can, Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/can: test_j1939_wire missing assertion for CAN_EFF_FLAG on transmitted J1939 frames
--

commit 19b14aab4922e084b42afa782b6d7a0743ee25d8
Author: Oleksij Rempel <o.rempel@pengutronix.de>

selftests: can: add J1939 wire-format tests against CAN_RAW

Add a test that pairs a J1939 socket with a CAN_RAW socket so the actual
CAN frames on the wire are checked in both directions. Both PDU1 (destination
specific) and PDU2 (broadcast) single frames are covered.

> diff --git a/tools/testing/selftests/net/can/test_j1939_wire.c b/tools/testing/selftests/net/can/test_j1939_wire.c
> new file mode 100644
> index 0000000000000..8f5113394f990
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/test_j1939_wire.c

[ ... ]

> +/* J1939 TX -> CAN_RAW: destination-specific (PDU1) single frame. */
> +TEST_F(j1939_wire, tx_pdu1_to_raw)
> +{
> +	uint8_t payload[8] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 };
> +	struct can_frame frame;
> +	canid_t id;
> +
> +	ASSERT_EQ(tx_to_raw_send_recv(self, false, PGN_PDU1, SA_PEER, payload,
> +				      &frame), 0);
> +	id = frame.can_id & CAN_EFF_MASK;

[Severity: Medium]
Does this code need to assert that CAN_EFF_FLAG is set before masking it out?

By applying CAN_EFF_MASK, the extended frame flag is dropped. Should there be
a check to verify that the transmitted J1939 frames are actually extended
frames, rather than standard frames that happen to match the lower bits?

> +
> +	EXPECT_EQ(J1939_ID_PRIO(id), PRIO);
> +	EXPECT_EQ(J1939_ID_SA(id), SA_LOCAL);			/* source address */
> +	EXPECT_EQ(J1939_ID_PS(id), SA_PEER);		/* destination (PS) */
> +	EXPECT_EQ(J1939_ID_PF(id), J1939_PF(PGN_PDU1));
> +	ASSERT_EQ(frame.can_dlc, sizeof(payload));
> +	EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
> +}

[ ... ]

> +/* J1939 TX -> CAN_RAW: broadcast (PDU2) single frame. */
> +TEST_F(j1939_wire, tx_pdu2_to_raw)
> +{
> +	uint8_t payload[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
> +	struct can_frame frame;
> +	canid_t id;
> +
> +	ASSERT_EQ(tx_to_raw_send_recv(self, true, PGN_PDU2, J1939_NO_ADDR,
> +				      payload, &frame), 0);
> +	id = frame.can_id & CAN_EFF_MASK;

[Severity: Medium]
Similarly, should this test in tx_pdu2_to_raw() also assert that the
CAN_EFF_FLAG is set on the received frame.can_id?

> +
> +	EXPECT_EQ(J1939_ID_PRIO(id), PRIO);
> +	EXPECT_EQ(J1939_ID_SA(id), SA_LOCAL);			/* source address */
> +	EXPECT_EQ((id >> 8) & J1939_PGN_MAX, PGN_PDU2);	/* full broadcast PGN */
> +	ASSERT_EQ(frame.can_dlc, sizeof(payload));
> +	EXPECT_EQ(memcmp(frame.data, payload, sizeof(payload)), 0);
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261014154435.3577983-1-o.rempel@pengutronix.de?part=5

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-14 15:52 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-14 15:44 [PATCH can-next v1 0/8] selftests: can: add J1939 socket and transport tests Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 1/8] selftests: can: add J1939 test helper header and library Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 2/8] selftests: can: add J1939 base functional tests Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 3/8] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 4/8] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 5/8] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
2026-10-14 15:52   ` sashiko-bot
2026-10-14 15:44 ` [PATCH can-next v1 6/8] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 7/8] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
2026-10-14 15:44 ` [PATCH can-next v1 8/8] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox