* [PATCH can-next v3 1/9] selftests: can: install net/lib.sh with the CAN tests
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 2/9] selftests: can: add J1939 test helper header and library Oleksij Rempel
` (7 subsequent siblings)
8 siblings, 0 replies; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add ../lib.sh and ../lib/sh/*.sh to TEST_INCLUDES. net/lib.sh sources
lib/sh/defer.sh. The install needs lib.sh and the lib/sh scripts.
test_raw_filter.sh sources ../lib.sh, but the Makefile does not install
lib.sh. A full selftest install works, because the net tests install
lib.sh. An install of only net/can does not work. The installed
test_raw_filter.sh cannot find lib.sh.
Found by installing the tests with "make TARGETS=net/can install". With
this change the installed test_raw_filter.sh runs.
Fixes: 77442ffa83e8 ("selftests: can: Import tst-filter from can-tests")
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Changes in v3:
- No changes.
Changes in v2:
- New in v2. An install of net/can on its own left out net/lib.sh
(sashiko).
---
tools/testing/selftests/net/can/Makefile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 5b82e60a03e75ace2cae949ceeab09a497c96074..3adaa13dcbfa303c4be2bb6a9b5dbdac76d68260 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_INCLUDES := ../lib.sh $(wildcard ../lib/sh/*.sh)
+
TEST_GEN_FILES := test_raw_filter
include ../../lib.mk
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH can-next v3 2/9] selftests: can: add J1939 test helper header and library
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 1/9] selftests: can: install net/lib.sh with the CAN tests Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 3/9] selftests: can: add J1939 base functional tests Oleksij Rempel
` (6 subsequent siblings)
8 siblings, 0 replies; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add code shared by the later J1939 selftests. j1939_helpers.h and
j1939_helpers.c hold the C helpers for socket setup, timed receive and
CAN id encoding. They also hold the helpers for TP and ETP frames. Only
the multi-frame tests build those frames. j1939_tests.h holds the parts
that use the kselftest harness: the fixture setup macros and
j1939_test_main(). j1939_lib.sh holds the vcan setup and cleanup and the
test runner. A wrapper sources j1939_lib.sh and runs the test binary of
the same name.
j1939_lib.sh runs the tests only on a vcan that it creates itself. The
tests that follow send frames as other nodes and forge Address Claims, so
they must not run on a real CAN bus.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q: can timespec_diff_ms() overflow on 32-bit, where long is 32 bits?
A: No. A static assert pins NSEC_PER_SEC and NSEC_PER_MSEC to long
long, so the product is computed in long long; the result in ms fits
a long.
Changes in v3:
- say which protocol is missing before skipping (sashiko)
- say in the J1939_RECV_TIMEOUT_MS comment that it covers whole messages
too
- say whether CANIF already exists or cannot be created
- add static asserts for the type of NSEC_PER_SEC and NSEC_PER_MSEC and
for the control-frame layout
- fix comments that did not match the code
- do not repeat the test name in the SKIP line
- move the helper code from the header to j1939_helpers.c and
j1939_helpers.h; j1939_tests.h keeps the parts that use the harness
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- replace the J1939_TEST_MAIN() macro with j1939_test_main(), called
from a small main() in each test
- fix a -Wstringop-truncation warning in j1939_if_index()
- add a relaxed (default) and a strict (J1939_STRICT_TIMING) timing mode
- skip, not fail, when the kernel lacks CAN_RAW (sashiko)
- name all constants and add static asserts for dependent values
- return bool from success/failure helpers named *_ok(), avoid casts
- receive into a buffer one byte longer than expected, so that a longer
message fails the check (sashiko)
- j1939_lib.sh: run only on a vcan it creates, drop the real CAN path
(sashiko)
- j1939_lib.sh: trap only EXIT (sashiko)
- j1939_lib.sh: run the test binary from the script directory
- j1939_lib.sh: fix the shellcheck warnings
- j1939_lib.sh: take the test binary name from the wrapper name, and say
why the setup is skipped
---
tools/testing/selftests/net/can/Makefile | 2 +
tools/testing/selftests/net/can/j1939_helpers.c | 461 ++++++++++++++++++++++++
tools/testing/selftests/net/can/j1939_helpers.h | 337 +++++++++++++++++
tools/testing/selftests/net/can/j1939_lib.sh | 92 +++++
tools/testing/selftests/net/can/j1939_tests.h | 71 ++++
5 files changed, 963 insertions(+)
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 3adaa13dcbfa303c4be2bb6a9b5dbdac76d68260..953f4538740984d7342f6e264ac13fadc0790ed2 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_INCLUDES := ../lib.sh $(wildcard ../lib/sh/*.sh)
TEST_GEN_FILES := test_raw_filter
diff --git a/tools/testing/selftests/net/can/j1939_helpers.c b/tools/testing/selftests/net/can/j1939_helpers.c
new file mode 100644
index 0000000000000000000000000000000000000000..2457513c559c50acbba409c6b4c429a79b7798e0
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_helpers.c
@@ -0,0 +1,461 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/* Code of the shared J1939 selftest helpers, see j1939_helpers.h. */
+#include "j1939_helpers.h"
+
+char CANIF[IFNAMSIZ];
+
+int j1939_id_prio(canid_t id)
+{
+ return (id >> J1939_ID_PRIO_SHIFT) & J1939_ID_PRIO_MASK;
+}
+
+int j1939_id_pgn(canid_t id)
+{
+ return (id >> J1939_ID_PGN_SHIFT) & J1939_PGN_MAX;
+}
+
+int j1939_id_pf(canid_t id)
+{
+ return J1939_PF(j1939_id_pgn(id));
+}
+
+int j1939_id_ps(canid_t id)
+{
+ return j1939_id_pgn(id) & UINT8_MAX;
+}
+
+int j1939_id_sa(canid_t id)
+{
+ return id & UINT8_MAX;
+}
+
+bool j1939_is_data_frame(canid_t can_id)
+{
+ return (can_id & (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_ERR_FLAG)) ==
+ CAN_EFF_FLAG;
+}
+
+int j1939_timing_ms(int relaxed_ms, int strict_ms)
+{
+ return getenv("J1939_STRICT_TIMING") ? strict_ms : relaxed_ms;
+}
+
+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;
+ snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", CANIF);
+ if (ioctl(s, SIOCGIFINDEX, &ifr) >= 0)
+ idx = ifr.ifr_ifindex;
+ close(s);
+ return idx;
+}
+
+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;
+}
+
+int j1939_open(int ifindex, uint8_t sa, pgn_t pgn)
+{
+ return j1939_open_flags(ifindex, J1939_NO_NAME, sa, pgn,
+ J1939_OPEN_NONE);
+}
+
+bool j1939_connect_name_ok(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));
+}
+
+bool j1939_connect_ok(int s, int ifindex, uint8_t da, pgn_t pgn)
+{
+ return j1939_connect_name_ok(s, ifindex, J1939_NO_NAME, da, pgn);
+}
+
+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;
+}
+
+ssize_t j1939_sendto_addr(int s, const void *buf, size_t len,
+ const struct sockaddr_can *dst)
+{
+ return sendto(s, buf, len, 0, (const struct sockaddr *)dst,
+ sizeof(*dst));
+}
+
+ssize_t 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 j1939_sendto_addr(s, buf, len, &dst);
+}
+
+bool j1939_sendto_ok(int s, const void *buf, size_t len,
+ int ifindex, name_t name, uint8_t da,
+ pgn_t pgn)
+{
+ ssize_t want_len = len;
+
+ return j1939_sendto(s, buf, len, ifindex, name, da, pgn) == want_len;
+}
+
+bool send_all_ok(int s, const void *buf, size_t len)
+{
+ ssize_t want_len = len;
+
+ return send(s, buf, len, 0) == want_len;
+}
+
+bool write_all_ok(int fd, const void *buf, size_t len)
+{
+ ssize_t want_len = len;
+
+ return write(fd, buf, len) == want_len;
+}
+
+bool read_all_ok(int fd, void *buf, size_t len)
+{
+ ssize_t want_len = len;
+
+ return read(fd, buf, len) == want_len;
+}
+
+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 != J1939_RAW_RCVBUF_DEFAULT &&
+ 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;
+}
+
+int open_raw(int ifindex)
+{
+ return open_raw_rcvbuf(ifindex, J1939_RAW_RCVBUF_DEFAULT);
+}
+
+ssize_t 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);
+}
+
+bool recvfrom_verify_ok(int s, const void *want, size_t len,
+ int timeout_ms, struct sockaddr_can *src)
+{
+ struct pollfd pfd = { .fd = s, .events = POLLIN };
+ socklen_t src_len = sizeof(*src);
+ size_t rx_len = len + 1;
+ uint8_t *rx_buf = malloc(rx_len);
+ ssize_t want_len = len, n = -1;
+ bool ok;
+
+ if (!rx_buf)
+ return false;
+
+ if (poll(&pfd, 1, timeout_ms) > 0)
+ n = recvfrom(s, rx_buf, rx_len, 0, (struct sockaddr *)src,
+ &src_len);
+ ok = n == want_len && !memcmp(want, rx_buf, len);
+
+ free(rx_buf);
+ return ok;
+}
+
+bool recv_verify_ok(int s, const void *want, size_t len,
+ int timeout_ms)
+{
+ return recvfrom_verify_ok(s, want, len, timeout_ms, NULL);
+}
+
+bool send_recv_verify_ok(int tx, int rx, const void *buf,
+ size_t len, int timeout_ms)
+{
+ if (!send_all_ok(tx, buf, len))
+ return false;
+ return recv_verify_ok(rx, buf, len, timeout_ms);
+}
+
+bool recv_frame_ok(int raw, struct can_frame *frame,
+ int timeout_ms)
+{
+ ssize_t want_len = sizeof(*frame);
+
+ return recv_timeout(raw, frame, sizeof(*frame), timeout_ms) == want_len;
+}
+
+canid_t j1939_canid(uint8_t prio, pgn_t pgn, uint8_t da,
+ uint8_t sa)
+{
+ canid_t id = CAN_EFF_FLAG | (prio << J1939_ID_PRIO_SHIFT) |
+ (pgn << J1939_ID_PGN_SHIFT) | sa;
+
+ /* PDU1: the destination goes into the PS byte; PDU2 ignores da */
+ if (J1939_IS_PDU1(pgn))
+ id |= da << J1939_ID_PGN_SHIFT;
+ return id;
+}
+
+void cm_dat_init(uint8_t *dat, uint8_t cmd)
+{
+ memset(dat, J1939_FILL_BYTE, CAN_MAX_DLEN);
+ dat[J1939_CM_CMD_OFF] = cmd;
+}
+
+void put_name(uint8_t *dat, name_t name)
+{
+ size_t i;
+
+ for (i = 0; i < sizeof(name); i++)
+ dat[i] = name >> (CHAR_BIT * i);
+}
+
+void put_le(uint8_t *p, uint32_t v, size_t n)
+{
+ size_t i;
+
+ for (i = 0; i < n; i++)
+ p[i] = v >> (CHAR_BIT * i);
+}
+
+uint32_t get_le(const uint8_t *p, size_t n)
+{
+ uint32_t v = 0;
+ size_t i;
+
+ for (i = 0; i < n; i++) {
+ uint32_t byte = p[i];
+
+ v |= byte << (CHAR_BIT * i);
+ }
+ return v;
+}
+
+void put_le16(uint8_t *p, uint16_t v)
+{
+ put_le(p, v, sizeof(v));
+}
+
+void put_le24(uint8_t *p, uint32_t v)
+{
+ put_le(p, v, J1939_LE24_LEN);
+}
+
+void put_le32(uint8_t *p, uint32_t v)
+{
+ put_le(p, v, sizeof(v));
+}
+
+uint16_t get_le16(const uint8_t *p)
+{
+ return get_le(p, sizeof(uint16_t));
+}
+
+uint32_t get_le24(const uint8_t *p)
+{
+ return get_le(p, J1939_LE24_LEN);
+}
+
+uint32_t get_le32(const uint8_t *p)
+{
+ return get_le(p, sizeof(uint32_t));
+}
+
+void put_pgn(uint8_t *dat, pgn_t pgn)
+{
+ put_le24(&dat[J1939_CM_PGN_OFF], pgn);
+}
+
+bool send_raw8_ok(int raw, uint8_t prio, pgn_t pgn, uint8_t da,
+ uint8_t sa, const uint8_t dat[CAN_MAX_DLEN])
+{
+ struct can_frame f = {};
+
+ f.can_id = j1939_canid(prio, pgn, da, sa);
+ f.can_dlc = CAN_MAX_DLEN;
+ memcpy(f.data, dat, CAN_MAX_DLEN);
+ return write_all_ok(raw, &f, sizeof(f));
+}
+
+size_t j1939_dt_chunk_len(size_t size, size_t off)
+{
+ size_t n = size - off;
+
+ return n < J1939_DT_DATA_LEN ? n : J1939_DT_DATA_LEN;
+}
+
+bool send_dt_ok(int raw, uint8_t prio, pgn_t dt_pgn, uint8_t da,
+ uint8_t sa, uint8_t seq, const uint8_t *payload,
+ size_t off, size_t size)
+{
+ uint8_t dat[CAN_MAX_DLEN];
+
+ /*
+ * off >= size would make the size_t subtraction size - off
+ * underflow.
+ */
+ if (off >= size)
+ return false;
+
+ memset(dat, J1939_FILL_BYTE, sizeof(dat));
+ dat[J1939_DT_SEQ_OFF] = seq;
+ memcpy(&dat[J1939_DT_DATA_OFF], payload + off,
+ j1939_dt_chunk_len(size, off));
+ return send_raw8_ok(raw, prio, dt_pgn, da, sa, dat);
+}
+
+long timespec_diff_ms(const struct timespec *a,
+ const struct timespec *b)
+{
+ return ((b->tv_sec - a->tv_sec) * NSEC_PER_SEC +
+ (b->tv_nsec - a->tv_nsec)) / NSEC_PER_MSEC;
+}
+
+void deadline_after_ms(struct timespec *deadline, int timeout_ms)
+{
+ clock_gettime(CLOCK_MONOTONIC, deadline);
+ deadline->tv_sec += timeout_ms / MSEC_PER_SEC;
+ deadline->tv_nsec += (timeout_ms % MSEC_PER_SEC) * NSEC_PER_MSEC;
+ if (deadline->tv_nsec >= NSEC_PER_SEC) {
+ deadline->tv_sec++;
+ deadline->tv_nsec -= NSEC_PER_SEC;
+ }
+}
+
+bool recv_match_ok(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;
+
+ deadline_after_ms(&deadline, timeout_ms);
+
+ while (remaining > 0 && poll(&pfd, 1, remaining) > 0) {
+ if (recv(raw, frame, sizeof(*frame), 0) < 0)
+ return false;
+ if (j1939_is_data_frame(frame->can_id) &&
+ match(frame->can_id, ctx))
+ return true;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ remaining = timespec_diff_ms(&now, &deadline);
+ }
+ return false;
+}
+
+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 */
+ j1939_id_sa(id) == m->want_sa; /* source */
+}
+
+bool recv_for_peer_ok(int raw, uint8_t want_pf, uint8_t want_da,
+ uint8_t want_sa, struct can_frame *frame,
+ int timeout_ms)
+{
+ struct j1939_peer_match m = {
+ .want_pf = want_pf, .want_da = want_da, .want_sa = want_sa,
+ };
+
+ return recv_match_ok(raw, timeout_ms, frame, match_peer_dest, &m);
+}
+
+bool recv_cm_cmd_ok(int raw, uint8_t want_pf, uint8_t want_da,
+ uint8_t want_sa, struct can_frame *frame,
+ int timeout_ms, uint8_t want_cmd)
+{
+ if (!recv_for_peer_ok(raw, want_pf, want_da, want_sa, frame,
+ timeout_ms))
+ return false;
+ return frame->data[J1939_CM_CMD_OFF] == want_cmd;
+}
+
+void fill_pattern(uint8_t *buf, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ buf[i] = i ^ (i >> CHAR_BIT) ^ J1939_PATTERN_SEED;
+}
+
+bool can_proto_missing(int type, int proto)
+{
+ int probe = socket(PF_CAN, type, proto);
+
+ if (probe < 0)
+ return errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT;
+
+ close(probe);
+ return false;
+}
diff --git a/tools/testing/selftests/net/can/j1939_helpers.h b/tools/testing/selftests/net/can/j1939_helpers.h
new file mode 100644
index 0000000000000000000000000000000000000000..7dcd5bbe57f05f21c7707ea73fb5f283fba1c810
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_helpers.h
@@ -0,0 +1,337 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) */
+/*
+ * Shared helpers for the J1939 selftests.
+ *
+ * The individual tests are standalone kselftest binaries. This header
+ * declares the helpers they share, and j1939_helpers.c holds their code.
+ * The parts that use the kselftest harness are in j1939_tests.h.
+ *
+ * Helpers named *_ok() return true on success and false on failure, with
+ * errno set where a system call failed. Helpers that return a file
+ * descriptor or a length return -1 on failure.
+ *
+ * Timing modes: timer checks use relaxed bounds by default, so that slow
+ * machines such as QEMU in CI pass. Set J1939_STRICT_TIMING in the
+ * environment (any value) for tight bounds on a real machine.
+ */
+#ifndef __SELFTESTS_NET_CAN_J1939_HELPERS_H__
+#define __SELFTESTS_NET_CAN_J1939_HELPERS_H__
+
+#include <limits.h>
+#include <stdbool.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>
+
+#include "clock-helpers.h"
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+#endif
+#ifndef DIV_ROUND_UP
+#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
+#endif
+
+/*
+ * How long to wait for an expected frame or message. Not a protocol timer: a
+ * guess with room for a slow VM.
+ */
+#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. A guess.
+ */
+#define J1939_NEG_RECV_TIMEOUT_MS 300
+_Static_assert(J1939_NEG_RECV_TIMEOUT_MS < J1939_RECV_TIMEOUT_MS,
+ "J1939_NEG_RECV_TIMEOUT_MS must be the shorter timeout");
+
+/* Send priority of a new J1939 socket, see j1939_sk_init() in the kernel. */
+#define J1939_PRIO_DEFAULT 6
+
+/* Unused data bytes are sent as 0xff (SAE J1939-21:2001 Figures 14, 15). */
+#define J1939_FILL_BYTE 0xff
+
+/*
+ * PGN fields: PF is the second byte, PS the low byte. A PF of at least
+ * J1939_PDU2_PF_MIN selects PDU2, and PS is not an address
+ * (SAE J1939-21:2001 sections 5.1.2 and 5.2.5).
+ */
+#define J1939_PGN_PF_SHIFT CHAR_BIT /* below PF: the PS byte */
+#define J1939_PDU2_PF_MIN 0xf0
+#define J1939_PF(pgn) (((pgn) >> J1939_PGN_PF_SHIFT) & UINT8_MAX)
+#define J1939_IS_PDU1(pgn) (J1939_PF(pgn) < J1939_PDU2_PF_MIN)
+/* Data Page bit of a PGN (SAE J1939-21:2001 section 5.2.3) */
+#define J1939_PGN_DP 0x10000
+
+/*
+ * Fields of the 29-bit CAN id (SAE J1939-21:2001 section 5.2): the priority,
+ * then the PGN, whose PS byte holds the destination of a PDU1 frame, then
+ * the source address in the low byte. The j1939_id_*() decoders mask their
+ * field, so they take a can_id with the CAN_*_FLAG bits still set.
+ */
+#define J1939_ID_PRIO_BITS 3
+#define J1939_ID_PRIO_SHIFT (CAN_EFF_ID_BITS - J1939_ID_PRIO_BITS)
+#define J1939_ID_PRIO_MASK ((1U << J1939_ID_PRIO_BITS) - 1)
+#define J1939_ID_PGN_SHIFT CHAR_BIT /* below the PGN: the SA byte */
+_Static_assert((J1939_PGN_MAX + 1) << J1939_ID_PGN_SHIFT ==
+ 1U << J1939_ID_PRIO_SHIFT,
+ "the PGN field must end where the priority starts");
+
+int j1939_id_prio(canid_t id);
+int j1939_id_pgn(canid_t id);
+int j1939_id_pf(canid_t id);
+int j1939_id_ps(canid_t id);
+int j1939_id_sa(canid_t id);
+
+/* Bytes in a 24-bit field of a TP or ETP control frame. */
+#define J1939_LE24_LEN 3
+
+/*
+ * Byte offsets in TP and ETP frames (SAE J1939-21:2001 Figures 14 and 15,
+ * ISO 11783-6:2004 K.2.2.1 and K.2.3).
+ */
+#define J1939_CM_CMD_OFF 0 /* control byte */
+#define J1939_CM_PGN_OFF 5 /* message PGN, J1939_LE24_LEN bytes */
+#define J1939_DT_SEQ_OFF 0 /* sequence number */
+#define J1939_DT_DATA_OFF 1 /* first payload byte */
+_Static_assert(J1939_CM_PGN_OFF + J1939_LE24_LEN == CAN_MAX_DLEN,
+ "the message PGN is the last field of a control frame");
+
+/* Sizes and limits. */
+#define J1939_SF_MAX_SIZE CAN_MAX_DLEN /* largest single frame */
+/* payload bytes in one TP/ETP data (DT) frame */
+#define J1939_DT_DATA_LEN (CAN_MAX_DLEN - J1939_DT_DATA_OFF)
+/* highest TP sequence number: the field is one byte */
+#define J1939_DT_SEQ_MAX UINT8_MAX
+/* largest TP message; anything larger needs ETP */
+#define J1939_TP_MAX_SIZE (J1939_DT_SEQ_MAX * J1939_DT_DATA_LEN)
+_Static_assert(J1939_TP_MAX_SIZE == 1785,
+ "SAE J1939-21:2001 section 5.10.1.1 gives 1785 bytes");
+/* number of TP/ETP DT packets needed for len bytes, rounded up */
+#define J1939_NUM_PKTS(len) DIV_ROUND_UP(len, J1939_DT_DATA_LEN)
+
+/*
+ * J1939 uses 29-bit data frames only. Its CAN filter in net/can/j1939/main.c
+ * drops RTR frames, and error frames never reach it.
+ */
+bool j1939_is_data_frame(canid_t can_id);
+
+/*
+ * Size of a CAN_RAW receive buffer for every frame of the largest ETP test
+ * message. A guess with plenty of headroom, not computed. Set best-effort
+ * via SO_RCVBUFFORCE.
+ */
+#define J1939_RAW_BIG_RCVBUF (8 * 1024 * 1024)
+
+/* Flags for j1939_open_flags(). */
+#define J1939_OPEN_NONE 0
+#define J1939_OPEN_BCAST 0x1
+
+/* rcvbuf value for open_raw_rcvbuf(): keep the default receive buffer. */
+#define J1939_RAW_RCVBUF_DEFAULT 0
+
+/* Set by j1939_test_main() from the CANIF environment variable. */
+extern char CANIF[IFNAMSIZ];
+
+/*
+ * Return relaxed_ms by default, or strict_ms if J1939_STRICT_TIMING is set in
+ * the environment. See "Timing modes" at the top of this file.
+ */
+int j1939_timing_ms(int relaxed_ms, int strict_ms);
+
+/* Resolve the ifindex of the test interface named by CANIF. */
+int j1939_if_index(void);
+
+/*
+ * Open a J1939 socket bound to (name, sa, pgn); flags may set
+ * J1939_OPEN_BCAST.
+ */
+int j1939_open_flags(int ifindex, name_t name, uint8_t sa,
+ pgn_t pgn, int flags);
+
+/* Open a J1939 socket bound to (sa, pgn) with no NAME and no broadcast. */
+int j1939_open(int ifindex, uint8_t sa, pgn_t pgn);
+
+/* Connect a J1939 socket to the peer (name, da, pgn). */
+bool j1939_connect_name_ok(int s, int ifindex, name_t name,
+ uint8_t da, pgn_t pgn);
+bool j1939_connect_ok(int s, int ifindex, uint8_t da, pgn_t pgn);
+
+/* Fill a sockaddr_can describing a J1939 (name, da, pgn) destination. */
+void j1939_fill_sockaddr(struct sockaddr_can *addr, int ifindex,
+ name_t name, uint8_t da, pgn_t pgn);
+
+/* sendto() len bytes from buf to the J1939 destination dst. */
+ssize_t j1939_sendto_addr(int s, const void *buf, size_t len,
+ const struct sockaddr_can *dst);
+
+/* sendto() len bytes from buf to the J1939 (name, da, pgn) destination. */
+ssize_t j1939_sendto(int s, const void *buf, size_t len,
+ int ifindex, name_t name, uint8_t da,
+ pgn_t pgn);
+
+/* One j1939_sendto() that sent all len bytes; no retry. */
+bool j1939_sendto_ok(int s, const void *buf, size_t len,
+ int ifindex, name_t name, uint8_t da,
+ pgn_t pgn);
+
+/* One send(), write() or read() that moved all len bytes; no retry. */
+bool send_all_ok(int s, const void *buf, size_t len);
+bool write_all_ok(int fd, const void *buf, size_t len);
+bool read_all_ok(int fd, void *buf, size_t len);
+
+/*
+ * Open a CAN_RAW socket on ifindex. Unless rcvbuf is J1939_RAW_RCVBUF_DEFAULT,
+ * request that SO_RCVBUFFORCE size (best-effort: a failure is logged, not
+ * fatal).
+ */
+int open_raw_rcvbuf(int ifindex, int rcvbuf);
+int open_raw(int ifindex);
+
+/* poll() + recv(): returns the recv() result, or -1 on timeout/error. */
+ssize_t recv_timeout(int s, void *buf, size_t len,
+ int timeout_ms);
+
+/*
+ * recvfrom() one message on s within timeout_ms, check that it is the len
+ * bytes in want, and return the sender in *src unless src is NULL. The buffer
+ * has one spare byte, so a longer message fails the check.
+ */
+bool recvfrom_verify_ok(int s, const void *want, size_t len,
+ int timeout_ms, struct sockaddr_can *src);
+
+/* recvfrom_verify_ok() without the sender. */
+bool recv_verify_ok(int s, const void *want, size_t len,
+ int timeout_ms);
+
+/* Send len bytes on tx and verify they arrive byte-for-byte on rx. */
+bool send_recv_verify_ok(int tx, int rx, const void *buf,
+ size_t len, int timeout_ms);
+
+/* Receive one whole CAN frame from a raw socket within timeout_ms. */
+bool recv_frame_ok(int raw, struct can_frame *frame,
+ int timeout_ms);
+
+/*
+ * Build the 29-bit CAN id of a J1939 frame. prio must fit in
+ * J1939_ID_PRIO_BITS, and the PS byte of a PDU1 pgn must be 0.
+ */
+canid_t j1939_canid(uint8_t prio, pgn_t pgn, uint8_t da,
+ uint8_t sa);
+
+/*
+ * Start a TP/ETP control frame: every byte J1939_FILL_BYTE, then the control
+ * byte cmd.
+ */
+void cm_dat_init(uint8_t *dat, uint8_t cmd);
+
+/*
+ * Serialize a 64-bit NAME little-endian into a buffer of sizeof(name_t)
+ * bytes (host-agnostic).
+ */
+void put_name(uint8_t *dat, name_t name);
+
+/*
+ * Little-endian codecs for the multi-byte fields of TP/ETP connection
+ * management (CM) frames (host-agnostic).
+ */
+void put_le(uint8_t *p, uint32_t v, size_t n);
+uint32_t get_le(const uint8_t *p, size_t n);
+void put_le16(uint8_t *p, uint16_t v);
+void put_le24(uint8_t *p, uint32_t v);
+void put_le32(uint8_t *p, uint32_t v);
+uint16_t get_le16(const uint8_t *p);
+uint32_t get_le24(const uint8_t *p);
+uint32_t get_le32(const uint8_t *p);
+
+/* Embed the PGN of the transported message into a TP/ETP control frame. */
+void put_pgn(uint8_t *dat, pgn_t pgn);
+
+/* Send one J1939-encoded frame of CAN_MAX_DLEN bytes with priority prio. */
+bool send_raw8_ok(int raw, uint8_t prio, pgn_t pgn, uint8_t da,
+ uint8_t sa, const uint8_t dat[CAN_MAX_DLEN]);
+
+/*
+ * Payload bytes of the DT frame that starts at offset off of a message of
+ * size bytes; off must be below size.
+ */
+size_t j1939_dt_chunk_len(size_t size, size_t off);
+
+/*
+ * Send a TP/ETP data (DT) frame: sequence number seq and up to
+ * J1939_DT_DATA_LEN bytes from offset off of payload, a message of size
+ * bytes.
+ */
+bool send_dt_ok(int raw, uint8_t prio, pgn_t dt_pgn, uint8_t da,
+ uint8_t sa, uint8_t seq, const uint8_t *payload,
+ size_t off, size_t size);
+
+/* Milliseconds from a to b (negative if b is earlier), CLOCK_MONOTONIC. */
+long timespec_diff_ms(const struct timespec *a,
+ const struct timespec *b);
+
+_Static_assert(__builtin_types_compatible_p(__typeof__(NSEC_PER_SEC),
+ long long) &&
+ __builtin_types_compatible_p(__typeof__(NSEC_PER_MSEC),
+ long long),
+ "timespec_diff_ms() needs signed long long arithmetic");
+
+/* Set *deadline to timeout_ms from now on CLOCK_MONOTONIC. */
+void deadline_after_ms(struct timespec *deadline, int timeout_ms);
+
+/*
+ * Poll and recv on raw until match() accepts a frame, or time out. match()
+ * gets the raw can_id; the j1939_id_*() decoders mask the flag bits. Other
+ * traffic must not extend the wait, so poll against a fixed deadline.
+ */
+bool recv_match_ok(int raw, int timeout_ms,
+ struct can_frame *frame,
+ bool (*match)(canid_t id, const void *ctx),
+ const void *ctx);
+
+struct j1939_peer_match { uint8_t want_pf, want_da, want_sa; };
+
+bool match_peer_dest(canid_t id, const void *ctx);
+
+/*
+ * Receive the next frame whose CAN-id PF == want_pf, sent from want_sa to
+ * want_da (PS).
+ */
+bool recv_for_peer_ok(int raw, uint8_t want_pf, uint8_t want_da,
+ uint8_t want_sa, struct can_frame *frame,
+ int timeout_ms);
+
+/* recv_for_peer_ok() and check that the control byte is want_cmd. */
+bool recv_cm_cmd_ok(int raw, uint8_t want_pf, uint8_t want_da,
+ uint8_t want_sa, struct can_frame *frame,
+ int timeout_ms, uint8_t want_cmd);
+
+/* Arbitrary; keeps fill_pattern() from being the plain byte offset. */
+#define J1939_PATTERN_SEED 0x3c
+
+/*
+ * Fill buf with a deterministic, position-dependent byte pattern. The
+ * i >> CHAR_BIT term makes each 256-byte block of the first 64 KiB differ,
+ * so a misplaced packet shows up.
+ */
+void fill_pattern(uint8_t *buf, size_t len);
+
+/* Is the CAN protocol proto missing from this kernel? */
+bool can_proto_missing(int type, int proto);
+
+#endif /* __SELFTESTS_NET_CAN_J1939_HELPERS_H__ */
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 0000000000000000000000000000000000000000..07e960e155fabcef9c0f159b0fa808d29f0d3d3b
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_lib.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Shared bring-up for the J1939 selftest wrappers. A wrapper sources this
+# file and runs the test binary of the same name, without .sh:
+#
+# source "$(dirname "$0")/j1939_lib.sh"
+#
+# It creates (and deletes) the vcan named by CANIF (vcan0 by default), runs the
+# test binary through the kselftest net lib and reports the result. The tests
+# send frames as other nodes and forge Address Claims, so they must not run on
+# a real CAN bus: a CANIF whose name does not start with "vcan" is skipped.
+#
+# For the J1939_STRICT_TIMING timing mode, see j1939_helpers.h.
+
+# shellcheck disable=SC2034 # used by tests_run in lib.sh
+ALL_TESTS="j1939_run"
+
+net_dir=$(dirname "$0")/..
+# shellcheck source=../lib.sh
+source "$net_dir/lib.sh"
+
+export CANIF=${CANIF:-"vcan0"}
+J1939_TEST_PROG=$(basename "$0" .sh)
+
+# Undo only what this script 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
+ echo "SKIP: CANIF name '$CANIF' does not start with vcan"
+ exit "$ksft_skip"
+ fi
+
+ if ip link show "$CANIF" >/dev/null 2>&1; then
+ echo "SKIP: $CANIF already exists;" \
+ "delete it and run the test again"
+ exit "$ksft_skip"
+ fi
+
+ if ! ip link add name "$CANIF" type vcan; then
+ echo "SKIP: cannot create $CANIF" \
+ "(no vcan support or no permission)"
+ exit "$ksft_skip"
+ fi
+ created=1
+
+ if ! ip link set dev "$CANIF" up; then
+ echo "SKIP: cannot bring up $CANIF"
+ exit "$ksft_skip"
+ fi
+ 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
+
+ "$(dirname "$0")/$J1939_TEST_PROG"
+ rc=$?
+
+ # The test binary exits ksft_skip when it cannot run (see
+ # j1939_test_main()); 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" ""
+ else
+ check_err "$rc"
+ fi
+ log_test "$J1939_TEST_PROG"
+}
+
+trap cleanup EXIT
+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 0000000000000000000000000000000000000000..3536adfd9892f938f99708208dc97ac24db5c113
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) */
+/*
+ * The parts of the J1939 selftest helpers that use the kselftest harness:
+ * the fixture setup macros and j1939_test_main(). The other helpers are in
+ * j1939_helpers.h, which this header includes.
+ *
+ * Each test binary is run by its .sh wrapper (see j1939_lib.sh), which sets up
+ * the vcan interface and passes its name in the CANIF environment variable.
+ *
+ * The including .c file must include "kselftest_harness.h" before this
+ * header.
+ */
+#ifndef __SELFTESTS_NET_CAN_J1939_TESTS_H__
+#define __SELFTESTS_NET_CAN_J1939_TESTS_H__
+
+#include "j1939_helpers.h"
+
+/*
+ * 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)
+
+/*
+ * FIXTURE_SETUP helper: open a CAN_RAW socket on self->ifindex into self->raw
+ * with the given SO_RCVBUFFORCE size (or J1939_RAW_RCVBUF_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)
+
+/*
+ * Body of each test's main(): require CANIF in the environment and a kernel
+ * that supports CAN_J1939 and CAN_RAW, then run the harness. Skip when any
+ * of them is missing.
+ */
+static inline int j1939_test_main(int argc, char **argv)
+{
+ char *ifname = getenv("CANIF");
+
+ if (!ifname) {
+ fprintf(stderr, "# CANIF must name the test interface\n");
+ return KSFT_SKIP;
+ }
+ strncpy(CANIF, ifname, sizeof(CANIF) - 1);
+
+ if (can_proto_missing(SOCK_DGRAM, CAN_J1939)) {
+ fprintf(stderr, "# kernel lacks CAN_J1939\n");
+ return KSFT_SKIP;
+ }
+ if (can_proto_missing(SOCK_RAW, CAN_RAW)) {
+ fprintf(stderr, "# kernel lacks CAN_RAW\n");
+ return KSFT_SKIP;
+ }
+
+ return test_harness_run(argc, argv);
+}
+
+#endif /* __SELFTESTS_NET_CAN_J1939_TESTS_H__ */
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH can-next v3 3/9] selftests: can: add J1939 base functional tests
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 1/9] selftests: can: install net/lib.sh with the CAN tests Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 2/9] selftests: can: add J1939 test helper header and library Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-13 6:01 ` netdev-bot+sashiko
2026-11-11 15:25 ` [PATCH can-next v3 4/9] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
` (5 subsequent siblings)
8 siblings, 1 reply; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add a functional selftest for the SAE J1939 socket interface. It sends
between CAN_J1939 sockets over a vcan interface and checks payload
integrity for single frame, TP and ETP sizes, the boundaries between
them, and single-frame and BAM broadcast. A single-frame broadcast
reaches a receiver only with SO_BROADCAST.
Enable CONFIG_CAN_J1939 and CONFIG_CAN_RAW in the test config. The J1939
tests skip if either is missing.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Changes in v3:
- use the 200 ms maximum packet gap of the spec, per packet, in the
receive timeout of a BAM message (sashiko)
- add a test that a single-frame broadcast reaches a receiver only
with SO_BROADCAST
- assert that PGN_PDU1 has PS 0, that both PGNs are valid, and that
SA_TX and SA_RX are two different unicast addresses
- rebuild the tests when clock-helpers.h changes
- link j1939_helpers.c into the J1939 test binaries
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name the message sizes and add static asserts for them
- return bool from helpers named *_ok()
- sort the Makefile lists, config and .gitignore; derive TEST_GEN_FILES
from TEST_PROGS
- add CONFIG_CAN_RAW to config here, not in the opts patch: every test
needs it
- the wrapper no longer sets J1939_TEST_PROG
- say why a round trip failed instead of printing a stale errno
(sashiko)
- say that size_boundaries checks only the payload, not the transport
(sashiko)
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 13 +-
tools/testing/selftests/net/can/config | 2 +
tools/testing/selftests/net/can/test_j1939_base.c | 236 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_base.sh | 4 +
5 files changed, 254 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 764a53fc837f7dca4f2b0c03d1dc0c1b30c57671..ac9a3abe416f924998b4b337df3ea6a3891ca3de 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_j1939_base
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 953f4538740984d7342f6e264ac13fadc0790ed2..64a436a59aa69a381cb98bb6a9b15a3feeac9eff 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -4,12 +4,21 @@ 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_helpers.h j1939_tests.h ../../clock-helpers.h
+
+TEST_PROGS := \
+ test_j1939_base.sh \
+ test_raw_filter.sh \
+# end of TEST_PROGS
TEST_FILES := j1939_lib.sh
TEST_INCLUDES := ../lib.sh $(wildcard ../lib/sh/*.sh)
-TEST_GEN_FILES := test_raw_filter
+# each .sh in TEST_PROGS runs the binary of the same name
+TEST_GEN_FILES := $(TEST_PROGS:.sh=)
include ../../lib.mk
+
+# the J1939 tests link the shared helper code
+$(filter $(OUTPUT)/test_j1939_%,$(TEST_GEN_FILES)): j1939_helpers.c
diff --git a/tools/testing/selftests/net/can/config b/tools/testing/selftests/net/can/config
index 188f7979667097071401f84cc8e81d17f838af85..c132e17011e3c5dd04310b45b665b0c2782e2c85 100644
--- a/tools/testing/selftests/net/can/config
+++ b/tools/testing/selftests/net/can/config
@@ -1,3 +1,5 @@
CONFIG_CAN=m
CONFIG_CAN_DEV=m
+CONFIG_CAN_J1939=m
+CONFIG_CAN_RAW=m
CONFIG_CAN_VCAN=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 0000000000000000000000000000000000000000..7bf492c767bf618af0be5ac89db7d7507c92e15c
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_base.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 base functional selftests.
+ *
+ * Send between SOCK_DGRAM/CAN_J1939 sockets 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).
+ *
+ * Run by test_j1939_base.sh, see j1939_tests.h.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+/* arbitrary addresses and PGNs, within the asserts below */
+#define SA_TX 0x80
+#define SA_RX 0x90
+#define PGN_PDU1 0x0ab00 /* destination specific */
+#define PGN_PDU2 0x0feee /* broadcast */
+_Static_assert(J1939_IS_PDU1(PGN_PDU1) && !J1939_IS_PDU1(PGN_PDU2),
+ "PGN_PDU1 must be PDU1 and PGN_PDU2 must be PDU2");
+_Static_assert((PGN_PDU1 & UINT8_MAX) == 0,
+ "the kernel rejects a PDU1 PGN whose PS byte is not 0");
+_Static_assert(PGN_PDU1 <= J1939_PGN_MAX && PGN_PDU2 <= J1939_PGN_MAX,
+ "PGN_PDU1 and PGN_PDU2 must be valid PGNs");
+_Static_assert(SA_TX != SA_RX && SA_TX <= J1939_MAX_UNICAST_ADDR &&
+ SA_RX <= J1939_MAX_UNICAST_ADDR,
+ "SA_TX and SA_RX must be two different unicast addresses");
+
+/* An arbitrary TP size inside the TP range, and one that needs ETP. */
+#define TP_SIZE 128
+#define ETP_SIZE 2000
+_Static_assert(TP_SIZE > J1939_SF_MAX_SIZE, "TP_SIZE must need TP");
+_Static_assert(TP_SIZE < J1939_TP_MAX_SIZE, "TP_SIZE must fit TP");
+_Static_assert(ETP_SIZE > J1939_TP_MAX_SIZE, "ETP_SIZE must need ETP");
+
+/*
+ * Longest gap the spec allows before a BAM data packet (SAE J1939-21:2001
+ * section 5.12.3). The kernel waits 50 ms, see j1939_session_tx_rts() and
+ * j1939_session_tx_dat().
+ */
+#define BAM_PKT_GAP_MAX_MS 200
+
+FIXTURE(j1939_base)
+{
+ int ifindex;
+ int tx;
+ int rx;
+ int rx_no_bcast; /* a receiver without SO_BROADCAST */
+ bool bcast;
+};
+
+FIXTURE_SETUP(j1939_base)
+{
+ self->tx = -1;
+ self->rx = -1;
+ self->rx_no_bcast = -1;
+ J1939_SETUP_IFINDEX(self);
+}
+
+FIXTURE_TEARDOWN(j1939_base)
+{
+ if (self->tx >= 0)
+ close(self->tx);
+ if (self->rx >= 0)
+ close(self->rx);
+ if (self->rx_no_bcast >= 0)
+ close(self->rx_no_bcast);
+}
+
+/*
+ * 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. bcast opens both sockets with J1939_OPEN_BCAST, for the PDU2 and BAM
+ * tests.
+ */
+static bool pair_up_ok(FIXTURE_DATA(j1939_base) *self, uint8_t rx_addr,
+ uint8_t tx_dst, pgn_t pgn, bool bcast)
+{
+ int flags = bcast ? J1939_OPEN_BCAST : J1939_OPEN_NONE;
+
+ self->bcast = bcast;
+ 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 false;
+ }
+
+ 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 false;
+ }
+
+ if (!j1939_connect_ok(self->tx, self->ifindex, tx_dst, pgn)) {
+ fprintf(stderr, "# connect tx failed: %d\n", errno);
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * Send a deterministic payload of the given size on tx and verify it is
+ * received byte for byte on rx.
+ */
+static bool roundtrip_ok(FIXTURE_DATA(j1939_base) *self, size_t size)
+{
+ int timeout_ms = J1939_RECV_TIMEOUT_MS;
+ uint8_t *tx_buf;
+ bool ok;
+
+ /* a BAM message arrives only after all its packet gaps */
+ if (self->bcast && size > J1939_SF_MAX_SIZE)
+ timeout_ms += J1939_NUM_PKTS(size) * BAM_PKT_GAP_MAX_MS;
+
+ tx_buf = malloc(size);
+ if (!tx_buf) {
+ fprintf(stderr, "# alloc failed\n");
+ return false;
+ }
+ fill_pattern(tx_buf, size);
+
+ errno = 0;
+ ok = send_recv_verify_ok(self->tx, self->rx, tx_buf, size, timeout_ms);
+ if (!ok)
+ fprintf(stderr, "# round-trip failed at size %zu: %s\n", size,
+ errno ? strerror(errno) : "no or wrong data");
+
+ free(tx_buf);
+ return ok;
+}
+
+/* Largest single frame, unicast PDU1. */
+TEST_F(j1939_base, single_frame_pdu1)
+{
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, SA_RX, PGN_PDU1, false));
+ EXPECT_TRUE(roundtrip_ok(self, J1939_SF_MAX_SIZE));
+}
+
+/* Transport Protocol, unicast. */
+TEST_F(j1939_base, tp_roundtrip)
+{
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, SA_RX, PGN_PDU1, false));
+ EXPECT_TRUE(roundtrip_ok(self, TP_SIZE));
+}
+
+/* Extended Transport Protocol, unicast. */
+TEST_F(j1939_base, etp_roundtrip)
+{
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, SA_RX, PGN_PDU1, false));
+ EXPECT_TRUE(roundtrip_ok(self, ETP_SIZE));
+}
+
+/*
+ * Sizes on both sides of the single frame / TP and TP / ETP limits. Only the
+ * payload is checked here, not which transport carried it.
+ */
+TEST_F(j1939_base, size_boundaries)
+{
+ static const size_t sizes[] = {
+ J1939_SF_MAX_SIZE, J1939_SF_MAX_SIZE + 1,
+ J1939_TP_MAX_SIZE, J1939_TP_MAX_SIZE + 1,
+ };
+ size_t k;
+
+ for (k = 0; k < ARRAY_SIZE(sizes); k++) {
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, SA_RX, PGN_PDU1, false));
+ EXPECT_TRUE(roundtrip_ok(self, sizes[k]))
+ TH_LOG("failed at boundary size %zu", sizes[k]);
+ }
+}
+
+/* Single-frame broadcast, PDU2. */
+TEST_F(j1939_base, single_frame_broadcast_pdu2)
+{
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, J1939_NO_ADDR, PGN_PDU2, true));
+ EXPECT_TRUE(roundtrip_ok(self, J1939_SF_MAX_SIZE));
+}
+
+/* BAM (broadcast transport). */
+TEST_F(j1939_base, bam_broadcast)
+{
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, J1939_NO_ADDR, PGN_PDU2, true));
+ EXPECT_TRUE(roundtrip_ok(self, TP_SIZE));
+}
+
+/* A single-frame broadcast reaches only a receiver with SO_BROADCAST. */
+TEST_F(j1939_base, bcast_needs_so_broadcast)
+{
+ uint8_t tx_buf[J1939_SF_MAX_SIZE];
+ uint8_t rx_buf[J1939_SF_MAX_SIZE];
+ int enable = 1;
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+ ASSERT_TRUE(pair_up_ok(self, SA_RX, J1939_NO_ADDR, PGN_PDU2, true));
+ self->rx_no_bcast = j1939_open(self->ifindex, SA_RX, PGN_PDU2);
+ ASSERT_GE(self->rx_no_bcast, 0)
+ TH_LOG("open rx_no_bcast failed: %d", errno);
+
+ /* the receiver with SO_BROADCAST shows that the frame went out */
+ ASSERT_TRUE(send_recv_verify_ok(self->tx, self->rx, tx_buf,
+ sizeof(tx_buf), J1939_RECV_TIMEOUT_MS));
+ EXPECT_EQ(recv_timeout(self->rx_no_bcast, rx_buf, sizeof(rx_buf),
+ J1939_NEG_RECV_TIMEOUT_MS), -1)
+ TH_LOG("broadcast received without SO_BROADCAST");
+
+ /*
+ * Set SO_BROADCAST and send again. The frame must arrive now.
+ * That shows the first frame was dropped for the missing flag,
+ * not for another reason.
+ */
+ ASSERT_EQ(setsockopt(self->rx_no_bcast, SOL_SOCKET, SO_BROADCAST,
+ &enable, sizeof(enable)), 0);
+ EXPECT_TRUE(send_recv_verify_ok(self->tx, self->rx_no_bcast, tx_buf,
+ sizeof(tx_buf), J1939_RECV_TIMEOUT_MS))
+ TH_LOG("broadcast not received even with SO_BROADCAST");
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
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 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_base.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH can-next v3 3/9] selftests: can: add J1939 base functional tests
2026-11-11 15:25 ` [PATCH can-next v3 3/9] selftests: can: add J1939 base functional tests Oleksij Rempel
@ 2026-11-13 6:01 ` netdev-bot+sashiko
0 siblings, 0 replies; 14+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 6:01 UTC (permalink / raw)
To: o.rempel
Cc: robin, socketcan, mkl, shuah, kernel, linux-can, linux-kernel,
linux-kselftest, fmaurer, mailhol, alexander.hoelzl,
quchaosheng000406, penguin-kernel, kuba
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] test_j1939_base never opens a CAN_RAW socket, yet it skips the
whole suite when CAN_RAW is missing.
--
selftests: can: add J1939 base functional tests
This adds a J1939 functional selftest. It sends between CAN_J1939 sockets
over vcan and checks payload integrity for single frame, TP and ETP sizes,
the boundaries between them, and single-frame and BAM broadcast. It also
enables CONFIG_CAN_J1939 and CONFIG_CAN_RAW in the selftest config.
> 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 0000000000000..7bf492c767bf6
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/test_j1939_base.c
> @@ -0,0 +1,236 @@
[ ... ]
> +int main(int argc, char **argv)
> +{
> + return j1939_test_main(argc, argv);
> +}
[Severity: Low]
Is this binary skipped on a kernel that has CONFIG_CAN_J1939 but not
CONFIG_CAN_RAW?
In j1939_tests.h, j1939_test_main() makes every J1939 binary depend on
CAN_RAW:
if (can_proto_missing(SOCK_RAW, CAN_RAW)) {
fprintf(stderr, "# kernel lacks CAN_RAW\n");
return KSFT_SKIP;
}
test_j1939_base only uses SOCK_DGRAM/CAN_J1939 sockets:
- pair_up_ok() calls j1939_open_flags() and j1939_connect_ok().
- bcast_needs_so_broadcast calls j1939_open().
- The data path is send_recv_verify_ok() and recv_timeout() on those
sockets.
Nothing in this file calls open_raw(), open_raw_rcvbuf() or
J1939_SETUP_RAW.
On such a kernel, the single frame, TP, ETP, size boundary, broadcast, BAM
and SO_BROADCAST tests would all report SKIP, even though they could run.
The v2 changelog says CONFIG_CAN_RAW is added here because "every test
needs it". That seems true only because of this check, not because of
anything the base test does.
Could the CAN_RAW check move into the binaries that actually open raw
sockets, or become a per-binary or per-test requirement?
The check itself came from the earlier patch in this series ("selftests:
can: add J1939 test helper header and library"). This is the first binary
that gets skipped by it for no reason.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261111-j1939-selftests-v3-0-5a80277ab363%40pengutronix.de
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH can-next v3 4/9] selftests: can: add J1939 socket option and metadata tests
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (2 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 3/9] selftests: can: add J1939 base functional tests Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-13 6:02 ` netdev-bot+sashiko
2026-11-11 15:25 ` [PATCH can-next v3 5/9] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
` (4 subsequent siblings)
8 siblings, 1 reply; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add a selftest for the J1939 socket options and send metadata. The send
priority is checked on the wire via a CAN_RAW peer, and the option values
are read back with getsockopt. The test also covers the SO_J1939_FILTER
PGN whitelist, unconnected sendto(), a bind() with a wrong address family
and the SO_J1939_ERRQUEUE TX-completion ACK.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q (sashiko): setsockopt() for SO_J1939_PROMISC and SO_J1939_ERRQUEUE
returns 1 on success, not 0. Why does the test accept that?
A: It is a stack bug. j1939_sk_setsockopt_flag() returns the value that
the caller passed, not 0. A negative value sets the flag too, and the
call then fails. For -1 the errno is 1 (EPERM). This series changes no
kernel code. The tests set only 0 and 1. They accept any return value
>= 0, so they pass with or without a fix.
Changes in v3:
- add j1939_set_flag_ok(), with a comment that the return value is a
stack bug (sashiko)
- assert that PRIO_SET is a valid unprivileged priority
- assert that PGN_A and PGN_B are valid PDU1 PGNs with PS 0, and that
SA_TX and SA_RX are two different unicast addresses
- fail the error-queue read on a truncated cmsg, and check ee_errno
- check SO_J1939_SEND_PRIO strictly in the round-trip test; errno is 0
when only the value read back is wrong
- check that the filter drops PGN_B with an ordering barrier, not a
timeout
- fix comments that did not match the code
- put the new helpers in j1939_helpers.c and j1939_helpers.h
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, add static asserts, avoid casts, return bool from
helpers named *_ok() and fill test payloads with fill_pattern()
- raw_prio(): take only a J1939 data frame; no other node sends on the
vcan that j1939_lib.sh now creates (sashiko)
- check the errno of bind() with a wrong address family
- request only the TX ACK timestamp, the one the test checks
- also clear the PROMISC and ERRQUEUE flags and read them back
- move CONFIG_CAN_RAW to the base patch
- keep the raw socket in the fixture; without CAN_RAW the test is now
skipped (sashiko)
- keep the Makefile and .gitignore lists sorted; the wrapper no longer
sets J1939_TEST_PROG
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 1 +
tools/testing/selftests/net/can/j1939_helpers.c | 56 ++++
tools/testing/selftests/net/can/j1939_helpers.h | 29 ++
tools/testing/selftests/net/can/test_j1939_opts.c | 309 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_opts.sh | 4 +
6 files changed, 400 insertions(+)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index ac9a3abe416f924998b4b337df3ea6a3891ca3de..81c6e5e4396ac789e0ac1ebad4f11d0f7ef9cbaa 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_j1939_base
+test_j1939_opts
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 64a436a59aa69a381cb98bb6a9b15a3feeac9eff..6b51f8e671461555ee4818cf9a3e8ea7d5fe7c35 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -8,6 +8,7 @@ LOCAL_HDRS := j1939_helpers.h j1939_tests.h ../../clock-helpers.h
TEST_PROGS := \
test_j1939_base.sh \
+ test_j1939_opts.sh \
test_raw_filter.sh \
# end of TEST_PROGS
diff --git a/tools/testing/selftests/net/can/j1939_helpers.c b/tools/testing/selftests/net/can/j1939_helpers.c
index 2457513c559c50acbba409c6b4c429a79b7798e0..0435c973e837c09bc73eb774939a957f31fdd554 100644
--- a/tools/testing/selftests/net/can/j1939_helpers.c
+++ b/tools/testing/selftests/net/can/j1939_helpers.c
@@ -441,6 +441,62 @@ bool recv_cm_cmd_ok(int raw, uint8_t want_pf, uint8_t want_da,
return frame->data[J1939_CM_CMD_OFF] == want_cmd;
}
+bool j1939_set_flag_ok(int s, int optname, int val)
+{
+ return setsockopt(s, SOL_CAN_J1939, optname, &val, sizeof(val)) >= 0;
+}
+
+bool errqueue_enable_ack_ok(int s)
+{
+ int tstamp_flags = SOF_TIMESTAMPING_TX_ACK;
+
+ if (!j1939_set_flag_ok(s, SO_J1939_ERRQUEUE, 1))
+ return false;
+
+ return !setsockopt(s, SOL_SOCKET, SO_TIMESTAMPING, &tstamp_flags,
+ sizeof(tstamp_flags));
+}
+
+bool msg_has_tstamp_ack(struct msghdr *msg)
+{
+ struct cmsghdr *cmsg;
+
+ 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_errno == ENOMSG &&
+ serr->ee_origin == SO_EE_ORIGIN_TIMESTAMPING &&
+ serr->ee_info == SCM_TSTAMP_ACK)
+ return true;
+ }
+ return false;
+}
+
+bool errqueue_read_ack_ok(int s)
+{
+ struct pollfd pfd = { .fd = s, .events = POLLERR };
+ union {
+ char buf[CMSG_SPACE(sizeof(struct sock_extended_err))];
+ struct cmsghdr align; /* portable cmsg alignment */
+ } ctrl;
+ struct msghdr msg = {
+ .msg_control = ctrl.buf,
+ .msg_controllen = sizeof(ctrl.buf),
+ };
+
+ if (poll(&pfd, 1, J1939_RECV_TIMEOUT_MS) <= 0 ||
+ recvmsg(s, &msg, MSG_ERRQUEUE) < 0 ||
+ msg.msg_flags & MSG_CTRUNC)
+ return false;
+
+ return msg_has_tstamp_ack(&msg);
+}
+
void fill_pattern(uint8_t *buf, size_t len)
{
size_t i;
diff --git a/tools/testing/selftests/net/can/j1939_helpers.h b/tools/testing/selftests/net/can/j1939_helpers.h
index 7dcd5bbe57f05f21c7707ea73fb5f283fba1c810..0acb7ce1fcc6459989604b19d8dcc5861e5f7086 100644
--- a/tools/testing/selftests/net/can/j1939_helpers.h
+++ b/tools/testing/selftests/net/can/j1939_helpers.h
@@ -35,6 +35,8 @@
#include <linux/can.h>
#include <linux/can/raw.h>
#include <linux/can/j1939.h>
+#include <linux/errqueue.h>
+#include <linux/net_tstamp.h>
#include "clock-helpers.h"
@@ -321,6 +323,33 @@ bool recv_cm_cmd_ok(int raw, uint8_t want_pf, uint8_t want_da,
uint8_t want_sa, struct can_frame *frame,
int timeout_ms, uint8_t want_cmd);
+/*
+ * Set the J1939 flag option optname (SO_J1939_PROMISC or SO_J1939_ERRQUEUE)
+ * to val. A stack bug makes setsockopt() return val instead of 0 on success
+ * (j1939_sk_setsockopt_flag()), so any value >= 0 counts as success.
+ */
+bool j1939_set_flag_ok(int s, int optname, int val);
+
+/*
+ * Enable the error queue of s with SOF_TIMESTAMPING_TX_ACK only: a finished
+ * send then queues one TX-completion ACK, or an abort entry if the session
+ * fails (__j1939_sk_errqueue() in net/can/j1939/socket.c). The kernel reads
+ * the timestamping flags when the session ends, so the caller may call this
+ * function after the send, as long as the session has not ended.
+ */
+bool errqueue_enable_ack_ok(int s);
+
+/* Does the error-queue message msg carry the TX-completion ACK? */
+bool msg_has_tstamp_ack(struct msghdr *msg);
+
+/*
+ * Read one message from the error queue of s, set up by
+ * errqueue_enable_ack_ok(), and check that it is the TX-completion ACK. With
+ * those flags the kernel returns each entry with one cmsg, SCM_J1939_ERRQUEUE.
+ * No iovec is passed, so no message data is read.
+ */
+bool errqueue_read_ack_ok(int s);
+
/* Arbitrary; keeps fill_pattern() from being the plain byte offset. */
#define J1939_PATTERN_SEED 0x3c
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 0000000000000000000000000000000000000000..a729e39c0be4fc8e77717c71000f205dd14f453b
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_opts.c
@@ -0,0 +1,309 @@
+// 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 kernel's
+ * default), unconnected sendto(2), a wrong-family bind rejection,
+ * SO_J1939_ERRQUEUE TX-completion notifications, and set/getsockopt
+ * round-trips of SO_J1939_SEND_PRIO, SO_J1939_PROMISC and SO_J1939_ERRQUEUE.
+ *
+ * Run by test_j1939_opts.sh, see j1939_tests.h.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+/* arbitrary addresses */
+#define SA_TX 0x80
+#define SA_RX 0x90
+
+/* two different destination-specific PGNs, otherwise arbitrary */
+#define PGN_A 0x0ab00
+#define PGN_B 0x0cd00
+_Static_assert(J1939_IS_PDU1(PGN_A) && J1939_IS_PDU1(PGN_B) && PGN_A != PGN_B,
+ "PGN_A and PGN_B must be two different PDU1 PGNs");
+_Static_assert((PGN_A & UINT8_MAX) == 0 && (PGN_B & UINT8_MAX) == 0,
+ "the kernel rejects a PDU1 PGN whose PS byte is not 0");
+_Static_assert(PGN_A <= J1939_PGN_MAX && PGN_B <= J1939_PGN_MAX,
+ "PGN_A and PGN_B must be valid PGNs");
+_Static_assert(SA_TX != SA_RX && SA_TX <= J1939_MAX_UNICAST_ADDR &&
+ SA_RX <= J1939_MAX_UNICAST_ADDR,
+ "SA_TX and SA_RX must be two different unicast addresses");
+
+/*
+ * SO_J1939_SEND_PRIO values below this one need CAP_NET_ADMIN
+ * (j1939_sk_setsockopt()).
+ */
+#define PRIO_MIN_UNPRIV 2
+
+/* a valid priority that needs no privilege and differs from the default */
+#define PRIO_SET 3
+_Static_assert(PRIO_SET >= PRIO_MIN_UNPRIV && PRIO_SET <= J1939_ID_PRIO_MASK &&
+ PRIO_SET != J1939_PRIO_DEFAULT,
+ "PRIO_SET must be a valid, unprivileged, non-default priority");
+
+/*
+ * An arbitrary size that needs TP, so that the ACK ends a multi-frame
+ * session.
+ */
+#define ERRQUEUE_MSG_SIZE 128
+_Static_assert(ERRQUEUE_MSG_SIZE > J1939_SF_MAX_SIZE,
+ "ERRQUEUE_MSG_SIZE must need TP");
+_Static_assert(ERRQUEUE_MSG_SIZE <= J1939_TP_MAX_SIZE,
+ "ERRQUEUE_MSG_SIZE must fit TP");
+
+/*
+ * Read the next CAN frame from a raw socket and return its J1939 priority, or
+ * -1 on timeout or if it is not a J1939 data frame.
+ */
+static int raw_prio(int raw, int timeout_ms)
+{
+ struct can_frame frame;
+
+ if (!recv_frame_ok(raw, &frame, timeout_ms) ||
+ !j1939_is_data_frame(frame.can_id))
+ return -1;
+ return j1939_id_prio(frame.can_id);
+}
+
+/*
+ * Set an int J1939 sockopt, read it back, and return the value read, or -1 if
+ * a call fails. errno is 0 if both calls succeed. SO_J1939_SEND_PRIO must
+ * return 0; the flag options go through j1939_set_flag_ok().
+ */
+static int sockopt_roundtrip(int s, int optname, int val)
+{
+ int got = -1;
+ socklen_t len = sizeof(got);
+ bool set_ok;
+
+ errno = 0;
+ if (optname == SO_J1939_SEND_PRIO)
+ set_ok = !setsockopt(s, SOL_CAN_J1939, optname, &val,
+ sizeof(val));
+ else
+ set_ok = j1939_set_flag_ok(s, optname, val);
+ if (!set_ok)
+ 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, J1939_RAW_RCVBUF_DEFAULT);
+}
+
+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,
+ };
+ struct sockaddr_can src = {};
+ uint8_t tx_buf[CAN_MAX_DLEN];
+ uint8_t rx_buf[CAN_MAX_DLEN];
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+
+ 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 */
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME, SA_RX, PGN_A))
+ TH_LOG("sendto PGN_A failed: %d", errno);
+ EXPECT_TRUE(recv_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS))
+ TH_LOG("PGN_A was not received despite matching filter");
+
+ /*
+ * PGN_B must be dropped by the filter. Send PGN_A after it and read it
+ * back: a PGN_B that got through is read before PGN_A or is still
+ * queued after it.
+ */
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME, SA_RX, PGN_B))
+ TH_LOG("sendto PGN_B failed: %d", errno);
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME, SA_RX, PGN_A))
+ TH_LOG("sendto PGN_A failed: %d", errno);
+ ASSERT_TRUE(recvfrom_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS, &src))
+ TH_LOG("PGN_A after PGN_B was not received");
+ EXPECT_EQ(src.can_addr.j1939.pgn, filt.pgn)
+ TH_LOG("PGN_B was received before PGN_A");
+ EXPECT_EQ(recv(self->rx, rx_buf, sizeof(rx_buf), MSG_DONTWAIT), -1)
+ TH_LOG("PGN_B was received but should have been filtered out");
+ EXPECT_EQ(errno, EAGAIN);
+}
+
+/* Without SO_J1939_SEND_PRIO the kernel's default priority is used. */
+TEST_F(j1939_opts, send_prio_default)
+{
+ uint8_t buf[CAN_MAX_DLEN] = {};
+
+ self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+ ASSERT_GE(self->tx, 0);
+
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, buf, sizeof(buf), self->ifindex,
+ J1939_NO_NAME, SA_RX, PGN_A));
+
+ 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[CAN_MAX_DLEN] = {};
+
+ 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_TRUE(j1939_sendto_ok(self->tx, buf, sizeof(buf), self->ifindex,
+ J1939_NO_NAME, SA_RX, PGN_A));
+
+ 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[CAN_MAX_DLEN];
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+
+ 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_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME, SA_RX,
+ PGN_A));
+
+ EXPECT_TRUE(recv_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS));
+}
+
+/* A 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");
+ EXPECT_EQ(errno, EINVAL);
+ close(s);
+}
+
+/* SO_J1939_ERRQUEUE delivers TX completion notifications for a TP send. */
+TEST_F(j1939_opts, errqueue_tx_completion)
+{
+ uint8_t tx_buf[ERRQUEUE_MSG_SIZE];
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+
+ self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+ ASSERT_GE(self->tx, 0);
+
+ ASSERT_TRUE(errqueue_enable_ack_ok(self->tx))
+ TH_LOG("enabling the error queue 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_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME, SA_RX,
+ PGN_A));
+
+ ASSERT_TRUE(recv_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS));
+
+ EXPECT_TRUE(errqueue_read_ack_ok(self->tx))
+ TH_LOG("no SCM_TSTAMP_ACK TX completion on the error queue");
+}
+
+/* getsockopt(2) 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(2) reads back the SO_J1939_PROMISC flag, set and cleared. */
+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, true), true)
+ TH_LOG("SO_J1939_PROMISC set round-trip failed: %d", errno);
+ EXPECT_EQ(sockopt_roundtrip(self->tx, SO_J1939_PROMISC, false), false)
+ TH_LOG("SO_J1939_PROMISC clear round-trip failed: %d", errno);
+}
+
+/* getsockopt(2) reads back the SO_J1939_ERRQUEUE flag, set and cleared. */
+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, true), true)
+ TH_LOG("SO_J1939_ERRQUEUE set round-trip failed: %d", errno);
+ EXPECT_EQ(sockopt_roundtrip(self->tx, SO_J1939_ERRQUEUE, false), false)
+ TH_LOG("SO_J1939_ERRQUEUE clear round-trip failed: %d", errno);
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
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 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_opts.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH can-next v3 4/9] selftests: can: add J1939 socket option and metadata tests
2026-11-11 15:25 ` [PATCH can-next v3 4/9] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
@ 2026-11-13 6:02 ` netdev-bot+sashiko
0 siblings, 0 replies; 14+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 6:02 UTC (permalink / raw)
To: o.rempel
Cc: robin, socketcan, mkl, shuah, kernel, linux-can, linux-kernel,
linux-kselftest, fmaurer, mailhol, alexander.hoelzl,
quchaosheng000406, penguin-kernel, kuba
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] In TEST_F(j1939_opts, filter_pgn)
(tools/testing/selftests/net/can/test_j1939_opts.c), the check that…
--
selftests: can: add J1939 socket option and metadata tests
This adds a J1939 selftest that checks socket options and send metadata.
It covers the SO_J1939_SEND_PRIO priority seen on the wire through a
CAN_RAW peer, getsockopt round-trips, the SO_J1939_FILTER PGN whitelist,
unconnected sendto(), a wrong-family bind() and the SO_J1939_ERRQUEUE
TX-completion ACK.
> 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 0000000000000..a729e39c0be4f
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/test_j1939_opts.c
> @@ -0,0 +1,309 @@
[ ... ]
> +/* SO_J1939_FILTER whitelists PGN_A; PGN_A is received, PGN_B is dropped. */
> +TEST_F(j1939_opts, filter_pgn)
> +{
[ ... ]
> + /*
> + * PGN_B must be dropped by the filter. Send PGN_A after it and read it
> + * back: a PGN_B that got through is read before PGN_A or is still
> + * queued after it.
> + */
> + ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
> + self->ifindex, J1939_NO_NAME, SA_RX, PGN_B))
> + TH_LOG("sendto PGN_B failed: %d", errno);
> + ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
> + self->ifindex, J1939_NO_NAME, SA_RX, PGN_A))
> + TH_LOG("sendto PGN_A failed: %d", errno);
> + ASSERT_TRUE(recvfrom_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
> + J1939_RECV_TIMEOUT_MS, &src))
> + TH_LOG("PGN_A after PGN_B was not received");
> + EXPECT_EQ(src.can_addr.j1939.pgn, filt.pgn)
> + TH_LOG("PGN_B was received before PGN_A");
> + EXPECT_EQ(recv(self->rx, rx_buf, sizeof(rx_buf), MSG_DONTWAIT), -1)
> + TH_LOG("PGN_B was received but should have been filtered out");
> + EXPECT_EQ(errno, EAGAIN);
> +}
[Severity: Low]
Do the two looped-back frames always reach the rx socket in send order?
The comment above assumes they do, and so does the v3 changelog entry
"check that the filter drops PGN_B with an ordering barrier, not a
timeout".
The J1939 session queue serializes the two transmissions. Each frame is
then sent from the session's hrtimer:
j1939_tp_txtimer()->j1939_simple_txnext()->j1939_send_one()->can_send()
On vcan without IFF_ECHO, can_send() in net/can/af_can.c loops the frame
back with:
if (newskb)
netif_rx(newskb);
This puts it on the backlog of whichever CPU is running at that moment.
If PGN_A is already queued, j1939_sk_queue_activate_next_locked() starts
its timer from the PGN_B timer's softirq. It normally stays on the same
CPU, so the order holds in that case.
If the PGN_B session has already ended before sendto(PGN_A), the PGN_A
session starts from sendmsg on the test task's current CPU. Suppose the
task has moved to another CPU and NET_RX on the first CPU has been
deferred, for example to ksoftirqd or on PREEMPT_RT. Could PGN_A then be
delivered first?
In that case recvfrom_verify_ok() returns PGN_A. The recv(MSG_DONTWAIT)
then returns -1/EAGAIN before a PGN_B that wrongly passed the filter has
been queued.
This can only give a false pass when SO_J1939_FILTER is broken, never a
false failure. Should the comment and the changelog say less about
ordering, or is there a stronger ordering guarantee the test can rely
on here?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261111-j1939-selftests-v3-0-5a80277ab363%40pengutronix.de
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH can-next v3 5/9] selftests: can: add J1939 addressing and concurrency tests
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (3 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 4/9] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-13 6:02 ` netdev-bot+sashiko
2026-11-11 15:25 ` [PATCH can-next v3 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
` (3 subsequent siblings)
8 siblings, 1 reply; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add a selftest for J1939 NAME-based addressing and address claiming. In
the two conflict tests, a lower NAME takes the address from the socket
and a higher NAME does not. A stress test runs several TP sessions at the
same time, each with its own payload.
The kernel lets a NAME use a claimed address only 250 ms after the
claim. The tests do not sleep for a fixed time before the first send.
They retry it until it succeeds, and two of them also check that it does
not succeed well before the hold-off ends. With J1939_STRICT_TIMING set,
it must also succeed soon after the hold-off ends. The early and late
limits are guesses.
In the NAME-addressed test the receiver is bound to a NAME and no
address, so the frame reaches it only through the kernel's NAME match.
The test that loses its address waits until the kernel has mapped the
winning NAME before it closes its sockets. If the last socket closes
while the hold-off timer of a claim is still pending, the kernel keeps a
reference to the interface and deleting it hangs.
The NAME is serialized onto the wire byte-wise (put_name) so the tests
behave the same on big- and little-endian hosts.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q (sashiko): can the forged Address Claims take an address from another
ECU on a real bus?
A: No. j1939_lib.sh runs the tests only on a vcan it creates.
Q (sashiko): the hang when the last socket closes during the hold-off is
a kernel bug. Why is it not fixed here?
A: This series changes no kernel code; the fix belongs in the stack. The
tests keep a socket open until the hold-off timer has run, even when a
test fails.
Changes in v3:
- wait out the claim hold-off in the teardown of a failed test, so that
a failure does not hang the interface delete (sashiko)
- assert that the claim retry is shorter than the strict window and than
1 s, and that the fixed addresses are unicast
- check that a receiver bound to another NAME gets nothing, and that the
lost address stays lost after the winner is mapped
- check that a send from a claimed NAME carries the claimed address
- assert that the claim test data fits one frame
- fix comments that did not match the code
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, add static asserts, avoid casts and return bool
from helpers named *_ok()
- retry the first send after a claim instead of a fixed 300 ms sleep
(sashiko)
- wait until the kernel has handled a forged claim (sashiko)
- check the kernel's 250 ms hold-off after a claim; in strict mode the
first send must also succeed soon after it ends
- bind the receiver of the NAME test to a NAME only (sashiko)
- stress: give each session its own payload (sashiko), start all
sessions together and check the source address of each message
- stress: if a fork() fails, still reap the children (sashiko)
- check the priority, destination and NAME of the Address Claimed frame
- in the test that loses its address, wait until the kernel has mapped
the winning NAME before closing the sockets
- check that the losing NAME of a defended claim gets no address
- keep the Makefile and .gitignore lists sorted; the wrapper no longer
sets J1939_TEST_PROG
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 1 +
tools/testing/selftests/net/can/j1939_helpers.h | 7 +-
tools/testing/selftests/net/can/test_j1939_addr.c | 736 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_addr.sh | 4 +
5 files changed, 748 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index 81c6e5e4396ac789e0ac1ebad4f11d0f7ef9cbaa..c9b50d4865c4a1f7523b96516669d7fc6e0d151c 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: GPL-2.0-only
+test_j1939_addr
test_j1939_base
test_j1939_opts
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 6b51f8e671461555ee4818cf9a3e8ea7d5fe7c35..857ff49298290aa7322a5f1b02ecc4566442cbbb 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -7,6 +7,7 @@ CFLAGS += -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INC
LOCAL_HDRS := j1939_helpers.h j1939_tests.h ../../clock-helpers.h
TEST_PROGS := \
+ test_j1939_addr.sh \
test_j1939_base.sh \
test_j1939_opts.sh \
test_raw_filter.sh \
diff --git a/tools/testing/selftests/net/can/j1939_helpers.h b/tools/testing/selftests/net/can/j1939_helpers.h
index 0acb7ce1fcc6459989604b19d8dcc5861e5f7086..a120713a1d3d036146d04c3f415406cd9ad17c56 100644
--- a/tools/testing/selftests/net/can/j1939_helpers.h
+++ b/tools/testing/selftests/net/can/j1939_helpers.h
@@ -12,7 +12,12 @@
*
* Timing modes: timer checks use relaxed bounds by default, so that slow
* machines such as QEMU in CI pass. Set J1939_STRICT_TIMING in the
- * environment (any value) for tight bounds on a real machine.
+ * environment (any value) for tight bounds on a real machine:
+ *
+ * J1939_STRICT_TIMING=1 ./test_j1939_addr.sh
+ *
+ * Where a bound differs between the modes, the check picks it with
+ * j1939_timing_ms().
*/
#ifndef __SELFTESTS_NET_CAN_J1939_HELPERS_H__
#define __SELFTESTS_NET_CAN_J1939_HELPERS_H__
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 0000000000000000000000000000000000000000..b3e325be38ddd30255700d327e4e9f53a4e792cd
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_addr.c
@@ -0,0 +1,736 @@
+// 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, the Address Claimed frame on the wire and the kernel's
+ * hold-off after it, address-claim contention where the lower NAME wins, and
+ * a stress run of many concurrent transport sessions.
+ *
+ * Run by test_j1939_addr.sh, see j1939_tests.h.
+ */
+#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"
+
+/* fixed unicast addresses, arbitrary */
+#define SA_TX 0x80
+#define SA_RX 0x90
+#define SA_CLAIM 0x50
+#define SA_DST 0x60
+
+/* stress session i sends from SA_STRESS_TX + i to SA_STRESS_RX + i */
+#define SA_STRESS_TX 0x40
+#define SA_STRESS_RX 0x20
+
+/* NAMEs, arbitrary except for the order of the conflict NAMEs */
+#define NAME_TX 0x00000000c0ffee01ULL
+#define NAME_RX 0x00000000c0ffee02ULL
+/*
+ * Industry Group, NAME bits 62-60. Bit 63, Arbitrary Address Capable, stays
+ * clear.
+ */
+#define NAME_IG_SHIFT 60
+#define NAME_IG_BITS 3
+#define NAME_IG(ig) ((ig) * (1ULL << NAME_IG_SHIFT))
+/*
+ * NAME_LOWER has the lower Industry Group but the higher bottom byte, and
+ * NAME_HIGHER the reverse, so a kernel that compares byte-swapped NAMEs
+ * picks the wrong winner in the conflict tests.
+ */
+#define NAME_CLAIM (NAME_IG(4) | 0xc0ffee40)
+#define NAME_LOWER (NAME_IG(2) | 0xc0ffee60) /* wins */
+#define NAME_HIGHER (NAME_IG(5) | 0xc0ffee20) /* loses */
+_Static_assert(((NAME_CLAIM | NAME_LOWER | NAME_HIGHER) >> NAME_IG_SHIFT) <
+ 1U << NAME_IG_BITS,
+ "the Industry Group must fit its bits, so bit 63 stays clear");
+_Static_assert(NAME_LOWER < NAME_CLAIM && NAME_CLAIM < NAME_HIGHER,
+ "NAME_CLAIM must lie between NAME_LOWER and NAME_HIGHER");
+_Static_assert((NAME_HIGHER & UINT8_MAX) < (NAME_CLAIM & UINT8_MAX),
+ "a byte-swapped NAME compare must reverse the order");
+_Static_assert((NAME_CLAIM & UINT8_MAX) < (NAME_LOWER & UINT8_MAX),
+ "a byte-swapped NAME compare must reverse the order");
+/* the data of an Address Claimed frame is the NAME */
+_Static_assert(sizeof(name_t) == CAN_MAX_DLEN, "a NAME must fill one frame");
+
+/* PGN, arbitrary */
+#define PGN_TEST 0x0ab00
+_Static_assert(J1939_IS_PDU1(PGN_TEST), "PGN_TEST must be PDU1");
+
+/* stress run, arbitrary tuning */
+#define STRESS_N 8
+#define STRESS_SIZE 128 /* a TP size */
+_Static_assert(STRESS_SIZE > J1939_SF_MAX_SIZE, "STRESS_SIZE must need TP");
+_Static_assert(STRESS_SIZE <= J1939_TP_MAX_SIZE, "STRESS_SIZE must fit TP");
+/* the stress addresses lie below all fixed ones, which all differ */
+_Static_assert(SA_STRESS_RX + STRESS_N <= SA_STRESS_TX &&
+ SA_STRESS_TX + STRESS_N <= SA_CLAIM &&
+ SA_CLAIM < SA_DST && SA_CLAIM < SA_TX && SA_CLAIM < SA_RX &&
+ SA_DST != SA_TX && SA_DST != SA_RX && SA_TX != SA_RX,
+ "test addresses overlap");
+_Static_assert((SA_TX <= J1939_MAX_UNICAST_ADDR) &&
+ (SA_RX <= J1939_MAX_UNICAST_ADDR) &&
+ (SA_DST <= J1939_MAX_UNICAST_ADDR),
+ "the fixed addresses must be unicast");
+
+/* ends of a pipe(2) fd pair */
+enum { PIPE_RD, PIPE_WR, PIPE_NUM };
+
+/* bytes of the data frames the claim tests send */
+#define SEND_DATA_LEN CAN_MAX_DLEN
+_Static_assert(SEND_DATA_LEN <= J1939_SF_MAX_SIZE,
+ "SEND_DATA_LEN must fit one frame");
+
+/*
+ * The kernel lets a NAME use a claimed address only ACLAIM_HOLDOFF_MS after
+ * the Address Claimed frame (j1939_ecu_timer_start() in
+ * net/can/j1939/bus.c). Before that, a send from or to that NAME fails with
+ * EADDRNOTAVAIL.
+ *
+ * The first send must succeed no later than ACLAIM_MAX_MS (strict:
+ * ACLAIM_STRICT_MAX_MS). A defended address must stay ours, and the
+ * contender must get none, until that latest time. The tests that time the
+ * first send also check that it does not succeed before ACLAIM_MIN_MS.
+ * ACLAIM_MARGIN_MS is a guess.
+ */
+#define ACLAIM_HOLDOFF_MS 250
+#define ACLAIM_MARGIN_MS 50
+#define ACLAIM_MIN_MS (ACLAIM_HOLDOFF_MS - ACLAIM_MARGIN_MS)
+#define ACLAIM_MAX_MS J1939_RECV_TIMEOUT_MS
+#define ACLAIM_STRICT_MAX_MS (ACLAIM_HOLDOFF_MS + ACLAIM_MARGIN_MS)
+#define ACLAIM_RETRY_MS 25 /* retry interval, arbitrary */
+
+_Static_assert(ACLAIM_MARGIN_MS < ACLAIM_HOLDOFF_MS,
+ "ACLAIM_MIN_MS must stay above zero");
+_Static_assert(ACLAIM_STRICT_MAX_MS <= ACLAIM_MAX_MS,
+ "strict mode must not allow more than relaxed mode");
+_Static_assert(ACLAIM_RETRY_MS < ACLAIM_MARGIN_MS,
+ "a retry must land inside the strict window");
+_Static_assert(ACLAIM_RETRY_MS < MSEC_PER_SEC,
+ "usleep() may reject 1 s or more");
+
+static int j1939_open_name(int ifindex, name_t name, uint8_t sa, pgn_t pgn)
+{
+ return j1939_open_flags(ifindex, name, sa, pgn, J1939_OPEN_NONE);
+}
+
+/*
+ * Wait on obs until deadline for an Address Claimed frame that carries
+ * name_bytes; skip other claims. On failure errno is set, ETIMEDOUT at the
+ * deadline.
+ */
+static bool recv_claim_name_ok(int obs,
+ const uint8_t name_bytes[sizeof(name_t)],
+ const struct timespec *deadline)
+{
+ /* one spare byte: a longer message shows up as a wrong length */
+ uint8_t rx[sizeof(name_t) + 1];
+ const ssize_t want_len = sizeof(name_t);
+ struct timespec now;
+ long left_ms;
+ ssize_t n;
+
+ for (;;) {
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ left_ms = timespec_diff_ms(&now, deadline);
+ if (left_ms <= 0) {
+ errno = ETIMEDOUT;
+ return false;
+ }
+
+ errno = 0;
+ n = recv_timeout(obs, rx, sizeof(rx), left_ms);
+ if (n < 0) {
+ if (!errno) /* poll() timed out */
+ errno = ETIMEDOUT;
+ return false;
+ }
+
+ if (n == want_len && !memcmp(rx, name_bytes, sizeof(name_t)))
+ return true;
+ }
+}
+
+/*
+ * Send an Address Claimed frame for (sa, name) from the raw socket, so that
+ * the kernel maps name to sa, and set *t0 to the time just before it is sent.
+ * Return once the kernel has handled it: j1939_can_recv()
+ * (net/can/j1939/main.c) runs j1939_ac_recv() before j1939_sk_recv(), so a
+ * J1939 socket that receives the claim proves it.
+ *
+ * The caller must keep another J1939 socket bound on ifindex; closing the
+ * last one would drop the mapping. On failure errno is set, ETIMEDOUT if the
+ * claim did not come back within J1939_RECV_TIMEOUT_MS.
+ */
+static bool forge_address_claim_ok(int raw, int ifindex, uint8_t sa,
+ name_t name, struct timespec *t0)
+{
+ uint8_t name_bytes[sizeof(name_t)];
+ struct timespec deadline;
+ bool ok = false;
+ int obs, err;
+
+ obs = j1939_open_flags(ifindex, J1939_NO_NAME, J1939_NO_ADDR,
+ J1939_PGN_ADDRESS_CLAIMED, J1939_OPEN_BCAST);
+ if (obs < 0)
+ return false;
+
+ put_name(name_bytes, name);
+ clock_gettime(CLOCK_MONOTONIC, t0);
+ deadline_after_ms(&deadline, J1939_RECV_TIMEOUT_MS);
+ if (send_raw8_ok(raw, J1939_PRIO_DEFAULT, J1939_PGN_ADDRESS_CLAIMED,
+ J1939_NO_ADDR, sa, name_bytes))
+ ok = recv_claim_name_ok(obs, name_bytes, &deadline);
+
+ err = errno;
+ close(obs);
+ errno = err;
+ return ok;
+}
+
+static int aclaim_max_ms(void)
+{
+ return j1939_timing_ms(ACLAIM_MAX_MS, ACLAIM_STRICT_MAX_MS);
+}
+
+/*
+ * The first sendto() after an Address Claimed frame (send() if dst is NULL).
+ * t0 is the time just before the claim was sent. Retry a failed sendto()
+ * every ACLAIM_RETRY_MS until aclaim_max_ms() after t0. Sets *elapsed_ms to
+ * the time from t0 to the end of the last try.
+ *
+ * Returns true if a send succeeded in time. On false, errno is ETIMEDOUT if
+ * the send succeeded too late (the data was sent), EIO if it sent only part
+ * of the data, else the errno of sendto().
+ */
+static bool send_after_claim_ok(int s, const void *buf, size_t len,
+ const struct sockaddr_can *dst,
+ const struct timespec *t0, long *elapsed_ms)
+{
+ int max_ms = aclaim_max_ms();
+ /* a NULL dst with length 0 makes sendto() act as send() */
+ socklen_t dst_len = dst ? sizeof(*dst) : 0;
+ ssize_t want_len = len, ret;
+ struct timespec now;
+
+ for (;;) {
+ ret = sendto(s, buf, len, 0, (const struct sockaddr *)dst,
+ dst_len);
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ *elapsed_ms = timespec_diff_ms(t0, &now);
+
+ if (ret >= 0 && ret != want_len) {
+ errno = EIO;
+ return false;
+ }
+ if (*elapsed_ms > max_ms) {
+ if (ret == want_len)
+ errno = ETIMEDOUT;
+ return false;
+ }
+ if (ret == want_len)
+ return true;
+ usleep(ACLAIM_RETRY_MS * USEC_PER_MSEC);
+ }
+}
+
+/*
+ * Check that every sendto() to dst fails with EADDRNOTAVAIL. Retry every
+ * ACLAIM_RETRY_MS until aclaim_max_ms() after t0, the latest time a claim may
+ * take effect. One try could hide a claim that the kernel applies late. On
+ * false, errno is that of the last sendto(), or 0 if it succeeded.
+ */
+static bool send_refused_ok(int s, const void *buf, size_t len,
+ const struct sockaddr_can *dst,
+ const struct timespec *t0)
+{
+ int max_ms = aclaim_max_ms();
+ struct timespec now;
+
+ for (;;) {
+ if (j1939_sendto_addr(s, buf, len, dst) >= 0) {
+ errno = 0;
+ return false;
+ }
+ if (errno != EADDRNOTAVAIL)
+ return false;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (timespec_diff_ms(t0, &now) > max_ms)
+ return true;
+ usleep(ACLAIM_RETRY_MS * USEC_PER_MSEC);
+ }
+}
+
+/*
+ * One TP transfer from sa_tx to sa_rx. The payload depends on sa_tx, so a
+ * receiver that gets the data of another session fails. After the setup, write
+ * one byte to ready, close it and wait until the parent closes go, so that all
+ * sessions start together.
+ * Returns the exit status of the child process.
+ */
+static int stress_child(int ifindex, uint8_t sa_tx, uint8_t sa_rx, int ready,
+ int go)
+{
+ uint8_t tx_buf[STRESS_SIZE];
+ int tx, rx, rc = EXIT_FAILURE;
+ struct sockaddr_can src;
+ bool setup, signalled;
+ char c = 0; /* the sync byte, any value */
+ size_t i;
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+ for (i = 0; i < sizeof(tx_buf); i++)
+ tx_buf[i] ^= sa_tx;
+
+ rx = j1939_open(ifindex, sa_rx, PGN_TEST);
+ tx = j1939_open(ifindex, sa_tx, J1939_NO_PGN);
+ setup = rx >= 0 && tx >= 0 &&
+ j1939_connect_ok(tx, ifindex, sa_rx, PGN_TEST);
+
+ /*
+ * Signal even after a setup error, and close ready at once: then the
+ * parent gets EOF, not a hang, if a child dies before its write.
+ */
+ signalled = write_all_ok(ready, &c, sizeof(c));
+ close(ready);
+
+ /* read() returns 0 at EOF, when the parent closes go */
+ if (!signalled || read(go, &c, sizeof(c)) != 0 || !setup)
+ goto out;
+
+ if (!send_all_ok(tx, tx_buf, sizeof(tx_buf)))
+ goto out;
+
+ if (recvfrom_verify_ok(rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS, &src) &&
+ src.can_addr.j1939.addr == sa_tx)
+ rc = EXIT_SUCCESS;
+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)
+{
+ /*
+ * A failed test can stop while the hold-off timer of a claim is
+ * pending (see ac_conflict_lose). The teardown does not know when the
+ * claim started, so wait as long as a claim may take.
+ */
+ /* POSIX allows usleep() to fail with EINVAL for 1 s or more */
+ if (_metadata->exit_code != KSFT_PASS)
+ poll(NULL, 0, ACLAIM_MAX_MS);
+
+ 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[CAN_MAX_DLEN] = {};
+ struct can_frame frame;
+
+ self->raw = open_raw(self->ifindex);
+ ASSERT_GE(self->raw, 0);
+
+ /* static source address: no address claim needed to transmit */
+ self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+ ASSERT_GE(self->tx, 0);
+ ASSERT_TRUE(j1939_connect_name_ok(self->tx, self->ifindex,
+ J1939_NO_NAME, SA_RX, PGN_TEST));
+
+ ASSERT_TRUE(send_all_ok(self->tx, buf, sizeof(buf)));
+
+ ASSERT_TRUE(recv_frame_ok(self->raw, &frame, J1939_RECV_TIMEOUT_MS));
+ ASSERT_TRUE(j1939_is_data_frame(frame.can_id))
+ TH_LOG("CAN id 0x%08x is not a 29-bit data frame",
+ frame.can_id);
+ EXPECT_EQ(j1939_id_sa(frame.can_id), SA_TX)
+ TH_LOG("source address on wire is 0x%02x, expected 0x%02x",
+ 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[CAN_MAX_DLEN];
+ struct timespec t0;
+ long elapsed_ms;
+ int other;
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+
+ /*
+ * Bind the receiver to NAME_RX and no address, so it gets the frame
+ * only if the kernel matches the destination NAME. The forged claim
+ * maps NAME_RX -> SA_RX.
+ */
+ self->rx = j1939_open_name(self->ifindex, NAME_RX, J1939_NO_ADDR,
+ PGN_TEST);
+ ASSERT_GE(self->rx, 0);
+
+ /* a receiver bound to another NAME must not get the frame */
+ other = j1939_open_name(self->ifindex, NAME_TX, J1939_NO_ADDR,
+ PGN_TEST);
+ ASSERT_GE(other, 0);
+
+ self->raw = open_raw(self->ifindex);
+ ASSERT_GE(self->raw, 0);
+
+ ASSERT_TRUE(forge_address_claim_ok(self->raw, self->ifindex, SA_RX,
+ NAME_RX, &t0))
+ TH_LOG("failed to inject address claim: %d", errno);
+
+ self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+ ASSERT_GE(self->tx, 0);
+ ASSERT_TRUE(j1939_connect_name_ok(self->tx, self->ifindex, NAME_RX,
+ J1939_NO_ADDR, PGN_TEST))
+ TH_LOG("connect to peer NAME failed: %d", errno);
+
+ ASSERT_TRUE(send_after_claim_ok(self->tx, tx_buf, sizeof(tx_buf), NULL,
+ &t0, &elapsed_ms))
+ TH_LOG("send to peer NAME failed after %ld ms: %s",
+ elapsed_ms, strerror(errno));
+ /* but not before the kernel's hold-off after the claim */
+ EXPECT_GE(elapsed_ms, ACLAIM_MIN_MS)
+ TH_LOG("peer NAME reachable after only %ld ms", elapsed_ms);
+
+ EXPECT_TRUE(recv_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS));
+ EXPECT_EQ(recv_timeout(other, tx_buf, sizeof(tx_buf),
+ J1939_NEG_RECV_TIMEOUT_MS), -1)
+ TH_LOG("a receiver bound to another NAME got the frame");
+ close(other);
+}
+
+/* Many concurrent transport-protocol sessions run without corruption. */
+TEST_F(j1939_addr, stress_concurrent_sessions)
+{
+ int i, n, ready[PIPE_NUM], go[PIPE_NUM], failed = 0;
+ pid_t pids[STRESS_N];
+ char c;
+
+ ASSERT_EQ(pipe(ready), 0);
+ ASSERT_EQ(pipe(go), 0);
+
+ for (n = 0; n < STRESS_N; n++) {
+ pids[n] = fork();
+ if (pids[n] < 0)
+ break;
+ if (pids[n] == 0) {
+ close(ready[PIPE_RD]);
+ close(go[PIPE_WR]);
+ _exit(stress_child(self->ifindex, SA_STRESS_TX + n,
+ SA_STRESS_RX + n, ready[PIPE_WR],
+ go[PIPE_RD]));
+ }
+ }
+
+ close(ready[PIPE_WR]);
+ close(go[PIPE_RD]);
+
+ /* wait until all children are set up, then start them */
+ for (i = 0; i < n; i++)
+ if (!read_all_ok(ready[PIPE_RD], &c, sizeof(c)))
+ break;
+ close(ready[PIPE_RD]);
+ close(go[PIPE_WR]);
+
+ for (i = 0; i < n; i++) {
+ int status;
+
+ if (waitpid(pids[i], &status, 0) < 0 ||
+ !WIFEXITED(status) || WEXITSTATUS(status) != EXIT_SUCCESS)
+ failed++;
+ }
+
+ ASSERT_EQ(n, STRESS_N)
+ TH_LOG("fork failed after %d of %d children", n, STRESS_N);
+ EXPECT_EQ(failed, 0)
+ TH_LOG("%d of %d concurrent sessions failed", failed, STRESS_N);
+}
+
+/*
+ * Claim an address the way j1939acd would: bind a NAME to an address and send
+ * an Address Claimed message. Returns the (still open) socket on success, and
+ * in *t0 the time just before the claim was sent.
+ */
+static int claim_addr(int ifindex, name_t name, uint8_t sa,
+ struct timespec *t0)
+{
+ uint8_t name_bytes[sizeof(name_t)];
+ int s;
+
+ s = j1939_open_flags(ifindex, name, sa, J1939_NO_PGN, J1939_OPEN_BCAST);
+ if (s < 0)
+ return -1;
+
+ put_name(name_bytes, name);
+ clock_gettime(CLOCK_MONOTONIC, t0);
+ if (!j1939_sendto_ok(s, name_bytes, sizeof(name_bytes), ifindex,
+ J1939_NO_NAME, J1939_NO_ADDR,
+ J1939_PGN_ADDRESS_CLAIMED)) {
+ close(s);
+ return -1;
+ }
+
+ return s;
+}
+
+/* The destination of the data frames, PGN_TEST to SA_DST. */
+static void fill_data_dst(struct sockaddr_can *dst, int ifindex)
+{
+ j1939_fill_sockaddr(dst, ifindex, J1939_NO_NAME, SA_DST, PGN_TEST);
+}
+
+static ssize_t send_data(int s, int ifindex)
+{
+ uint8_t data[SEND_DATA_LEN] = {};
+ struct sockaddr_can dst;
+
+ fill_data_dst(&dst, ifindex);
+ return j1939_sendto_addr(s, data, sizeof(data), &dst);
+}
+
+/* send_data(), as the first send after a claim; see send_after_claim_ok() */
+static bool send_data_after_claim_ok(int s, int ifindex,
+ const struct timespec *t0,
+ long *elapsed_ms)
+{
+ uint8_t data[SEND_DATA_LEN] = {};
+ struct sockaddr_can dst;
+
+ fill_data_dst(&dst, ifindex);
+ return send_after_claim_ok(s, data, sizeof(data), &dst, t0, elapsed_ms);
+}
+
+/*
+ * Is id an Address Claimed frame from the address in *ctx (a uint8_t), as
+ * claim_addr() sends it: default priority, global destination?
+ */
+static bool match_addr_claimed(canid_t id, const void *ctx)
+{
+ const uint8_t *sa = ctx;
+
+ return j1939_id_prio(id) == J1939_PRIO_DEFAULT &&
+ j1939_id_pf(id) == J1939_PF(J1939_PGN_ADDRESS_CLAIMED) &&
+ j1939_id_ps(id) == J1939_NO_ADDR &&
+ j1939_id_sa(id) == *sa;
+}
+
+/* Is id a PGN_TEST data frame from the source address *ctx? */
+static bool match_data_from(canid_t id, const void *ctx)
+{
+ const uint8_t *sa = ctx;
+
+ return j1939_id_pf(id) == J1939_PF(PGN_TEST) &&
+ j1939_id_sa(id) == *sa;
+}
+
+/* Sending an Address Claimed message puts it on the wire and claims. */
+TEST_F(j1939_addr, address_claim_send)
+{
+ uint8_t name_bytes[sizeof(name_t)];
+ const uint8_t claim_sa = SA_CLAIM;
+ struct can_frame frame;
+ struct timespec t0;
+ long elapsed_ms;
+ int name_tx;
+
+ put_name(name_bytes, NAME_CLAIM);
+
+ self->raw = open_raw(self->ifindex);
+ ASSERT_GE(self->raw, 0);
+
+ self->tx = claim_addr(self->ifindex, NAME_CLAIM, SA_CLAIM, &t0);
+ ASSERT_GE(self->tx, 0)
+ TH_LOG("address claim failed: %d", errno);
+
+ ASSERT_TRUE(recv_match_ok(self->raw, J1939_RECV_TIMEOUT_MS, &frame,
+ match_addr_claimed, &claim_sa))
+ TH_LOG("no Address Claimed frame seen on the wire");
+ EXPECT_EQ(frame.can_dlc, sizeof(name_bytes));
+ EXPECT_EQ(memcmp(frame.data, name_bytes, sizeof(name_bytes)), 0)
+ TH_LOG("Address Claimed frame does not carry NAME_CLAIM");
+
+ /*
+ * Send from a socket bound to NAME_CLAIM and no address, so that only
+ * the kernel's NAME lookup can put SA_CLAIM on the wire.
+ */
+ name_tx = j1939_open_name(self->ifindex, NAME_CLAIM, J1939_NO_ADDR,
+ J1939_NO_PGN);
+ ASSERT_GE(name_tx, 0);
+ EXPECT_TRUE(send_data_after_claim_ok(name_tx, self->ifindex, &t0,
+ &elapsed_ms))
+ TH_LOG("transmit from claimed address failed after %ld ms: %s",
+ elapsed_ms, strerror(errno));
+ /* but not before the kernel's hold-off after the claim */
+ EXPECT_GE(elapsed_ms, ACLAIM_MIN_MS)
+ TH_LOG("claimed address usable after only %ld ms", elapsed_ms);
+ /* and the data frame carries the claimed address */
+ EXPECT_TRUE(recv_match_ok(self->raw, J1939_RECV_TIMEOUT_MS, &frame,
+ match_data_from, &claim_sa))
+ TH_LOG("no data frame from the claimed address on the wire");
+ close(name_tx);
+}
+
+/*
+ * Claim SA_CLAIM with NAME_CLAIM and check that we can send. Then forge a
+ * claim for SA_CLAIM with the contender NAME and set *forge_t0 to the time
+ * just before it is sent. Store the result of the next send in *tx_ret, with
+ * errno as that send set it. Return false if a setup step failed.
+ */
+static bool ac_conflict_probe_ok(FIXTURE_DATA(j1939_addr) *self,
+ name_t contender, ssize_t *tx_ret,
+ struct timespec *forge_t0)
+{
+ struct timespec t0;
+ long elapsed_ms;
+
+ self->raw = open_raw(self->ifindex);
+ if (self->raw < 0)
+ return false;
+ self->tx = claim_addr(self->ifindex, NAME_CLAIM, SA_CLAIM, &t0);
+ if (self->tx < 0)
+ return false;
+
+ /* baseline: we own the address and can transmit */
+ if (!send_data_after_claim_ok(self->tx, self->ifindex, &t0,
+ &elapsed_ms))
+ return false;
+
+ if (!forge_address_claim_ok(self->raw, self->ifindex, SA_CLAIM,
+ contender, forge_t0))
+ return false;
+
+ /*
+ * One plain send, no retry: the kernel has already handled the
+ * contending claim (see forge_address_claim_ok()), so the result must
+ * be final now. A retry could hide a wrong first result.
+ */
+ *tx_ret = send_data(self->tx, self->ifindex);
+ return true;
+}
+
+/* A higher-priority competing claim takes the address away from us. */
+TEST_F(j1939_addr, ac_conflict_lose)
+{
+ uint8_t data[SEND_DATA_LEN] = {};
+ struct sockaddr_can winner;
+ struct timespec forge_t0;
+ long elapsed_ms;
+ ssize_t tx_ret;
+
+ ASSERT_TRUE(ac_conflict_probe_ok(self, NAME_LOWER, &tx_ret,
+ &forge_t0));
+
+ EXPECT_LT(tx_ret, 0)
+ TH_LOG("transmit still succeeded after losing the address");
+ EXPECT_EQ(errno, EADDRNOTAVAIL);
+
+ /*
+ * Keep the sockets open until the kernel maps NAME_LOWER at the end
+ * of its hold-off. If the last socket closes while that timer is
+ * pending, the kernel keeps a reference to the interface and
+ * deleting it hangs.
+ */
+ self->rx = j1939_open(self->ifindex, SA_RX, J1939_NO_PGN);
+ ASSERT_GE(self->rx, 0);
+ j1939_fill_sockaddr(&winner, self->ifindex, NAME_LOWER, J1939_NO_ADDR,
+ PGN_TEST);
+ EXPECT_TRUE(send_after_claim_ok(self->rx, data, sizeof(data), &winner,
+ &forge_t0, &elapsed_ms))
+ TH_LOG("send to NAME_LOWER failed after %ld ms: %s",
+ elapsed_ms, strerror(errno));
+
+ /* the lost address stays lost once the winner is mapped */
+ EXPECT_LT(send_data(self->tx, self->ifindex), 0)
+ TH_LOG("transmit succeeded again after the winner was mapped");
+ EXPECT_EQ(errno, EADDRNOTAVAIL);
+}
+
+/* A lower-priority competing claim does not take our address. */
+TEST_F(j1939_addr, ac_conflict_defend)
+{
+ uint8_t data[SEND_DATA_LEN] = {};
+ struct sockaddr_can loser;
+ struct timespec forge_t0;
+ ssize_t tx_ret;
+
+ ASSERT_TRUE(ac_conflict_probe_ok(self, NAME_HIGHER, &tx_ret,
+ &forge_t0));
+
+ EXPECT_EQ(tx_ret, SEND_DATA_LEN)
+ TH_LOG("lost the address to a lower-priority claim");
+
+ self->rx = j1939_open(self->ifindex, SA_RX, J1939_NO_PGN);
+ ASSERT_GE(self->rx, 0);
+ j1939_fill_sockaddr(&loser, self->ifindex, NAME_HIGHER, J1939_NO_ADDR,
+ PGN_TEST);
+ EXPECT_TRUE(send_refused_ok(self->rx, data, sizeof(data), &loser,
+ &forge_t0))
+ TH_LOG("no EADDRNOTAVAIL for the lower-priority NAME: %s",
+ errno ? strerror(errno) : "it was sent");
+
+ EXPECT_EQ(send_data(self->tx, self->ifindex), SEND_DATA_LEN)
+ TH_LOG("lost the address later to a lower-priority claim");
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
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 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_addr.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH can-next v3 5/9] selftests: can: add J1939 addressing and concurrency tests
2026-11-11 15:25 ` [PATCH can-next v3 5/9] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
@ 2026-11-13 6:02 ` netdev-bot+sashiko
0 siblings, 0 replies; 14+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 6:02 UTC (permalink / raw)
To: o.rempel
Cc: robin, socketcan, mkl, shuah, kernel, linux-can, linux-kernel,
linux-kselftest, fmaurer, mailhol, alexander.hoelzl,
quchaosheng000406, penguin-kernel, kuba
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The ACLAIM_* block comment in
tools/testing/selftests/net/can/test_j1939_addr.c (lines 99-100) says:…
Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the patch
depends on it and works around it.
--
selftests: can: add J1939 addressing and concurrency tests
This commit adds a J1939 selftest. It covers NAME-based addressing, the
Address Claimed frame and its 250 ms hold-off, address claim conflicts,
and a stress run of concurrent TP sessions. To avoid a hang when the
interface is deleted, the tests keep a socket open until the hold-off
timer has run.
> 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 0000000000000..b3e325be38ddd
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/test_j1939_addr.c
> @@ -0,0 +1,736 @@
[ ... ]
> +/*
> + * The kernel lets a NAME use a claimed address only ACLAIM_HOLDOFF_MS after
> + * the Address Claimed frame (j1939_ecu_timer_start() in
> + * net/can/j1939/bus.c). Before that, a send from or to that NAME fails with
> + * EADDRNOTAVAIL.
> + *
> + * The first send must succeed no later than ACLAIM_MAX_MS (strict:
> + * ACLAIM_STRICT_MAX_MS). A defended address must stay ours, and the
> + * contender must get none, until that latest time. The tests that time the
> + * first send also check that it does not succeed before ACLAIM_MIN_MS.
[Severity: Low]
Is this comment accurate? Only name_addressing and address_claim_send
check EXPECT_GE(elapsed_ms, ACLAIM_MIN_MS).
ac_conflict_lose also times the first send to NAME_LOWER after the forged
claim with send_after_claim_ok(). It only uses elapsed_ms in TH_LOG():
EXPECT_TRUE(send_after_claim_ok(self->rx, data, sizeof(data), &winner,
&forge_t0, &elapsed_ms))
TH_LOG("send to NAME_LOWER failed after %ld ms: %s",
elapsed_ms, strerror(errno));
The baseline send_data_after_claim_ok() in ac_conflict_probe_ok() has no
lower bound check either.
The kernel makes the winning NAME reachable only once the hold-off timer
fires, so a lower bound check in ac_conflict_lose would be valid. As
written, a kernel that maps the winning contender right away would still
pass ac_conflict_lose.
Should the comment be reworded, or should ac_conflict_lose add an
EXPECT_GE(elapsed_ms, ACLAIM_MIN_MS) check?
> + * ACLAIM_MARGIN_MS is a guess.
> + */
[ ... ]
> +FIXTURE_TEARDOWN(j1939_addr)
> +{
> + /*
> + * A failed test can stop while the hold-off timer of a claim is
> + * pending (see ac_conflict_lose). The teardown does not know when the
> + * claim started, so wait as long as a claim may take.
> + */
> + /* POSIX allows usleep() to fail with EINVAL for 1 s or more */
> + if (_metadata->exit_code != KSFT_PASS)
> + poll(NULL, 0, ACLAIM_MAX_MS);
[Severity: High]
This isn't a bug introduced by this patch, but this teardown works around
a leak in net/can/j1939. Because of the workaround, the suite cannot catch
that leak.
j1939_ac_process() arms the hold-off timer and then drops its own
reference. During the hold-off, only ecu->ac_timer holds the new ECU:
net/can/j1939/address-claim.c:j1939_ac_process() {
...
j1939_ecu_timer_start(ecu);
out_ecu_put:
j1939_ecu_put(ecu);
}
When the last socket closes, __j1939_rx_release() tears down the priv but
does not cancel a pending ac_timer:
net/can/j1939/main.c:__j1939_rx_release() {
...
j1939_can_rx_unregister(priv);
j1939_ecu_unmap_all(priv);
j1939_priv_set(priv->ndev, NULL);
...
}
j1939_ecu_unmap_all() only walks priv->ents[], so it skips an ECU that is
still in its hold-off:
for (i = 0; i < ARRAY_SIZE(priv->ents); i++)
if (priv->ents[i].ecu)
j1939_ecu_unmap_locked(priv->ents[i].ecu);
When the timer fires later, j1939_ecu_timer_handler() maps the ECU
without any check. That takes a new mapping reference:
write_lock_bh(&priv->lock);
j1939_ecu_map_locked(ecu);
j1939_ecu_put(ecu);
write_unlock_bh(&priv->lock);
Nothing unmaps the ECU after that. The priv is already detached from the
ndev, so j1939_netdev_notify() returns early on NETDEV_UNREGISTER:
priv = j1939_priv_get_by_ndev(ndev);
if (!priv)
goto notify_done;
The ECU keeps its netdev_hold() and j1939_priv_get(), and the priv keeps
its own netdev_hold(). unregister_netdevice() then waits forever.
can_create() and J1939 bind/sendto check no capabilities. Can a local
unprivileged user trigger this by sending an Address Claimed frame and
closing the socket within 250 ms? A claim from a remote ECU that arrives
just before the last local socket closes looks like it would take the same
path.
This poll(), and ac_conflict_lose keeping a socket open until NAME_LOWER
is mapped, keep the suite out of that window. The suite therefore passes
on a kernel that has the bug. The commit notes say "the fix belongs in the
stack". Could a test that closes the last socket during the hold-off and
then deletes the interface be added once the stack is fixed?
> +
> + if (self->tx >= 0)
> + close(self->tx);
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261111-j1939-selftests-v3-0-5a80277ab363%40pengutronix.de
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH can-next v3 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (4 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 5/9] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 7/9] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
` (2 subsequent siblings)
8 siblings, 0 replies; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Check J1939 single frames against a CAN_RAW socket in both directions,
for PDU1 and PDU2, both with the DP bit set: the CAN id and payload of a
J1939 send, and the payload, source address and PGN that the J1939
socket gets for an injected frame.
The base test sends between two J1939 sockets, so a CAN id encoding bug
and a matching decoding bug in the kernel cancel out there. CAN_RAW does
not use the J1939 code, so it checks each direction on its own.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q (sashiko): does the test check the priority of a received frame, or
the SCM_J1939_PRIO cmsg?
A: Neither. It checks the priority of sent frames only; the cmsg
metadata of received messages is not tested yet.
Q (sashiko): can the TX tests see a frame from another node?
A: No. They still read the first frame, but j1939_lib.sh runs them
only on a vcan it creates, where no other node sends.
Changes in v3:
- the @bcast comments say that @bcast opens the socket with SO_BROADCAST
(sashiko)
- assert that the addresses are two different unicast ones and that the
PGNs are valid, with the DP bit set and PS 0 for PDU1
- set the DP bit in the PDU2 PGN too
- clarify the @pgn comment and the RX mismatch log
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, avoid casts, return bool from helpers named
*_ok() and fill test payloads with fill_pattern()
- check that a sent frame is an extended data frame (sashiko, Marc)
- check the full PGN of a sent frame (sashiko); use a PDU1 PGN with DP
set
- check the source address and PGN of a received frame (sashiko)
- say in the commit message what CAN_RAW checks that the base test
cannot (sashiko)
- keep the Makefile and .gitignore lists sorted; the wrapper no longer
sets J1939_TEST_PROG
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 1 +
tools/testing/selftests/net/can/test_j1939_wire.c | 196 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_wire.sh | 4 +
4 files changed, 202 insertions(+)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index c9b50d4865c4a1f7523b96516669d7fc6e0d151c..d83fd28a50766c25069c60d10deb76e5722bba5b 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -2,4 +2,5 @@
test_j1939_addr
test_j1939_base
test_j1939_opts
+test_j1939_wire
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 857ff49298290aa7322a5f1b02ecc4566442cbbb..21898e645ced72f1fb450c920e85fb7e0151e2ad 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -10,6 +10,7 @@ TEST_PROGS := \
test_j1939_addr.sh \
test_j1939_base.sh \
test_j1939_opts.sh \
+ test_j1939_wire.sh \
test_raw_filter.sh \
# end of TEST_PROGS
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 0000000000000000000000000000000000000000..b476ac0fbcfb8595d6ac4d6167ce178c603b091f
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.c
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 wire-format selftests: a J1939 socket against CAN_RAW.
+ *
+ * Run by test_j1939_wire.sh, see j1939_tests.h.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+/* arbitrary addresses and PGNs, within the asserts below */
+#define SA_LOCAL 0x90 /* the J1939 socket under test */
+#define SA_PEER 0x20 /* the CAN_RAW peer */
+/* a destination-specific and a broadcast PGN, both with DP set */
+#define PGN_PDU1 (J1939_PGN_DP | 0x0ab00)
+#define PGN_PDU2 (J1939_PGN_DP | 0x0feee)
+_Static_assert(J1939_IS_PDU1(PGN_PDU1) && !J1939_IS_PDU1(PGN_PDU2),
+ "PGN_PDU1 must be PDU1 and PGN_PDU2 must be PDU2");
+_Static_assert((PGN_PDU1 & J1939_PGN_DP) && (PGN_PDU2 & J1939_PGN_DP),
+ "both PGNs must have the DP bit set");
+_Static_assert((PGN_PDU1 & UINT8_MAX) == 0,
+ "the kernel rejects a PDU1 PGN whose PS byte is not 0");
+_Static_assert(PGN_PDU1 <= J1939_PGN_MAX && PGN_PDU2 <= J1939_PGN_MAX,
+ "PGN_PDU1 and PGN_PDU2 must be valid PGNs");
+_Static_assert((SA_LOCAL != SA_PEER) &&
+ (SA_LOCAL <= J1939_MAX_UNICAST_ADDR) &&
+ (SA_PEER <= J1939_MAX_UNICAST_ADDR),
+ "SA_LOCAL and SA_PEER must be two different unicast addresses");
+
+#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, J1939_RAW_RCVBUF_DEFAULT);
+}
+
+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 check that the J1939 socket
+ * receives the same CAN_MAX_DLEN-byte payload, from SA_PEER and with PGN @pgn.
+ * Opens the J1939 RX socket into self->j1939 (the fixture teardown closes it).
+ * @bcast opens the socket with SO_BROADCAST. @pgn is the PGN of the injected
+ * frame and the one the socket binds to; @da is the frame's destination.
+ */
+static bool rx_single_frame_ok(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+ pgn_t pgn, uint8_t da,
+ const uint8_t payload[CAN_MAX_DLEN])
+{
+ struct sockaddr_can src;
+
+ 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 false;
+
+ if (!send_raw8_ok(self->raw, PRIO, pgn, da, SA_PEER, payload))
+ return false;
+
+ if (!recvfrom_verify_ok(self->j1939, payload, CAN_MAX_DLEN,
+ J1939_RECV_TIMEOUT_MS, &src)) {
+ fprintf(stderr, "# payload missing or not the one sent\n");
+ return false;
+ }
+
+ if (src.can_addr.j1939.addr != SA_PEER ||
+ src.can_addr.j1939.pgn != pgn) {
+ fprintf(stderr,
+ "# got SA 0x%02x PGN 0x%05x, want SA 0x%02x PGN 0x%05x\n",
+ src.can_addr.j1939.addr, src.can_addr.j1939.pgn,
+ SA_PEER, pgn);
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Send one CAN_MAX_DLEN-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 it with SO_BROADCAST.
+ */
+static bool tx_to_raw_send_recv_ok(FIXTURE_DATA(j1939_wire) *self, bool bcast,
+ pgn_t pgn, uint8_t da,
+ const uint8_t payload[CAN_MAX_DLEN],
+ 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 false;
+
+ if (!j1939_connect_ok(self->j1939, self->ifindex, da, pgn))
+ return false;
+
+ if (!send_all_ok(self->j1939, payload, CAN_MAX_DLEN))
+ return false;
+
+ return recv_frame_ok(self->raw, frame, J1939_RECV_TIMEOUT_MS);
+}
+
+/* J1939 TX -> CAN_RAW: destination-specific (PDU1) single frame. */
+TEST_F(j1939_wire, tx_pdu1_to_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(tx_to_raw_send_recv_ok(self, false, PGN_PDU1, SA_PEER,
+ payload, &frame));
+ ASSERT_TRUE(j1939_is_data_frame(frame.can_id))
+ TH_LOG("CAN id 0x%08x is not a 29-bit data frame",
+ frame.can_id);
+
+ EXPECT_EQ(j1939_id_prio(frame.can_id), PRIO);
+ EXPECT_EQ(j1939_id_sa(frame.can_id), SA_LOCAL);
+ EXPECT_EQ(j1939_id_ps(frame.can_id), SA_PEER);
+ /* PGN, with the PS (the destination) masked off */
+ EXPECT_EQ(j1939_id_pgn(frame.can_id) & J1939_PGN_PDU1_MAX, 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[CAN_MAX_DLEN];
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(rx_single_frame_ok(self, false, PGN_PDU1, SA_LOCAL,
+ payload));
+}
+
+/* J1939 TX -> CAN_RAW: broadcast (PDU2) single frame. */
+TEST_F(j1939_wire, tx_pdu2_to_raw)
+{
+ uint8_t payload[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(tx_to_raw_send_recv_ok(self, true, PGN_PDU2, J1939_NO_ADDR,
+ payload, &frame));
+ ASSERT_TRUE(j1939_is_data_frame(frame.can_id))
+ TH_LOG("CAN id 0x%08x is not a 29-bit data frame",
+ frame.can_id);
+
+ EXPECT_EQ(j1939_id_prio(frame.can_id), PRIO);
+ EXPECT_EQ(j1939_id_sa(frame.can_id), SA_LOCAL);
+ EXPECT_EQ(j1939_id_pgn(frame.can_id), PGN_PDU2);
+
+ 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[CAN_MAX_DLEN];
+
+ fill_pattern(payload, sizeof(payload));
+ ASSERT_TRUE(rx_single_frame_ok(self, true, PGN_PDU2, J1939_NO_ADDR,
+ payload));
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
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 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH can-next v3 7/9] selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (5 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-13 6:02 ` netdev-bot+sashiko
2026-11-11 15:25 ` [PATCH can-next v3 8/9] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 9/9] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
8 siblings, 1 reply; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add Transport Protocol (TP) and Extended Transport Protocol (ETP)
round-trip tests in both directions, with a CAN_RAW socket as the
transport peer. The raw peer sees the frames as they are on the wire, so
a framing bug in the kernel cannot cancel out against its own decoder.
The tests use a PGN with the DP bit set and check it in each control
frame the kernel sends. In the TX tests, the peer's EOMA must end the
session. The kernel then queues a TX-completion ACK on the socket's
error queue. No ACK may come before the EOMA.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q: can a test in test_j1939_multi.c fail without a bug in the test?
A: Yes, through one known race in the stack: it updates a session from
the loopback echo of its own CTS, RTS and DT frames, and a CAN_RAW peer
can see that echo before the stack does. A peer frame that follows
quickly can then cancel the session and fail the test. The fix
belongs in the stack; this series changes no kernel code.
Changes in v3:
- describe the known stack race in a comment at the top of the file
(sashiko)
- assert that the addresses are two different unicast ones and that
PGN_DATA is a valid PDU2 PGN
- assert that the last DT frames and the last ETP block are partial
- pass the buffer size to the ETP helpers
- check the RTS packets-per-CTS byte (the total or no limit), the fill
bytes of each DT frame and the length of the kernel's control frames
- say in the logs which check failed: any error-queue entry before the
EOMA, and a malformed ETP CTS or DPO
- use a TP size above 255 bytes, so the high byte of the size field
counts
- fix comments that did not match the code
- put errqueue_empty_ok() in j1939_helpers.c and j1939_helpers.h
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, add static asserts, avoid casts and return bool
from helpers named *_ok()
- fail at once on a CTS for 0 packets, a CTS that does not ask for the
next packet, or a DPO that does not match the CTS
- check the source address of the kernel's TP/ETP frames
- check the packet count in each CTS: all packets left, capped by the
RTS limit for TP and by a full block for ETP
- check the PGN in each control frame the kernel sends, with a PGN that
has the DP bit set
- check that the peer's EOMA ends a TP or ETP TX session with a
TX-completion ACK, and that no ACK comes before it (sashiko)
- a reassembled message longer than the one sent now fails (sashiko)
- check the sequence number and the length of each DT frame the kernel
sends (sashiko)
- keep the Makefile and .gitignore lists sorted; the wrapper no longer
sets J1939_TEST_PROG
---
tools/testing/selftests/net/can/.gitignore | 1 +
tools/testing/selftests/net/can/Makefile | 1 +
tools/testing/selftests/net/can/j1939_helpers.c | 7 +
tools/testing/selftests/net/can/j1939_helpers.h | 3 +
tools/testing/selftests/net/can/test_j1939_multi.c | 682 +++++++++++++++++++++
.../testing/selftests/net/can/test_j1939_multi.sh | 4 +
6 files changed, 698 insertions(+)
diff --git a/tools/testing/selftests/net/can/.gitignore b/tools/testing/selftests/net/can/.gitignore
index d83fd28a50766c25069c60d10deb76e5722bba5b..9042b532e0e19e5e9f4e0ecae3f28d73b7089405 100644
--- a/tools/testing/selftests/net/can/.gitignore
+++ b/tools/testing/selftests/net/can/.gitignore
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-only
test_j1939_addr
test_j1939_base
+test_j1939_multi
test_j1939_opts
test_j1939_wire
test_raw_filter
diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
index 21898e645ced72f1fb450c920e85fb7e0151e2ad..99113184a525a9496091ace622f56303d9f3c2ff 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -9,6 +9,7 @@ LOCAL_HDRS := j1939_helpers.h j1939_tests.h ../../clock-helpers.h
TEST_PROGS := \
test_j1939_addr.sh \
test_j1939_base.sh \
+ test_j1939_multi.sh \
test_j1939_opts.sh \
test_j1939_wire.sh \
test_raw_filter.sh \
diff --git a/tools/testing/selftests/net/can/j1939_helpers.c b/tools/testing/selftests/net/can/j1939_helpers.c
index 0435c973e837c09bc73eb774939a957f31fdd554..65073320ee66643e2447f4e57ccecdc5768a4b17 100644
--- a/tools/testing/selftests/net/can/j1939_helpers.c
+++ b/tools/testing/selftests/net/can/j1939_helpers.c
@@ -497,6 +497,13 @@ bool errqueue_read_ack_ok(int s)
return msg_has_tstamp_ack(&msg);
}
+bool errqueue_empty_ok(int s)
+{
+ struct pollfd pfd = { .fd = s, .events = POLLERR };
+
+ return poll(&pfd, 1, J1939_NEG_RECV_TIMEOUT_MS) == 0;
+}
+
void fill_pattern(uint8_t *buf, size_t len)
{
size_t i;
diff --git a/tools/testing/selftests/net/can/j1939_helpers.h b/tools/testing/selftests/net/can/j1939_helpers.h
index a120713a1d3d036146d04c3f415406cd9ad17c56..2e29a106e4dc60b0aeee921b5607f70ed2c6778d 100644
--- a/tools/testing/selftests/net/can/j1939_helpers.h
+++ b/tools/testing/selftests/net/can/j1939_helpers.h
@@ -355,6 +355,9 @@ bool msg_has_tstamp_ack(struct msghdr *msg);
*/
bool errqueue_read_ack_ok(int s);
+/* Is the error queue of s still empty after J1939_NEG_RECV_TIMEOUT_MS? */
+bool errqueue_empty_ok(int s);
+
/* Arbitrary; keeps fill_pattern() from being the plain byte offset. */
#define J1939_PATTERN_SEED 0x3c
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 0000000000000000000000000000000000000000..aec96e83305cf2d9129aed3ccdf4035437337ba7
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_multi.c
@@ -0,0 +1,682 @@
+// 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 against the kernel J1939 stack,
+ * in both directions. It takes the other side of the RTS/CTS/DT/EOMA
+ * exchange, for TP with a short message and for ETP with a payload that
+ * spans several CTS blocks (DPO windows).
+ *
+ * Known stack race: the stack updates a session from the loopback echo of
+ * its own CTS, RTS and DT frames, and the CAN_RAW peer can read that echo
+ * before the stack has handled it. A peer frame that follows quickly can
+ * then cancel the session, so these tests can fail at random. The fix
+ * belongs in the stack; this file does not work around it.
+ *
+ * Run by test_j1939_multi.sh, see j1939_tests.h.
+ */
+#include <errno.h>
+
+#include "kselftest_harness.h"
+#include "j1939_tests.h"
+
+/* arbitrary addresses */
+#define SA_LOCAL 0x90 /* the J1939 socket under test */
+#define SA_PEER 0x20 /* the CAN_RAW transport peer */
+_Static_assert((SA_LOCAL != SA_PEER) &&
+ (SA_LOCAL <= J1939_MAX_UNICAST_ADDR) &&
+ (SA_PEER <= J1939_MAX_UNICAST_ADDR),
+ "SA_LOCAL and SA_PEER must be two different unicast addresses");
+/*
+ * transported message PGN, an arbitrary PDU2 PGN with the DP bit set so a
+ * PGN cut to 16 bits fails the check
+ */
+#define PGN_DATA (J1939_PGN_DP | 0x0feef)
+_Static_assert(PGN_DATA & J1939_PGN_DP, "PGN_DATA must have the DP bit set");
+_Static_assert(!J1939_IS_PDU1(PGN_DATA) && PGN_DATA <= J1939_PGN_MAX,
+ "PGN_DATA must be a valid PDU2 PGN");
+
+/*
+ * Priority for the TP and ETP frames this test injects as the CAN_RAW
+ * peer. SAE J1939-21:2001 lists 7 as the TP.CM/TP.DT default and
+ * ISO 11783-6:2004 Annex K gives 6 for ETP. The stack checks neither, so
+ * one value serves both.
+ */
+#define PRIO 7
+
+/*
+ * PGNs, control bytes and field byte offsets (_OFF) of TP (SAE J1939-21:2001
+ * sections 5.10.3 and 5.10.4) and ETP (ISO 11783-6:2004 K.2.2.1 and K.2.3).
+ */
+
+/* PGNs */
+#define TP_CM 0x0ec00
+#define TP_DT 0x0eb00
+#define ETP_CM 0x0c800
+#define ETP_DT 0x0c700
+
+/* control bytes */
+#define TP_CMD_RTS 0x10
+#define TP_CMD_CTS 0x11
+#define TP_CMD_EOMA 0x13
+
+#define ETP_CMD_RTS 0x14
+#define ETP_CMD_CTS 0x15
+#define ETP_CMD_DPO 0x16
+#define ETP_CMD_EOMA 0x17
+
+/* field offsets */
+#define TP_RTS_SIZE_OFF 1 /* le16 */
+#define TP_RTS_PKTS_OFF 3
+#define TP_RTS_MAX_PKTS_OFF 4
+/* TP.CM_RTS max packets per CTS: no limit (SAE J1939-21:2001 section 5.10.3) */
+#define TP_RTS_NO_LIMIT 0xff
+#define TP_CTS_PKTS_OFF 1
+#define TP_CTS_NEXT_OFF 2
+#define TP_EOMA_SIZE_OFF 1 /* le16 */
+#define TP_EOMA_PKTS_OFF 3
+
+#define ETP_RTS_SIZE_OFF 1 /* le32 */
+#define ETP_CTS_PKTS_OFF 1
+#define ETP_CTS_NEXT_OFF 2 /* le24 */
+#define ETP_DPO_PKTS_OFF 1
+#define ETP_DPO_BASE_OFF 2 /* le24, packets before this block */
+#define ETP_EOMA_SIZE_OFF 1 /* le32 */
+
+/*
+ * largest ETP.CTS packet count: the field is one byte. It is also the
+ * kernel's ETP block (pkt.block, applied in j1939_session_tx_cts()).
+ */
+#define ETP_CTS_MAX_PKTS UINT8_MAX
+/* TP and ETP packet numbers start at 1 */
+#define TP_FIRST_PKT 1
+/* TP counts DT sequence numbers from the start of the message */
+#define TP_BASE_PKT 0
+
+/*
+ * arbitrary message sizes, within the asserts below: TP of several
+ * packets and above 255 bytes, ETP of several ETP.CTS blocks
+ */
+#define TP_SIZE 300
+_Static_assert(TP_SIZE > J1939_SF_MAX_SIZE, "TP_SIZE must need TP");
+_Static_assert(TP_SIZE <= J1939_TP_MAX_SIZE, "TP_SIZE must fit TP");
+_Static_assert(TP_SIZE > UINT8_MAX,
+ "TP_SIZE must use the high byte of the TP.CM size field");
+_Static_assert(TP_SIZE % J1939_DT_DATA_LEN != 0,
+ "the last TP DT frame must be partial, so its fill is checked");
+
+#define ETP_SIZE 6000U
+_Static_assert(ETP_SIZE > J1939_TP_MAX_SIZE, "ETP_SIZE must need ETP");
+_Static_assert(J1939_NUM_PKTS(ETP_SIZE) > ETP_CTS_MAX_PKTS,
+ "ETP_SIZE must span several ETP.CTS blocks");
+_Static_assert(ETP_SIZE % J1939_DT_DATA_LEN != 0,
+ "the last ETP DT frame must be partial, so its fill is checked");
+_Static_assert(J1939_NUM_PKTS(ETP_SIZE) % ETP_CTS_MAX_PKTS != 0,
+ "the last ETP block must be partial");
+
+/* an RTS per-CTS limit as large as the message: all packets in one CTS */
+#define TP_CTS_ALL J1939_NUM_PKTS(TP_SIZE)
+_Static_assert(TP_CTS_ALL <= UINT8_MAX, "the TP.CM packet counts are one byte");
+
+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 bool send_cm_frame_ok(int raw, uint8_t prio, pgn_t cm_pgn, uint8_t da,
+ uint8_t sa, uint8_t *dat, pgn_t data_pgn)
+{
+ put_pgn(dat, data_pgn);
+ return send_raw8_ok(raw, prio, cm_pgn, da, sa, dat);
+}
+
+/* send_cm_frame_ok() from the peer to the kernel, for PGN_DATA. */
+static bool peer_send_cm_ok(int raw, pgn_t cm_pgn, uint8_t *dat)
+{
+ return send_cm_frame_ok(raw, PRIO, cm_pgn, SA_LOCAL, SA_PEER, dat,
+ PGN_DATA);
+}
+
+/*
+ * Receive the next control frame on cm_pgn from the kernel to the peer and
+ * check that it is a full frame with the control byte cmd and PGN_DATA.
+ */
+static bool recv_kernel_cm_ok(int raw, pgn_t cm_pgn, uint8_t cmd,
+ struct can_frame *frame, int timeout_ms)
+{
+ if (!recv_cm_cmd_ok(raw, J1939_PF(cm_pgn), SA_PEER, SA_LOCAL, frame,
+ timeout_ms, cmd))
+ return false;
+
+ return frame->len == CAN_MAX_DLEN &&
+ get_le24(&frame->data[J1939_CM_PGN_OFF]) == PGN_DATA;
+}
+
+/*
+ * Build and send a TP.CM_RTS: message size, total packets and max packets/CTS.
+ */
+static bool send_tp_rts_ok(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[CAN_MAX_DLEN];
+
+ cm_dat_init(dat, TP_CMD_RTS);
+ put_le16(&dat[TP_RTS_SIZE_OFF], size);
+ dat[TP_RTS_PKTS_OFF] = total_pkts;
+ dat[TP_RTS_MAX_PKTS_OFF] = max_cts_pkts;
+ return send_cm_frame_ok(raw, prio, TP_CM, da, sa, dat, pgn);
+}
+
+/*
+ * Send a burst of @count DT frames for the block starting at the 1-based packet
+ * @next, each carrying up to J1939_DT_DATA_LEN payload bytes from the matching
+ * absolute offset. The first frame has sequence number @first_seq: TP numbers
+ * packets across the whole message (@next), ETP starts again at TP_FIRST_PKT
+ * in each DPO block.
+ */
+static bool send_dt_burst_ok(int raw, uint8_t prio, pgn_t dt_pgn, uint8_t da,
+ uint8_t sa, unsigned int next, unsigned int count,
+ const uint8_t *payload, size_t size,
+ unsigned int first_seq)
+{
+ unsigned int i;
+
+ for (i = 0; i < count; i++) {
+ size_t abs_pkt = (next - 1) + i;
+ uint8_t seq = first_seq + i;
+
+ if (!send_dt_ok(raw, prio, dt_pgn, da, sa, seq, payload,
+ abs_pkt * J1939_DT_DATA_LEN, size))
+ return false;
+ }
+ return true;
+}
+
+/* send_dt_burst_ok() for TP, which numbers packets across the message */
+static bool send_tp_dt_burst_ok(int raw, uint8_t da, uint8_t sa,
+ unsigned int next, unsigned int count,
+ const uint8_t *payload, size_t size)
+{
+ return send_dt_burst_ok(raw, PRIO, TP_DT, da, sa, next, count, payload,
+ size, next);
+}
+
+/*
+ * Check a CTS or ETP.CTS from the kernel that asks for cts_pkts packets from
+ * the 1-based packet next. The peer has sent `sent` of num_pkts packets, so
+ * the kernel must ask for packet sent + 1 and for
+ * min(cts_limit, num_pkts - sent) packets, see j1939_session_tx_cts(). No
+ * frame is lost here, so no resend is expected
+ * (SAE J1939-21:2001 section 5.10.3.2 allows one for TP).
+ */
+static bool cts_accept_ok(unsigned int cts_pkts, unsigned int next,
+ unsigned int sent, unsigned int num_pkts,
+ unsigned int cts_limit)
+{
+ unsigned int want = num_pkts - sent;
+
+ if (want > cts_limit)
+ want = cts_limit;
+ if (cts_pkts == want && next == sent + 1)
+ return true;
+
+ fprintf(stderr, "# CTS for %u packets from %u, want %u from %u\n",
+ cts_pkts, next, want, sent + 1);
+ return false;
+}
+
+/*
+ * Answer each CTS from the kernel with a DT burst from the peer, until
+ * num_pkts are delivered. Each CTS must ask for cts_limit packets, or for the
+ * rest, see cts_accept_ok().
+ */
+static bool tp_serve_cts_ok(int raw, const uint8_t *payload, size_t size,
+ unsigned int num_pkts, unsigned int cts_limit)
+{
+ unsigned int sent = 0;
+ struct can_frame frame;
+
+ while (sent < num_pkts) {
+ unsigned int cts_pkts, next;
+
+ if (!recv_kernel_cm_ok(raw, TP_CM, TP_CMD_CTS, &frame,
+ J1939_RECV_TIMEOUT_MS))
+ return false;
+
+ cts_pkts = frame.data[TP_CTS_PKTS_OFF];
+ next = frame.data[TP_CTS_NEXT_OFF]; /* 1-based */
+ if (!cts_accept_ok(cts_pkts, next, sent, num_pkts, cts_limit))
+ return false;
+
+ if (!send_tp_dt_burst_ok(raw, SA_LOCAL, SA_PEER, next, cts_pkts,
+ payload, size))
+ return false;
+
+ sent += cts_pkts;
+ }
+ return true;
+}
+
+/*
+ * Answer each ETP.CTS from the kernel with a DPO and the block's DT frames,
+ * until all size bytes of payload are delivered. Each ETP.CTS must ask
+ * for a full block of ETP_CTS_MAX_PKTS packets, or for the rest.
+ */
+static bool etp_rx_serve_cts_ok(int raw, const uint8_t *payload, size_t size)
+{
+ unsigned int num_pkts = J1939_NUM_PKTS(size), sent = 0;
+ uint8_t dat[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ while (sent < num_pkts) {
+ unsigned int cts_pkts, next;
+
+ if (!recv_kernel_cm_ok(raw, ETP_CM, ETP_CMD_CTS, &frame,
+ J1939_RECV_TIMEOUT_MS)) {
+ fprintf(stderr, "# no well-formed ETP CTS after %u/%u packets\n",
+ sent, num_pkts);
+ return false;
+ }
+
+ cts_pkts = frame.data[ETP_CTS_PKTS_OFF];
+ next = get_le24(&frame.data[ETP_CTS_NEXT_OFF]); /* 1-based */
+ if (!cts_accept_ok(cts_pkts, next, sent, num_pkts,
+ ETP_CTS_MAX_PKTS))
+ return false;
+
+ cm_dat_init(dat, ETP_CMD_DPO);
+ dat[ETP_DPO_PKTS_OFF] = cts_pkts;
+ put_le24(&dat[ETP_DPO_BASE_OFF], next - 1);
+ if (!peer_send_cm_ok(raw, ETP_CM, dat))
+ return false;
+
+ if (!send_dt_burst_ok(raw, PRIO, ETP_DT, SA_LOCAL, SA_PEER,
+ next, cts_pkts, payload, size,
+ TP_FIRST_PKT))
+ return false;
+
+ sent += cts_pkts;
+ }
+ return true;
+}
+
+/* Message offset of a DT frame's payload; base_pkt as for unpack_dt_into() */
+static size_t dt_off(const struct can_frame *frame, unsigned int base_pkt)
+{
+ return (base_pkt + (frame->data[J1939_DT_SEQ_OFF] - 1)) *
+ J1939_DT_DATA_LEN;
+}
+
+/*
+ * Scatter a received DT frame's payload into buf, a message of size bytes,
+ * at the offset implied by its sequence number. base_pkt is the number of
+ * packets before this block (TP_BASE_PKT for TP, the DPO offset for ETP);
+ * seq counts from 1 inside the block.
+ */
+static void unpack_dt_into(uint8_t *buf, size_t size, unsigned int base_pkt,
+ const struct can_frame *frame)
+{
+ size_t off = dt_off(frame, base_pkt);
+
+ /* drop a frame with a bad sequence number instead of overrunning buf */
+ if (off >= size)
+ return;
+
+ memcpy(buf + off, &frame->data[J1939_DT_DATA_OFF],
+ j1939_dt_chunk_len(size, off));
+}
+
+/* Are the bytes after the payload in a DT frame all J1939_FILL_BYTE? */
+static bool dt_fill_ok(const struct can_frame *frame, size_t size,
+ unsigned int base_pkt)
+{
+ size_t off = dt_off(frame, base_pkt);
+ size_t i;
+
+ if (off >= size)
+ return true; /* a bad sequence number fails elsewhere */
+ for (i = J1939_DT_DATA_OFF + j1939_dt_chunk_len(size, off);
+ i < CAN_MAX_DLEN; i++)
+ if (frame->data[i] != J1939_FILL_BYTE)
+ return false;
+ return true;
+}
+
+/*
+ * Receive count DT frames on dt_pgn from the kernel and scatter them into
+ * buf, a message of size bytes; base_pkt as for unpack_dt_into(). Each frame
+ * must have CAN_MAX_DLEN bytes (the kernel pads the last one with
+ * J1939_FILL_BYTE, see j1939_tp_tx_dat(), and dt_fill_ok() checks it) and the
+ * sequence number TP_FIRST_PKT + i. That holds for an ETP block and for a TP
+ * CTS window that starts at the first packet.
+ */
+static bool recv_dt_block_ok(int raw, pgn_t dt_pgn, uint8_t *buf, size_t size,
+ unsigned int base_pkt, unsigned int count)
+{
+ struct can_frame frame;
+ unsigned int i;
+
+ for (i = 0; i < count; i++) {
+ if (!recv_for_peer_ok(raw, J1939_PF(dt_pgn), SA_PEER, SA_LOCAL,
+ &frame, J1939_RECV_TIMEOUT_MS)) {
+ fprintf(stderr, "# missing DT %u of %u\n",
+ i + TP_FIRST_PKT, count);
+ return false;
+ }
+ if (frame.len != CAN_MAX_DLEN ||
+ frame.data[J1939_DT_SEQ_OFF] != TP_FIRST_PKT + i) {
+ fprintf(stderr, "# DT %u of %u: len %u, seq %u\n",
+ i + TP_FIRST_PKT, count, frame.len,
+ frame.data[J1939_DT_SEQ_OFF]);
+ return false;
+ }
+ if (!dt_fill_ok(&frame, size, base_pkt)) {
+ fprintf(stderr, "# DT %u of %u: bad fill bytes\n",
+ i + TP_FIRST_PKT, count);
+ return false;
+ }
+ unpack_dt_into(buf, size, base_pkt, &frame);
+ }
+ return true;
+}
+
+/*
+ * Open a TP RX j1939 socket and send the RTS for a TP_SIZE message that
+ * starts a receive session.
+ */
+static bool tp_rx_open_and_send_rts_ok(int *j1939, int raw, int ifindex,
+ uint8_t cts_limit)
+{
+ *j1939 = j1939_open(ifindex, SA_LOCAL, PGN_DATA);
+ if (*j1939 < 0)
+ return false;
+
+ return send_tp_rts_ok(raw, PRIO, PGN_DATA, SA_LOCAL, SA_PEER, TP_SIZE,
+ J1939_NUM_PKTS(TP_SIZE), cts_limit);
+}
+
+/*
+ * TP-RX round-trip: open the j1939 socket, drive a full TP receive from the
+ * raw peer advertising cts_limit packets per CTS, and verify the reassembled
+ * payload against the sent pattern. Each CTS must ask for cts_limit packets,
+ * or for the rest, see cts_accept_ok(). The kernel's EOMA frame is returned
+ * via *eoma_out.
+ */
+static bool tp_rx_roundtrip_ok(FIXTURE_DATA(j1939_multi) *self,
+ uint8_t cts_limit,
+ struct can_frame *eoma_out)
+{
+ uint8_t payload[TP_SIZE];
+
+ fill_pattern(payload, sizeof(payload));
+
+ if (!tp_rx_open_and_send_rts_ok(&self->j1939, self->raw, self->ifindex,
+ cts_limit))
+ return false;
+
+ if (!tp_serve_cts_ok(self->raw, payload, TP_SIZE,
+ J1939_NUM_PKTS(TP_SIZE), cts_limit))
+ return false;
+
+ if (!recv_kernel_cm_ok(self->raw, TP_CM, TP_CMD_EOMA, eoma_out,
+ J1939_RECV_TIMEOUT_MS))
+ return false;
+
+ return recv_verify_ok(self->j1939, payload, sizeof(payload),
+ J1939_RECV_TIMEOUT_MS);
+}
+
+/*
+ * 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;
+
+ /* the RTS allows all packets in one CTS */
+ ASSERT_TRUE(tp_rx_roundtrip_ok(self, TP_CTS_ALL, &frame))
+ TH_LOG("TP RX round-trip against the raw peer failed");
+
+ /* frame is the kernel's EOMA */
+ EXPECT_EQ(get_le16(&frame.data[TP_EOMA_SIZE_OFF]), TP_SIZE);
+ EXPECT_EQ(frame.data[TP_EOMA_PKTS_OFF], num_pkts);
+}
+
+/*
+ * Start a TP/ETP transmit session from the J1939 socket: open a socket bound
+ * to SA_LOCAL with no PGN, connect to the peer, send the size bytes of
+ * payload and collect the kernel's RTS from the raw peer (cm_pgn/rts_cmd
+ * select TP or ETP). *j1939 holds the fd before connect() or send() can
+ * fail, so the fixture teardown closes it.
+ */
+static bool tx_start_session_ok(int *j1939, int raw, int ifindex,
+ const void *payload, size_t size,
+ pgn_t cm_pgn, uint8_t rts_cmd,
+ struct can_frame *rts)
+{
+ *j1939 = j1939_open(ifindex, SA_LOCAL, J1939_NO_PGN);
+ if (*j1939 < 0)
+ return false;
+
+ if (!j1939_connect_ok(*j1939, ifindex, SA_PEER, PGN_DATA))
+ return false;
+
+ if (!send_all_ok(*j1939, payload, size))
+ return false;
+
+ return recv_kernel_cm_ok(raw, cm_pgn, rts_cmd, rts,
+ J1939_RECV_TIMEOUT_MS);
+}
+
+static bool tp_tx_start_session_ok(FIXTURE_DATA(j1939_multi) *self,
+ const void *payload, size_t size,
+ struct can_frame *rts)
+{
+ return tx_start_session_ok(&self->j1939, self->raw, self->ifindex,
+ payload, size, TP_CM, TP_CMD_RTS, rts);
+}
+
+static bool etp_tx_start_session_ok(FIXTURE_DATA(j1939_multi) *self,
+ const void *payload, size_t size,
+ struct can_frame *rts)
+{
+ return tx_start_session_ok(&self->j1939, self->raw, self->ifindex,
+ payload, size, ETP_CM, ETP_CMD_RTS, rts);
+}
+
+/*
+ * 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, which completes the session. The socket then
+ * gets the TX-completion ACK.
+ */
+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[CAN_MAX_DLEN];
+
+ fill_pattern(payload, sizeof(payload));
+ memset(rx_buf, 0, sizeof(rx_buf));
+
+ ASSERT_TRUE(tp_tx_start_session_ok(self, payload, sizeof(payload),
+ &frame));
+ ASSERT_TRUE(errqueue_enable_ack_ok(self->j1939));
+
+ EXPECT_EQ(get_le16(&frame.data[TP_RTS_SIZE_OFF]), TP_SIZE);
+ EXPECT_EQ(frame.data[TP_RTS_PKTS_OFF], num_pkts);
+ EXPECT_TRUE(frame.data[TP_RTS_MAX_PKTS_OFF] == num_pkts ||
+ frame.data[TP_RTS_MAX_PKTS_OFF] == TP_RTS_NO_LIMIT);
+
+ /* request all packets in one CTS, starting at the first */
+ cm_dat_init(dat, TP_CMD_CTS);
+ dat[TP_CTS_PKTS_OFF] = num_pkts;
+ dat[TP_CTS_NEXT_OFF] = TP_FIRST_PKT;
+ ASSERT_TRUE(peer_send_cm_ok(self->raw, TP_CM, dat));
+
+ ASSERT_TRUE(recv_dt_block_ok(self->raw, TP_DT, rx_buf, TP_SIZE,
+ TP_BASE_PKT, num_pkts));
+ EXPECT_TRUE(errqueue_empty_ok(self->j1939))
+ TH_LOG("error-queue entry before the EOMA");
+
+ cm_dat_init(dat, TP_CMD_EOMA);
+ put_le16(&dat[TP_EOMA_SIZE_OFF], TP_SIZE);
+ dat[TP_EOMA_PKTS_OFF] = num_pkts;
+ ASSERT_TRUE(peer_send_cm_ok(self->raw, TP_CM, dat));
+
+ EXPECT_EQ(memcmp(payload, rx_buf, sizeof(payload)), 0);
+ EXPECT_TRUE(errqueue_read_ack_ok(self->j1939))
+ TH_LOG("no TX-completion ACK after the EOMA");
+}
+
+/*
+ * 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 ETP.CTS blocks.
+ */
+TEST_F(j1939_multi, etp_rx_from_raw)
+{
+ static uint8_t payload[ETP_SIZE];
+ uint8_t dat[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ fill_pattern(payload, ETP_SIZE);
+
+ self->j1939 = j1939_open(self->ifindex, SA_LOCAL, PGN_DATA);
+ ASSERT_GE(self->j1939, 0);
+
+ cm_dat_init(dat, ETP_CMD_RTS);
+ put_le32(&dat[ETP_RTS_SIZE_OFF], ETP_SIZE);
+ ASSERT_TRUE(peer_send_cm_ok(self->raw, ETP_CM, dat));
+
+ ASSERT_TRUE(etp_rx_serve_cts_ok(self->raw, payload, sizeof(payload)));
+
+ ASSERT_TRUE(recv_kernel_cm_ok(self->raw, ETP_CM, ETP_CMD_EOMA, &frame,
+ J1939_RECV_TIMEOUT_MS))
+ TH_LOG("no well-formed ETP EOMA from kernel after last DT");
+ EXPECT_EQ(get_le32(&frame.data[ETP_EOMA_SIZE_OFF]), ETP_SIZE);
+
+ EXPECT_TRUE(recv_verify_ok(self->j1939, payload, ETP_SIZE,
+ J1939_RECV_TIMEOUT_MS))
+ TH_LOG("J1939 socket did not get the exact ETP message");
+}
+
+/*
+ * Ask the kernel for the next ETP block of cts_pkts packets, after got
+ * packets are already received. Check its DPO and receive the block into
+ * rx_buf, a message of size bytes.
+ */
+static bool etp_tx_serve_block_ok(FIXTURE_DATA(j1939_multi) *self,
+ uint8_t *rx_buf, size_t size,
+ unsigned int got, unsigned int cts_pkts)
+{
+ unsigned int next = got + 1; /* 1-based */
+ unsigned int dpo_num, dpo_base_pkt;
+ uint8_t dat[CAN_MAX_DLEN];
+ struct can_frame frame;
+
+ cm_dat_init(dat, ETP_CMD_CTS);
+ dat[ETP_CTS_PKTS_OFF] = cts_pkts;
+ put_le24(&dat[ETP_CTS_NEXT_OFF], next);
+ if (!peer_send_cm_ok(self->raw, ETP_CM, dat))
+ return false;
+
+ if (!recv_kernel_cm_ok(self->raw, ETP_CM, ETP_CMD_DPO, &frame,
+ J1939_RECV_TIMEOUT_MS)) {
+ fprintf(stderr, "# no well-formed DPO after %u packets\n", got);
+ return false;
+ }
+
+ dpo_num = frame.data[ETP_DPO_PKTS_OFF];
+ dpo_base_pkt = get_le24(&frame.data[ETP_DPO_BASE_OFF]);
+ if (dpo_num != cts_pkts || dpo_base_pkt != got) {
+ fprintf(stderr,
+ "# DPO for %u packets after %u, want %u after %u\n",
+ dpo_num, dpo_base_pkt, cts_pkts, got);
+ return false;
+ }
+
+ return recv_dt_block_ok(self->raw, ETP_DT, rx_buf, size, dpo_base_pkt,
+ dpo_num);
+}
+
+/*
+ * ETP TX: the J1939 socket transmits an ETP message to the CAN_RAW peer.
+ * raw requests the data in blocks of up to ETP_CTS_MAX_PKTS packets
+ * (ETP.CTS). For each block the kernel sends a DPO and the DT frames. The
+ * EOMA from raw completes the session, and the socket then gets the
+ * TX-completion ACK.
+ */
+TEST_F(j1939_multi, etp_tx_to_raw)
+{
+ unsigned int num_pkts = J1939_NUM_PKTS(ETP_SIZE);
+ static uint8_t payload[ETP_SIZE], rx_buf[ETP_SIZE];
+ uint8_t dat[CAN_MAX_DLEN];
+ struct can_frame frame;
+ unsigned int got = 0;
+
+ fill_pattern(payload, ETP_SIZE);
+ memset(rx_buf, 0, sizeof(rx_buf));
+
+ ASSERT_TRUE(etp_tx_start_session_ok(self, payload, ETP_SIZE, &frame));
+ ASSERT_TRUE(errqueue_enable_ack_ok(self->j1939));
+
+ EXPECT_EQ(get_le32(&frame.data[ETP_RTS_SIZE_OFF]), ETP_SIZE);
+
+ while (got < num_pkts) {
+ unsigned int cts_pkts = num_pkts - got;
+
+ if (cts_pkts > ETP_CTS_MAX_PKTS)
+ cts_pkts = ETP_CTS_MAX_PKTS;
+
+ ASSERT_TRUE(etp_tx_serve_block_ok(self, rx_buf, sizeof(rx_buf),
+ got, cts_pkts))
+ TH_LOG("ETP block after %u/%u packets failed", got,
+ num_pkts);
+ got += cts_pkts;
+ }
+ EXPECT_TRUE(errqueue_empty_ok(self->j1939))
+ TH_LOG("error-queue entry before the EOMA");
+
+ cm_dat_init(dat, ETP_CMD_EOMA);
+ put_le32(&dat[ETP_EOMA_SIZE_OFF], ETP_SIZE);
+ ASSERT_TRUE(peer_send_cm_ok(self->raw, ETP_CM, dat));
+
+ EXPECT_EQ(memcmp(payload, rx_buf, sizeof(payload)), 0);
+ EXPECT_TRUE(errqueue_read_ack_ok(self->j1939))
+ TH_LOG("no TX-completion ACK after the EOMA");
+}
+
+int main(int argc, char **argv)
+{
+ return j1939_test_main(argc, argv);
+}
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 0000000000000000000000000000000000000000..61f65365ec568f928b58bc4f892f97e9ec887027
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_multi.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+source "$(dirname "$0")/j1939_lib.sh"
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH can-next v3 7/9] selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
2026-11-11 15:25 ` [PATCH can-next v3 7/9] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
@ 2026-11-13 6:02 ` netdev-bot+sashiko
0 siblings, 0 replies; 14+ messages in thread
From: netdev-bot+sashiko @ 2026-11-13 6:02 UTC (permalink / raw)
To: o.rempel
Cc: robin, socketcan, mkl, shuah, kernel, linux-can, linux-kernel,
linux-kselftest, fmaurer, mailhol, alexander.hoelzl,
quchaosheng000406, penguin-kernel, kuba
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 0
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The patch adds four TP/ETP tests (tp_rx_from_raw,
tp_tx_to_raw, etp_rx_from_raw, etp_tx_to_raw) that the author says can…
--
selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
This adds TP and ETP round-trip selftests in both directions. A CAN_RAW
socket acts as the transport peer, so the frames are checked as they
appear on the wire. The TX tests also check that the TX-completion ACK
reaches the error queue only after the peer's EOMA.
> diff --git a/tools/testing/selftests/net/can/Makefile b/tools/testing/selftests/net/can/Makefile
> index 21898e645ced7..99113184a525a 100644
> --- a/tools/testing/selftests/net/can/Makefile
> +++ b/tools/testing/selftests/net/can/Makefile
> @@ -9,6 +9,7 @@ LOCAL_HDRS := j1939_helpers.h j1939_tests.h ../../clock-helpers.h
> TEST_PROGS := \
> test_j1939_addr.sh \
> test_j1939_base.sh \
> + test_j1939_multi.sh \
> test_j1939_opts.sh \
> test_j1939_wire.sh \
> test_raw_filter.sh \
[ ... ]
> 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 0000000000000..aec96e83305cf
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/test_j1939_multi.c
> @@ -0,0 +1,682 @@
> +// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
> +/*
> + * J1939 multi-frame wire selftests: TP and ETP against CAN_RAW.
> + *
[ ... ]
> + * Known stack race: the stack updates a session from the loopback echo of
> + * its own CTS, RTS and DT frames, and the CAN_RAW peer can read that echo
> + * before the stack has handled it. A peer frame that follows quickly can
> + * then cancel the session, so these tests can fail at random. The fix
> + * belongs in the stack; this file does not work around it.
[Severity: Medium]
test_j1939_multi.sh goes straight into TEST_PROGS. Will a default
kselftest run then see random failures from this race that cannot be told
apart from real regressions?
The raw peer always gets the echo first. raw_init() sets a zero can_mask,
which puts the raw socket on rx[RX_ALL]. j1939 registers on rx[RX_FIL].
can_rcv_filter() walks the two lists in that order:
net/can/af_can.c:can_rcv_filter() {
...
/* check for unfiltered entries */
hlist_for_each_entry_rcu(rcv, &dev_rcv_lists->rx[RX_ALL], list) {
deliver(skb, rcv);
...
/* check for can_id/mask entries */
hlist_for_each_entry_rcu(rcv, &dev_rcv_lists->rx[RX_FIL], list) {
...
}
In the RX tests, tp_serve_cts_ok() and etp_rx_serve_cts_ok() send the DPO
and DT frames as soon as they read the kernel's CTS. If the stack has not
handled the echo of its own CTS yet, last_cmd still holds the RTS. The
peer's DT then lands in the default case here:
net/can/j1939/transport.c:j1939_xtp_rx_dat_one() {
...
switch (session->last_cmd) {
case 0xff:
break;
case J1939_ETP_CMD_DPO:
...
default:
netdev_info(priv->ndev, "%s: 0x%p: last %02x\n", __func__,
session, session->last_cmd);
goto out_session_cancel;
}
...
}
In the TX tests, tp_tx_to_raw() and etp_tx_serve_block_ok() send a CTS
right after they read the kernel's RTS or the last DT of a block. If the
peer's CTS is handled before the RTS echo, last_cmd becomes CTS. The late
echo then cancels the session:
net/can/j1939/transport.c:j1939_xtp_rx_rts_session_active() {
...
if (session->last_cmd != 0) {
/* we received a second rts on the same connection */
...
j1939_session_timers_cancel(session);
j1939_session_cancel(session, J1939_XTP_ABORT_BUSY);
...
}
A late echo of the last DT of a block can also arrive after the peer's
next ETP.CTS. That echo hits the same default case in
j1939_xtp_rx_dat_one().
This file comment is the only note about the race in the tree. The Q/A
under the --- line gets dropped when the patch is applied, and the commit
message does not mention the race. At the end of the series ("selftests:
can: check SO_J1939_PROMISC receive effect") the comment is the same and
test_j1939_multi.sh is still in TEST_PROGS with nothing to work around
the race.
Could these tests stay out of the default run, be marked as expected
failures, or wait for the stack to handle each echo before the peer
replies, until the stack is fixed? Could the commit message also mention
the race?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261111-j1939-selftests-v3-0-5a80277ab363%40pengutronix.de
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH can-next v3 8/9] selftests: can: cover J1939 transport abort and timeout paths
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (6 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 7/9] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
2026-11-11 15:25 ` [PATCH can-next v3 9/9] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
8 siblings, 0 replies; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add transport edge cases to the multi-frame tests. The round-trip tests
only run transfers that succeed, and no TP transfer there is split into
CTS blocks. They leave out these cases:
- a CTS that asks for fewer packets than the message has
- the T3 timeout, when no CTS follows the RTS
- the T2 timeout, when no DT follows the CTS
- an Abort from the peer
The timeouts must end in a Connection Abort. The Abort from the peer must
cancel an incoming session.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q (sashiko): does tp_rx_cts_block_limit run into the echo race of
patch 7?
A: Yes, and more often: it waits for more CTS frames. The answer in
patch 7 applies.
Q: why is there no test for a Connection Abort received on a kernel TX
session?
A: The stack ignores it today and ends the session only on the T3
timeout, so such a test would fail. This series changes no kernel code.
Changes in v3:
- say in the body which cases the round-trip tests leave out
- assert that the last TP CTS window is partial
- check that the first CTS of the abort and T2 tests asks for all packets,
and check the EOMA of tp_rx_cts_block_limit
- reword the TP_CTS_LIMIT and T2 comments
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants, add static asserts, avoid casts and return bool
from helpers named *_ok()
- time T2 and T3 against the bounds of the timing mode; strict mode
catches a doubled timer, and the header no longer claims that the
default mode does (sashiko)
- tp_tx_rts_cts_timeout: check that the sender sees EHOSTUNREACH
- tp_rx_cts_block_limit: check the exact packet count in each CTS, not
only an upper bound
- time T2 and T3 from before the test opens its socket, so a delay in
the test cannot make an abort look early (sashiko)
---
tools/testing/selftests/net/can/test_j1939_multi.c | 270 ++++++++++++++++++++-
1 file changed, 269 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 aec96e83305cf2d9129aed3ccdf4035437337ba7..b35c5477afddf25ae650d2c9bfd32d4375b52809 100644
--- a/tools/testing/selftests/net/can/test_j1939_multi.c
+++ b/tools/testing/selftests/net/can/test_j1939_multi.c
@@ -5,7 +5,8 @@
* A CAN_RAW socket plays the transport peer against the kernel J1939 stack,
* in both directions. It takes the other side of the RTS/CTS/DT/EOMA
* exchange, for TP with a short message and for ETP with a payload that
- * spans several CTS blocks (DPO windows).
+ * spans several CTS blocks (DPO windows). The tests also cover the TP CTS
+ * block limit and the TP abort and timeout paths.
*
* Known stack race: the stack updates a session from the loopback echo of
* its own CTS, RTS and DT frames, and the CAN_RAW peer can read that echo
@@ -59,6 +60,7 @@ _Static_assert(!J1939_IS_PDU1(PGN_DATA) && PGN_DATA <= J1939_PGN_MAX,
#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
@@ -75,6 +77,7 @@ _Static_assert(!J1939_IS_PDU1(PGN_DATA) && PGN_DATA <= J1939_PGN_MAX,
#define TP_CTS_NEXT_OFF 2
#define TP_EOMA_SIZE_OFF 1 /* le16 */
#define TP_EOMA_PKTS_OFF 3
+#define TP_ABORT_REASON_OFF 1
#define ETP_RTS_SIZE_OFF 1 /* le32 */
#define ETP_CTS_PKTS_OFF 1
@@ -118,6 +121,67 @@ _Static_assert(J1939_NUM_PKTS(ETP_SIZE) % ETP_CTS_MAX_PKTS != 0,
#define TP_CTS_ALL J1939_NUM_PKTS(TP_SIZE)
_Static_assert(TP_CTS_ALL <= UINT8_MAX, "the TP.CM packet counts are one byte");
+/*
+ * arbitrary limit on packets per CTS, sent in the RTS, below the packet
+ * count of TP_SIZE
+ */
+#define TP_CTS_LIMIT 3U
+_Static_assert(J1939_NUM_PKTS(TP_SIZE) > TP_CTS_LIMIT,
+ "TP_SIZE must need more packets than TP_CTS_LIMIT");
+_Static_assert(J1939_NUM_PKTS(TP_SIZE) % TP_CTS_LIMIT != 0,
+ "the last TP CTS window must be partial");
+
+/*
+ * Connection Abort reasons (SAE J1939-21:2001 Table 6); the kernel names them
+ * J1939_XTP_ABORT_*.
+ */
+#define ABORT_REASON_BUSY 1
+#define ABORT_REASON_TIMEOUT 3
+
+/*
+ * TP responder T2: wait for the next DT after CTS (SAE J1939-21:2001
+ * section 5.10.2.4 and Figure C1).
+ */
+#define TP_T2_MS 1250
+/*
+ * TP originator T3: wait for a CTS after RTS (SAE J1939-21:2001 section
+ * 5.10.2.4 and Figure C1).
+ */
+#define TP_T3_MS 1250
+
+/*
+ * Bounds for the time from before the test opens its socket to the kernel's
+ * abort.
+ * The values below are guesses, not spec values.
+ */
+
+/* relaxed (default, for slow VMs): catches a timer that fires early or never */
+#define TP_RELAXED_EARLY_MS 450
+#define TP_RELAXED_LATE_MS 1250
+#define TP_RELAXED_MIN_MS(t) ((t) - TP_RELAXED_EARLY_MS)
+#define TP_RELAXED_MAX_MS(t) ((t) + TP_RELAXED_LATE_MS)
+
+/* strict: also catches a doubled timer */
+#define TP_STRICT_EARLY_MS 100
+#define TP_STRICT_LATE_MS 200
+#define TP_STRICT_MIN_MS(t) ((t) - TP_STRICT_EARLY_MS)
+#define TP_STRICT_MAX_MS(t) ((t) + TP_STRICT_LATE_MS)
+
+/* for any timer: the relaxed bounds must contain the strict bounds */
+_Static_assert(TP_RELAXED_EARLY_MS >= TP_STRICT_EARLY_MS,
+ "the relaxed minimum must not be above the strict one");
+_Static_assert(TP_RELAXED_LATE_MS >= TP_STRICT_LATE_MS,
+ "the relaxed maximum must not be below the strict one");
+_Static_assert(TP_T2_MS > TP_RELAXED_EARLY_MS,
+ "TP_T2_MS must exceed TP_RELAXED_EARLY_MS");
+_Static_assert(TP_T3_MS > TP_RELAXED_EARLY_MS,
+ "TP_T3_MS must exceed TP_RELAXED_EARLY_MS");
+/* so that strict mode catches a doubled T2 or T3 */
+_Static_assert(TP_STRICT_LATE_MS < TP_T2_MS,
+ "TP_STRICT_LATE_MS must stay below TP_T2_MS");
+_Static_assert(TP_STRICT_LATE_MS < TP_T3_MS,
+ "TP_STRICT_LATE_MS must stay below TP_T3_MS");
+
FIXTURE(j1939_multi)
{
int ifindex;
@@ -558,6 +622,116 @@ TEST_F(j1939_multi, tp_tx_to_raw)
TH_LOG("no TX-completion ACK after the EOMA");
}
+/*
+ * TP RX, per-CTS block limit: the RTS asks for at most TP_CTS_LIMIT packets
+ * per CTS, fewer than the message has. Each CTS from the kernel must ask
+ * for TP_CTS_LIMIT packets, or for the rest.
+ */
+TEST_F(j1939_multi, tp_rx_cts_block_limit)
+{
+ struct can_frame frame;
+
+ ASSERT_TRUE(tp_rx_roundtrip_ok(self, TP_CTS_LIMIT, &frame))
+ TH_LOG("TP RX round-trip against the raw peer failed");
+
+ /* frame is the kernel's EOMA */
+ EXPECT_EQ(get_le16(&frame.data[TP_EOMA_SIZE_OFF]), TP_SIZE);
+ EXPECT_EQ(frame.data[TP_EOMA_PKTS_OFF], J1939_NUM_PKTS(TP_SIZE));
+}
+
+/* Set *min_ms and *max_ms to the abort bounds of a TP timer of timer_ms. */
+static void tp_abort_window(int timer_ms, int *min_ms, int *max_ms)
+{
+ *min_ms = j1939_timing_ms(TP_RELAXED_MIN_MS(timer_ms),
+ TP_STRICT_MIN_MS(timer_ms));
+ *max_ms = j1939_timing_ms(TP_RELAXED_MAX_MS(timer_ms),
+ TP_STRICT_MAX_MS(timer_ms));
+}
+
+/*
+ * Wait until max_ms after t0 for the kernel's Connection Abort to the peer
+ * and store it in *frame; *elapsed_ms gets the time from t0 to its arrival.
+ */
+static bool recv_abort_timed_ok(int raw, const struct timespec *t0,
+ int max_ms, struct can_frame *frame,
+ long *elapsed_ms)
+{
+ struct timespec now;
+ bool ok;
+
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ *elapsed_ms = timespec_diff_ms(t0, &now);
+ if (*elapsed_ms > max_ms)
+ return false;
+
+ ok = recv_kernel_cm_ok(raw, TP_CM, TP_CMD_ABORT, frame,
+ max_ms - *elapsed_ms);
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ *elapsed_ms = timespec_diff_ms(t0, &now);
+ return ok;
+}
+
+/*
+ * Wait for an error on s and return it (SO_ERROR), or 0 if none came within
+ * J1939_RECV_TIMEOUT_MS.
+ */
+static int sock_wait_error(int s)
+{
+ struct pollfd pfd = { .fd = s };
+ socklen_t len;
+ int err = 0;
+
+ if (poll(&pfd, 1, J1939_RECV_TIMEOUT_MS) <= 0 ||
+ !(pfd.revents & POLLERR))
+ return 0;
+
+ len = sizeof(err);
+ if (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &len))
+ return 0;
+ return err;
+}
+
+/*
+ * 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 after
+ * T3 (TP_T3_MS), send a Connection Abort and report the error to the sender.
+ *
+ * The kernel starts T3 when it sends the RTS, and starts it again when it
+ * receives its own RTS back from the CAN echo (j1939_xtp_rx_rts() in
+ * net/can/j1939/transport.c). The wait is timed from before the socket is
+ * opened, so a delay in this test cannot make the abort look early.
+ */
+TEST_F(j1939_multi, tp_tx_rts_cts_timeout)
+{
+ uint8_t payload[TP_SIZE];
+ struct can_frame frame;
+ struct timespec t0;
+ int min_ms, max_ms;
+ long elapsed_ms;
+
+ fill_pattern(payload, sizeof(payload));
+ tp_abort_window(TP_T3_MS, &min_ms, &max_ms);
+
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+ ASSERT_TRUE(tp_tx_start_session_ok(self, payload, sizeof(payload),
+ &frame));
+
+ ASSERT_TRUE(recv_abort_timed_ok(self->raw, &t0, max_ms, &frame,
+ &elapsed_ms))
+ TH_LOG("kernel did not send a Connection Abort within %d ms",
+ max_ms);
+
+ /* the T3 timer has to run before the abort */
+ ASSERT_GE(elapsed_ms, min_ms)
+ TH_LOG("abort after only %ld ms, expected at least %d ms",
+ elapsed_ms, min_ms);
+ EXPECT_EQ(frame.data[TP_ABORT_REASON_OFF], ABORT_REASON_TIMEOUT);
+
+ /* j1939_xtp_abort_to_errno() reports a timeout as EHOSTUNREACH */
+ EXPECT_EQ(sock_wait_error(self->j1939), EHOSTUNREACH)
+ TH_LOG("the sender did not see the aborted session");
+}
+
/*
* 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
@@ -676,6 +850,100 @@ TEST_F(j1939_multi, etp_tx_to_raw)
TH_LOG("no TX-completion ACK after the EOMA");
}
+/* Send a TP.CM_Abort control frame from the peer to the kernel. */
+static bool send_abort_ok(int raw, uint8_t reason)
+{
+ uint8_t dat[CAN_MAX_DLEN];
+
+ cm_dat_init(dat, TP_CMD_ABORT);
+ dat[TP_ABORT_REASON_OFF] = reason;
+ return peer_send_cm_ok(raw, TP_CM, dat);
+}
+
+/*
+ * Open a TP RX j1939 socket, send an RTS that allows all packets in one CTS
+ * and check that the first CTS asks for all of them.
+ */
+static bool tp_rx_start_session_ok(int *j1939, int raw, int ifindex,
+ struct can_frame *cts)
+{
+ if (!tp_rx_open_and_send_rts_ok(j1939, raw, ifindex, TP_CTS_ALL))
+ return false;
+
+ return recv_kernel_cm_ok(raw, TP_CM, TP_CMD_CTS, cts,
+ J1939_RECV_TIMEOUT_MS) &&
+ cts_accept_ok(cts->data[TP_CTS_PKTS_OFF],
+ cts->data[TP_CTS_NEXT_OFF], 0,
+ J1939_NUM_PKTS(TP_SIZE), TP_CTS_ALL);
+}
+
+/*
+ * TP RX timeout: the CAN_RAW peer opens a session with RTS but never sends the
+ * data; the kernel's T2 timer must abort the session and emit a TP.CM_Abort.
+ * The kernel starts its receive timer when it gets the RTS
+ * (j1939_xtp_rx_rts()). It starts it again as T2 when it sends the CTS
+ * (j1939_session_tx_cts()) and on the CAN echo of that CTS
+ * (j1939_xtp_rx_cts_one()). The wait is timed from before the socket is
+ * opened.
+ */
+TEST_F(j1939_multi, tp_rx_timeout)
+{
+ struct can_frame frame;
+ struct timespec t0;
+ int min_ms, max_ms;
+ long elapsed_ms;
+
+ tp_abort_window(TP_T2_MS, &min_ms, &max_ms);
+
+ clock_gettime(CLOCK_MONOTONIC, &t0);
+ ASSERT_TRUE(tp_rx_start_session_ok(&self->j1939, self->raw,
+ self->ifindex, &frame))
+ TH_LOG("failed to open and start the TP rx session");
+
+ ASSERT_TRUE(recv_abort_timed_ok(self->raw, &t0, max_ms, &frame,
+ &elapsed_ms))
+ TH_LOG("kernel did not abort the stalled rx session");
+
+ /* the T2 timer has to run before the abort */
+ ASSERT_GE(elapsed_ms, min_ms)
+ TH_LOG("abort after only %ld ms, expected at least %d ms",
+ elapsed_ms, min_ms);
+ EXPECT_EQ(frame.data[TP_ABORT_REASON_OFF], 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;
+
+ fill_pattern(payload, sizeof(payload));
+
+ ASSERT_TRUE(tp_rx_start_session_ok(&self->j1939, self->raw,
+ self->ifindex, &frame))
+ TH_LOG("failed to open and start the TP rx session");
+
+ ASSERT_TRUE(send_abort_ok(self->raw, ABORT_REASON_BUSY));
+
+ /*
+ * Send all DT frames anyway. A kernel that ignored the abort would
+ * deliver the message.
+ */
+ ASSERT_TRUE(send_tp_dt_burst_ok(self->raw, SA_LOCAL, SA_PEER,
+ TP_FIRST_PKT, num_pkts, payload,
+ TP_SIZE));
+
+ pfd.fd = self->j1939;
+ pfd.events = POLLIN;
+ EXPECT_EQ(poll(&pfd, 1, J1939_NEG_RECV_TIMEOUT_MS), 0)
+ TH_LOG("socket delivered data despite the abort");
+}
+
int main(int argc, char **argv)
{
return j1939_test_main(argc, argv);
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH can-next v3 9/9] selftests: can: check SO_J1939_PROMISC receive effect
2026-11-11 15:25 [PATCH can-next v3 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (7 preceding siblings ...)
2026-11-11 15:25 ` [PATCH can-next v3 8/9] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
@ 2026-11-11 15:25 ` Oleksij Rempel
8 siblings, 0 replies; 14+ messages in thread
From: Oleksij Rempel @ 2026-11-11 15:25 UTC (permalink / raw)
To: Robin van der Gracht, Oliver Hartkopp, Marc Kleine-Budde, Shuah Khan
Cc: kernel, linux-can, linux-kernel, linux-kselftest, Felix Maurer,
Vincent Mailhol, Alexander Hölzl, Quchaosheng, Tetsuo Handa,
Oleksij Rempel
Add a test that SO_J1939_PROMISC changes what the RX path delivers. The
existing coverage only round-trips the flag through set/getsockopt, so a
kernel that stores the flag but never acts on it still passes.
The test uses one probe per filter. One probe goes to a different
address from the bound one. The other has a different PGN. The test
sends the probes three times: before the flag is set, while the flag is
set, and after the flag is cleared. The probes must arrive only while
the flag is set. After the flag is cleared, the test also sends a frame
for the bound address and PGN. That frame must arrive.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q: why does j1939_set_flag_ok() accept a return value of 1 from
setsockopt(SO_J1939_PROMISC)?
A: See patch 4: the return value is a stack bug, and this series changes
no kernel code.
Changes in v3:
- check the PGN of each probe received after SO_J1939_PROMISC is set;
both carry the same payload (sashiko)
- set the flag with j1939_set_flag_ok(), and check that the probes are
filtered again after SO_J1939_PROMISC is cleared
- check that a matching frame still arrives after SO_J1939_PROMISC is
cleared, and stop at once if it does not
- name each probe after what differs from the binding
- define the bound address and PGN once and derive the probes from them
- assert that SA_OTHER is a third unicast address
Changes in v2:
- drop the Assisted-by trailer; the cover letter says how an LLM was used
- wrap at 80 columns and fix the checkpatch warnings (Marc)
- name all constants and avoid casts
- check the address and the PGN filter each on its own
---
tools/testing/selftests/net/can/test_j1939_opts.c | 129 +++++++++++++++++++++-
1 file changed, 127 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 a729e39c0be4fc8e77717c71000f205dd14f453b..4be2db1e2bb64b3aba534cb9405996f6b1d34edd 100644
--- a/tools/testing/selftests/net/can/test_j1939_opts.c
+++ b/tools/testing/selftests/net/can/test_j1939_opts.c
@@ -6,8 +6,9 @@
* 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 kernel's
* default), unconnected sendto(2), a wrong-family bind rejection,
- * SO_J1939_ERRQUEUE TX-completion notifications, and set/getsockopt
- * round-trips of SO_J1939_SEND_PRIO, SO_J1939_PROMISC and SO_J1939_ERRQUEUE.
+ * SO_J1939_ERRQUEUE TX-completion notifications, the SO_J1939_PROMISC receive
+ * effect, and set/getsockopt round-trips of SO_J1939_SEND_PRIO,
+ * SO_J1939_PROMISC and SO_J1939_ERRQUEUE.
*
* Run by test_j1939_opts.sh, see j1939_tests.h.
*/
@@ -19,6 +20,10 @@
/* arbitrary addresses */
#define SA_TX 0x80
#define SA_RX 0x90
+#define SA_OTHER 0x50 /* neither SA_TX nor SA_RX */
+_Static_assert(SA_OTHER != SA_TX && SA_OTHER != SA_RX &&
+ SA_OTHER <= J1939_MAX_UNICAST_ADDR,
+ "SA_OTHER must be a third unicast address");
/* two different destination-specific PGNs, otherwise arbitrary */
#define PGN_A 0x0ab00
@@ -173,6 +178,126 @@ TEST_F(j1939_opts, filter_pgn)
EXPECT_EQ(errno, EAGAIN);
}
+/*
+ * rx binds to (PROMISC_BOUND_DA, PROMISC_BOUND_PGN). Without SO_J1939_PROMISC
+ * it must not get the probes below. Each probe differs from the binding only
+ * in the address or only in the PGN, so each filter is tested on its own.
+ */
+#define PROMISC_BOUND_DA SA_RX
+#define PROMISC_BOUND_PGN PGN_A
+_Static_assert(SA_OTHER != PROMISC_BOUND_DA && PGN_B != PROMISC_BOUND_PGN,
+ "each probe must differ from the binding");
+_Static_assert(J1939_IS_PDU1(PROMISC_BOUND_PGN) &&
+ PROMISC_BOUND_DA <= J1939_MAX_UNICAST_ADDR,
+ "the binding must be a unicast address and a PDU1 PGN");
+
+enum { PROBE_OTHER_PGN, PROBE_OTHER_ADDR, PROBE_NUM };
+
+static const struct {
+ uint8_t da;
+ pgn_t pgn;
+} promisc_probes[] = {
+ [PROBE_OTHER_PGN] = { PROMISC_BOUND_DA, PGN_B },
+ [PROBE_OTHER_ADDR] = { SA_OTHER, PROMISC_BOUND_PGN },
+};
+
+_Static_assert(ARRAY_SIZE(promisc_probes) == PROBE_NUM,
+ "promisc_probes needs one row per probe");
+
+/*
+ * Send every probe from tx and check that rx gets none of them. Keep
+ * sending after a probe arrives, so each filter is checked on its own.
+ * Return false if a probe arrives. Also return false at once if a probe
+ * cannot be sent.
+ */
+static bool no_probe_received_ok(FIXTURE_DATA(j1939_opts) *self,
+ const uint8_t *buf, size_t len)
+{
+ uint8_t rx_buf[CAN_MAX_DLEN];
+ bool ok = true;
+ size_t i;
+
+ for (i = 0; i < ARRAY_SIZE(promisc_probes); i++) {
+ if (!j1939_sendto_ok(self->tx, buf, len, self->ifindex,
+ J1939_NO_NAME, promisc_probes[i].da,
+ promisc_probes[i].pgn)) {
+ fprintf(stderr, "# sendto probe %zu failed: %d\n", i,
+ errno);
+ return false;
+ }
+ if (recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+ J1939_NEG_RECV_TIMEOUT_MS) != -1) {
+ fprintf(stderr, "# probe %zu was received\n", i);
+ ok = false;
+ }
+ }
+ return ok;
+}
+
+/* SO_J1939_PROMISC delivers frames that do not match the bound (addr, pgn). */
+TEST_F(j1939_opts, promisc_receive)
+{
+ struct sockaddr_can bound_src = {};
+ const pgn_t bound_pgn = PROMISC_BOUND_PGN;
+ uint8_t tx_buf[CAN_MAX_DLEN];
+ size_t i;
+
+ fill_pattern(tx_buf, sizeof(tx_buf));
+
+ self->rx = j1939_open(self->ifindex, PROMISC_BOUND_DA,
+ PROMISC_BOUND_PGN);
+ ASSERT_GE(self->rx, 0);
+ self->tx = j1939_open(self->ifindex, SA_TX, J1939_NO_PGN);
+ ASSERT_GE(self->tx, 0);
+
+ EXPECT_TRUE(no_probe_received_ok(self, tx_buf, sizeof(tx_buf)))
+ TH_LOG("probe check without promisc failed");
+
+ ASSERT_TRUE(j1939_set_flag_ok(self->rx, SO_J1939_PROMISC, 1))
+ TH_LOG("setting SO_J1939_PROMISC failed: %d", errno);
+
+ for (i = 0; i < ARRAY_SIZE(promisc_probes); i++) {
+ struct sockaddr_can src = {};
+ bool got;
+
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME,
+ promisc_probes[i].da,
+ promisc_probes[i].pgn))
+ TH_LOG("sendto probe %zu failed: %d", i, errno);
+ got = recvfrom_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS, &src);
+ EXPECT_TRUE(got)
+ TH_LOG("probe %zu not received despite promisc", i);
+ if (!got)
+ continue;
+ /* the probes carry the same payload, so check which one came */
+ EXPECT_EQ(src.can_addr.j1939.pgn, promisc_probes[i].pgn)
+ TH_LOG("probe %zu came with PGN 0x%05x", i,
+ src.can_addr.j1939.pgn);
+ }
+
+ /* with promisc cleared, the bound (addr, pgn) filters again */
+ ASSERT_TRUE(j1939_set_flag_ok(self->rx, SO_J1939_PROMISC, 0))
+ TH_LOG("clearing SO_J1939_PROMISC failed: %d", errno);
+ EXPECT_TRUE(no_probe_received_ok(self, tx_buf, sizeof(tx_buf)))
+ TH_LOG("probe check after clearing promisc failed");
+
+ /*
+ * Send a frame for the bound address and PGN. It must still
+ * arrive. A socket that receives nothing would pass the probe
+ * check after clearing, too.
+ */
+ ASSERT_TRUE(j1939_sendto_ok(self->tx, tx_buf, sizeof(tx_buf),
+ self->ifindex, J1939_NO_NAME,
+ PROMISC_BOUND_DA, PROMISC_BOUND_PGN))
+ TH_LOG("sendto of the matching frame failed: %d", errno);
+ ASSERT_TRUE(recvfrom_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS, &bound_src))
+ TH_LOG("no matching frame after promisc was cleared");
+ EXPECT_EQ(bound_src.can_addr.j1939.pgn, bound_pgn);
+}
+
/* Without SO_J1939_SEND_PRIO the kernel's default priority is used. */
TEST_F(j1939_opts, send_prio_default)
{
--
2.47.3
^ permalink raw reply [flat|nested] 14+ messages in thread