[PATCH can-next v1 4/8] selftests: can: add J1939 addressing and concurrency tests

From: Oleksij Rempel

Date: Wed Oct 14 2026 - 12:24:07 EST


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@xxxxxxxxxxxxxx>
---
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