* [PATCH can-next v2 1/9] selftests: can: install net/lib.sh with the CAN tests
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 2/9] selftests: can: add J1939 test helper header and library Oleksij Rempel
` (7 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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, so that they are
installed with the CAN tests. net/lib.sh sources lib/sh/defer.sh.
test_raw_filter.sh sources ../lib.sh, but the Makefile does not list it.
A full selftest install ships it with the net tests, but a selective one,
such as "make TARGETS=net/can install", leaves it out, and the installed
test_raw_filter.sh then fails to source it.
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 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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 2/9] selftests: can: add J1939 test helper header and library
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 1/9] selftests: can: install net/lib.sh with the CAN tests Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:37 ` sashiko-bot
2026-11-04 16:17 ` [PATCH can-next v2 3/9] selftests: can: add J1939 base functional tests Oleksij Rempel
` (6 subsequent siblings)
8 siblings, 1 reply; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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 the code the later J1939 selftests share: j1939_tests.h with the C
helpers (socket setup, timed receive, CAN id encoding), and j1939_lib.sh
with 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. NSEC_PER_SEC comes from clock-helpers.h as 1000000000LL, so the
product is computed in long long; the result in ms fits a long.
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_lib.sh | 86 +++
tools/testing/selftests/net/can/j1939_tests.h | 724 ++++++++++++++++++++++++++
3 files changed, 812 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_lib.sh b/tools/testing/selftests/net/can/j1939_lib.sh
new file mode 100644
index 0000000000000000000000000000000000000000..87228c692a13a8aad899f622519ebcb539b241e4
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_lib.sh
@@ -0,0 +1,86 @@
+#!/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_tests.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 add name "$CANIF" type vcan; then
+ echo "SKIP: cannot create $CANIF" \
+ "(already exists, or no vcan support)"
+ 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" "$J1939_TEST_PROG"
+ 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..78eb35f5b6b06847e7b004e827ea437b3bb8b38a
--- /dev/null
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -0,0 +1,724 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) */
+/*
+ * Shared helpers for the J1939 selftests.
+ *
+ * The individual tests are standalone kselftest binaries, so the common
+ * boilerplate (interface lookup, socket open/connect, timed receive, CAN-ID
+ * encoding and the CANIF/main() plumbing) lives here as static inline helpers.
+ *
+ * The including .c file must include "kselftest_harness.h" before this header
+ * (j1939_test_main() uses KSFT_SKIP and test_harness_run()).
+ *
+ * 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.
+ *
+ * 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_TESTS_H__
+#define __SELFTESTS_NET_CAN_J1939_TESTS_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. 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: broadcast, 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");
+
+static inline int j1939_id_prio(canid_t id)
+{
+ return (id >> J1939_ID_PRIO_SHIFT) & J1939_ID_PRIO_MASK;
+}
+
+static inline int j1939_id_pgn(canid_t id)
+{
+ return (id >> J1939_ID_PGN_SHIFT) & J1939_PGN_MAX;
+}
+
+static inline int j1939_id_pf(canid_t id)
+{
+ return J1939_PF(j1939_id_pgn(id));
+}
+
+static inline int j1939_id_ps(canid_t id)
+{
+ return j1939_id_pgn(id) & UINT8_MAX;
+}
+
+static inline int j1939_id_sa(canid_t id)
+{
+ return id & UINT8_MAX;
+}
+
+/* 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 */
+
+/* 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 (no RTR, no error frames), see
+ * net/can/j1939/main.c.
+ */
+static inline bool j1939_is_data_frame(canid_t can_id)
+{
+ return (can_id & (CAN_EFF_FLAG | CAN_RTR_FLAG | CAN_ERR_FLAG)) ==
+ CAN_EFF_FLAG;
+}
+
+/*
+ * Size of a CAN_RAW receive buffer large enough for a full ETP burst.
+ * A round number with headroom, not a computed minimum. 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. */
+static 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.
+ */
+static inline int j1939_timing_ms(int relaxed_ms, int strict_ms)
+{
+ return getenv("J1939_STRICT_TIMING") ? strict_ms : relaxed_ms;
+}
+
+/* Resolve the ifindex of the test interface named by CANIF. */
+static inline int j1939_if_index(void)
+{
+ struct ifreq ifr = {};
+ int s, idx = -1;
+
+ s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
+ if (s < 0)
+ return -1;
+ snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", CANIF);
+ if (ioctl(s, SIOCGIFINDEX, &ifr) >= 0)
+ idx = ifr.ifr_ifindex;
+ close(s);
+ return idx;
+}
+
+/*
+ * FIXTURE_SETUP helper: resolve CANIF's ifindex into self->ifindex and abort
+ * the test if it cannot be found. Used as a statement:
+ * J1939_SETUP_IFINDEX(self);
+ */
+#define J1939_SETUP_IFINDEX(self) \
+ do { \
+ (self)->ifindex = j1939_if_index(); \
+ ASSERT_GE((self)->ifindex, 0) \
+ TH_LOG("failed to resolve ifindex for '%s': %d", \
+ CANIF, errno); \
+ } while (0)
+
+/*
+ * Open a J1939 socket bound to (name, sa, pgn); flags may set
+ * J1939_OPEN_BCAST.
+ */
+static inline int j1939_open_flags(int ifindex, name_t name, uint8_t sa,
+ pgn_t pgn, int flags)
+{
+ struct sockaddr_can addr = {
+ .can_family = AF_CAN,
+ .can_ifindex = ifindex,
+ };
+ int s, one = 1;
+
+ s = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
+ if (s < 0)
+ return -1;
+
+ if ((flags & J1939_OPEN_BCAST) &&
+ setsockopt(s, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one))) {
+ close(s);
+ return -1;
+ }
+
+ addr.can_addr.j1939.name = name;
+ addr.can_addr.j1939.addr = sa;
+ addr.can_addr.j1939.pgn = pgn;
+ if (bind(s, (struct sockaddr *)&addr, sizeof(addr))) {
+ close(s);
+ return -1;
+ }
+ return s;
+}
+
+/* Open a J1939 socket bound to (sa, pgn) with no NAME and no broadcast. */
+static inline int j1939_open(int ifindex, uint8_t sa, pgn_t pgn)
+{
+ return j1939_open_flags(ifindex, J1939_NO_NAME, sa, pgn,
+ J1939_OPEN_NONE);
+}
+
+/* Connect a J1939 socket to the peer (name, da, pgn). */
+static inline 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));
+}
+
+static inline 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);
+}
+
+/* Fill a sockaddr_can describing a J1939 (name, da, pgn) destination. */
+static inline void j1939_fill_sockaddr(struct sockaddr_can *addr, int ifindex,
+ name_t name, uint8_t da, pgn_t pgn)
+{
+ memset(addr, 0, sizeof(*addr));
+ addr->can_family = AF_CAN;
+ addr->can_ifindex = ifindex;
+ addr->can_addr.j1939.name = name;
+ addr->can_addr.j1939.addr = da;
+ addr->can_addr.j1939.pgn = pgn;
+}
+
+/* sendto() len bytes from buf to the J1939 destination dst. */
+static inline 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));
+}
+
+/* sendto() len bytes from buf to the J1939 (name, da, pgn) destination. */
+static inline 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);
+}
+
+/* One j1939_sendto() that sent all len bytes; no retry. */
+static inline 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;
+}
+
+/* One send(), write() or read() that moved all len bytes; no retry. */
+static inline 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;
+}
+
+static inline bool write_all_ok(int fd, const void *buf, size_t len)
+{
+ ssize_t want_len = len;
+
+ return write(fd, buf, len) == want_len;
+}
+
+static inline bool read_all_ok(int fd, void *buf, size_t len)
+{
+ ssize_t want_len = len;
+
+ return read(fd, buf, len) == want_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).
+ */
+static inline int open_raw_rcvbuf(int ifindex, int rcvbuf)
+{
+ struct sockaddr_can addr = {
+ .can_family = AF_CAN,
+ .can_ifindex = ifindex,
+ };
+ int s;
+
+ s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+ if (s < 0)
+ return -1;
+
+ if (rcvbuf != 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;
+}
+
+static inline int open_raw(int ifindex)
+{
+ return open_raw_rcvbuf(ifindex, J1939_RAW_RCVBUF_DEFAULT);
+}
+
+/*
+ * 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)
+
+/* poll() + recv(): returns the recv() result, or -1 on timeout/error. */
+static inline 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);
+}
+
+/*
+ * 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.
+ */
+static inline 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);
+ uint8_t *rx_buf = malloc(len + 1);
+ 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, len + 1, 0, (struct sockaddr *)src,
+ &src_len);
+ ok = n == want_len && !memcmp(want, rx_buf, len);
+
+ free(rx_buf);
+ return ok;
+}
+
+/* recvfrom_verify_ok() without the sender. */
+static inline 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);
+}
+
+/* Send len bytes on tx and verify they arrive byte-for-byte on rx. */
+static inline 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);
+}
+
+/* Receive one whole CAN frame from a raw socket within timeout_ms. */
+static inline 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;
+}
+
+/* Encode a J1939 single frame the way the kernel does on transmit. */
+static inline canid_t j1939_canid(uint8_t prio, pgn_t pgn, uint8_t da,
+ uint8_t sa)
+{
+ canid_t id = CAN_EFF_FLAG | (prio << J1939_ID_PRIO_SHIFT) |
+ (pgn << J1939_ID_PGN_SHIFT) | sa;
+
+ /* the destination goes into the PS byte of the PGN field */
+ if (J1939_IS_PDU1(pgn))
+ id |= da << J1939_ID_PGN_SHIFT;
+ return id;
+}
+
+/*
+ * Start a TP/ETP control frame: every byte J1939_FILL_BYTE, then the control
+ * byte cmd.
+ */
+static inline void cm_dat_init(uint8_t *dat, uint8_t cmd)
+{
+ memset(dat, J1939_FILL_BYTE, CAN_MAX_DLEN);
+ dat[J1939_CM_CMD_OFF] = cmd;
+}
+
+/*
+ * Serialize a 64-bit NAME little-endian into a buffer of sizeof(name_t)
+ * bytes (host-agnostic).
+ */
+static inline 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);
+}
+
+/* Little-endian codecs for the TP/ETP CM multi-byte fields (host-agnostic). */
+static inline 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);
+}
+
+static inline 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;
+}
+
+static inline void put_le16(uint8_t *p, uint16_t v)
+{
+ put_le(p, v, sizeof(v));
+}
+
+static inline void put_le24(uint8_t *p, uint32_t v)
+{
+ put_le(p, v, J1939_LE24_LEN);
+}
+
+static inline void put_le32(uint8_t *p, uint32_t v)
+{
+ put_le(p, v, sizeof(v));
+}
+
+static inline uint16_t get_le16(const uint8_t *p)
+{
+ return get_le(p, sizeof(uint16_t));
+}
+
+static inline uint32_t get_le24(const uint8_t *p)
+{
+ return get_le(p, J1939_LE24_LEN);
+}
+
+static inline uint32_t get_le32(const uint8_t *p)
+{
+ return get_le(p, sizeof(uint32_t));
+}
+
+/* Embed the PGN of the transported message into a TP/ETP control frame. */
+static inline void put_pgn(uint8_t *dat, pgn_t pgn)
+{
+ put_le24(&dat[J1939_CM_PGN_OFF], pgn);
+}
+
+/* Send one J1939-encoded frame of CAN_MAX_DLEN bytes with priority prio. */
+static inline 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));
+}
+
+/*
+ * Payload bytes of the DT frame that starts at offset off of a message of
+ * size bytes; off must be below size.
+ */
+static inline 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;
+}
+
+/*
+ * 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.
+ */
+static inline 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);
+}
+
+/* Milliseconds from a to b (negative if b is earlier), CLOCK_MONOTONIC. */
+static inline 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;
+}
+
+/* Set *deadline to timeout_ms from now on CLOCK_MONOTONIC. */
+static inline 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;
+ }
+}
+
+/*
+ * 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.
+ */
+static inline 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;
+}
+
+struct j1939_peer_match { uint8_t want_pf, want_da, want_sa; };
+
+static inline bool match_peer_dest(canid_t id, const void *ctx)
+{
+ const struct j1939_peer_match *m = ctx;
+
+ return j1939_id_pf(id) == m->want_pf &&
+ j1939_id_ps(id) == m->want_da && /* destination */
+ j1939_id_sa(id) == m->want_sa; /* source */
+}
+
+/*
+ * Receive the next frame whose CAN-id PF == want_pf, sent from want_sa to
+ * want_da (PS).
+ */
+static inline 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);
+}
+
+/* recv_for_peer_ok() and check that the control byte is want_cmd. */
+static inline 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;
+}
+
+/* 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 differ, so a misplaced packet
+ * shows up.
+ */
+static inline 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;
+}
+
+/* Is the CAN protocol proto missing from this kernel? */
+static inline 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;
+}
+
+/*
+ * 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) ||
+ can_proto_missing(SOCK_RAW, CAN_RAW))
+ return KSFT_SKIP;
+
+ return test_harness_run(argc, argv);
+}
+
+#endif /* __SELFTESTS_NET_CAN_J1939_TESTS_H__ */
--
2.47.3
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH can-next v2 2/9] selftests: can: add J1939 test helper header and library
2026-11-04 16:17 ` [PATCH can-next v2 2/9] selftests: can: add J1939 test helper header and library Oleksij Rempel
@ 2026-11-04 16:37 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-11-04 16:37 UTC (permalink / raw)
To: Oleksij Rempel
Cc: linux-can, Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests: can: silent skip in j1939_test_main() when protocol is missing
--
commit f577ac7f70eba0a6d39e0839d07a59a287fefd8a
Author: Oleksij Rempel <o.rempel@pengutronix.de>
selftests: can: add J1939 test helper header and library
This commit adds j1939_tests.h and j1939_lib.sh to provide shared C
helpers and vcan setup/teardown logic for the J1939 selftests.
> 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 0000000000000..78eb35f5b6b06
> --- /dev/null
> +++ b/tools/testing/selftests/net/can/j1939_tests.h
> @@ -0,0 +1,724 @@
[ ... ]
> +/* Is the CAN protocol proto missing from this kernel? */
> +static inline 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;
> +}
> +
> +/*
> + * 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) ||
> + can_proto_missing(SOCK_RAW, CAN_RAW))
> + return KSFT_SKIP;
[Severity: Low]
In tools/testing/selftests/net/can/j1939_tests.h, when can_proto_missing()
returns true because socket(PF_CAN, SOCK_DGRAM, CAN_J1939) or
socket(PF_CAN, SOCK_RAW, CAN_RAW) fails with EAFNOSUPPORT or
EPROTONOSUPPORT, j1939_test_main() returns KSFT_SKIP without printing why
the test was skipped.
Since j1939_run() in tools/testing/selftests/net/can/j1939_lib.sh only
passes J1939_TEST_PROG to ret_set_ksft_status on a skip exit code, could
j1939_test_main() print a message stating which protocol support is missing
(similar to the CANIF check right above it) so the skip reason is visible
in the test output?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104-j1939-selftests-v2-0-cf797c83317b@pengutronix.de?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH can-next v2 3/9] selftests: can: add J1939 base functional tests
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 1/9] selftests: can: install net/lib.sh with the CAN tests Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 2/9] selftests: can: add J1939 test helper header and library Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 4/9] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
` (5 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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 runs two
CAN_J1939 sockets (a sender and a receiver) 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.
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 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 | 10 +-
tools/testing/selftests/net/can/config | 2 +
tools/testing/selftests/net/can/test_j1939_base.c | 180 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_base.sh | 4 +
5 files changed, 195 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..9068dd77fb903b6098bc36fbc4cf8075937eb66e 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -4,12 +4,18 @@ top_srcdir = ../../../../..
CFLAGS += -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INCLUDES)
-TEST_PROGS := test_raw_filter.sh
+LOCAL_HDRS := j1939_tests.h
+
+TEST_PROGS := \
+ test_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
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..d01f4c2c186cba07264a62a04ad53a0550c0ef2e
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_base.c
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 base functional selftests.
+ *
+ * Run two SOCK_DGRAM/CAN_J1939 sockets (a sender and a receiver) over a vcan
+ * interface and verify payload integrity across the transport-size transitions:
+ * a single frame (PDU1), Transport Protocol, Extended Transport Protocol, the
+ * exact boundaries between them, and single-frame and BAM broadcast (PDU2).
+ *
+ * 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; only the PDU class of the PGNs matters */
+#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");
+
+/* 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");
+
+FIXTURE(j1939_base)
+{
+ int ifindex;
+ int tx;
+ int rx;
+};
+
+FIXTURE_SETUP(j1939_base)
+{
+ self->tx = -1;
+ self->rx = -1;
+ J1939_SETUP_IFINDEX(self);
+}
+
+FIXTURE_TEARDOWN(j1939_base)
+{
+ if (self->tx >= 0)
+ close(self->tx);
+ if (self->rx >= 0)
+ close(self->rx);
+}
+
+/*
+ * Set up a receiver bound to (rx_addr, pgn) and a sender bound to SA_TX and
+ * connected to (tx_dst, pgn). Any pair opened by a previous call is closed
+ * first. 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;
+
+ 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)
+{
+ uint8_t *tx_buf;
+ bool ok;
+
+ 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,
+ J1939_RECV_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));
+}
+
+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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 4/9] selftests: can: add J1939 socket option and metadata tests
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (2 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 3/9] selftests: can: add J1939 base functional tests Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 5/9] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
` (4 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
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_tests.h | 67 ++++++
tools/testing/selftests/net/can/test_j1939_opts.c | 266 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_opts.sh | 4 +
5 files changed, 339 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 9068dd77fb903b6098bc36fbc4cf8075937eb66e..009ea9a53178a9218c11181150e9693bc3da2fdb 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -8,6 +8,7 @@ LOCAL_HDRS := j1939_tests.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_tests.h b/tools/testing/selftests/net/can/j1939_tests.h
index 78eb35f5b6b06847e7b004e827ea437b3bb8b38a..639e000ac5e8631e7ebfc1548a7b14892fa61b32 100644
--- a/tools/testing/selftests/net/can/j1939_tests.h
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -41,6 +41,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"
@@ -671,6 +673,71 @@ static inline bool recv_cm_cmd_ok(int raw, uint8_t want_pf, uint8_t want_da,
return frame->data[J1939_CM_CMD_OFF] == want_cmd;
}
+/*
+ * 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
+ * these flags when the session ends, so this may come after the send.
+ */
+static inline bool errqueue_enable_ack_ok(int s)
+{
+ int tstamp_flags = SOF_TIMESTAMPING_TX_ACK;
+ int enable = 1;
+
+ /* setsockopt(SO_J1939_ERRQUEUE) returns the flag value, not 0 */
+ if (setsockopt(s, SOL_CAN_J1939, SO_J1939_ERRQUEUE, &enable,
+ sizeof(enable)) < 0)
+ return false;
+
+ return !setsockopt(s, SOL_SOCKET, SO_TIMESTAMPING, &tstamp_flags,
+ sizeof(tstamp_flags));
+}
+
+/* Does the error-queue message msg carry the TX-completion ACK? */
+static inline 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_origin == SO_EE_ORIGIN_TIMESTAMPING &&
+ serr->ee_info == SCM_TSTAMP_ACK)
+ return true;
+ }
+ return false;
+}
+
+/*
+ * 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. Each
+ * entry then carries one cmsg, SCM_J1939_ERRQUEUE; no message data is read (no
+ * iovec is passed).
+ */
+static inline 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)
+ return false;
+
+ return msg_has_tstamp_ack(&msg);
+}
+
/* 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..10d0e379840277cddbe9d1f5090f900ce7dd65dc
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_opts.c
@@ -0,0 +1,266 @@
+// 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 <time.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");
+
+/* any valid priority but the default */
+#define PRIO_SET 3
+_Static_assert(PRIO_SET != J1939_PRIO_DEFAULT,
+ "PRIO_SET must differ from the default");
+
+/* a message that needs TP, so that the kernel runs a session to ACK */
+#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. */
+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 (or -1). */
+static int sockopt_roundtrip(int s, int optname, int val)
+{
+ int got = -1;
+ socklen_t len = sizeof(got);
+
+ if (setsockopt(s, SOL_CAN_J1939, optname, &val, sizeof(val)) < 0)
+ return -1;
+ if (getsockopt(s, SOL_CAN_J1939, optname, &got, &len) < 0)
+ return -1;
+ return got;
+}
+
+FIXTURE(j1939_opts)
+{
+ int ifindex;
+ int tx;
+ int rx;
+ int raw;
+};
+
+FIXTURE_SETUP(j1939_opts)
+{
+ self->tx = -1;
+ self->rx = -1;
+ self->raw = -1;
+
+ J1939_SETUP_IFINDEX(self);
+
+ J1939_SETUP_RAW(self, 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,
+ };
+ 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 */
+ 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);
+ EXPECT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+ J1939_NEG_RECV_TIMEOUT_MS), -1)
+ TH_LOG("PGN_B was received but should have been filtered out");
+}
+
+/* Without SO_J1939_SEND_PRIO the 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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 5/9] selftests: can: add J1939 addressing and concurrency tests
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (3 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 4/9] selftests: can: add J1939 socket option and metadata tests Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
` (3 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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. They retry the first send
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.
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_tests.h | 7 +-
tools/testing/selftests/net/can/test_j1939_addr.c | 679 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_addr.sh | 4 +
5 files changed, 691 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 009ea9a53178a9218c11181150e9693bc3da2fdb..d72095ad7d849d8a35c20e9c5bdaa81bc194bc64 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_tests.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_tests.h b/tools/testing/selftests/net/can/j1939_tests.h
index 639e000ac5e8631e7ebfc1548a7b14892fa61b32..bb22faa323e3d7de87a0a9a447216caaeb6b4b52 100644
--- a/tools/testing/selftests/net/can/j1939_tests.h
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -18,7 +18,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_TESTS_H__
#define __SELFTESTS_NET_CAN_J1939_TESTS_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..02f9e504b567f148b904cd0b95fae6ebbbf13cac
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_addr.c
@@ -0,0 +1,679 @@
+// 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 but for the order of the three 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");
+
+/* ends of a pipe(2) fd pair */
+enum { PIPE_RD, PIPE_WR, PIPE_NUM };
+
+/* bytes of the data frames that send_data() and its *_after_claim_ok() send */
+#define SEND_DATA_LEN CAN_MAX_DLEN
+
+/*
+ * 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 earlier than ACLAIM_MIN_MS and no later
+ * than ACLAIM_MAX_MS (strict: ACLAIM_STRICT_MAX_MS), in the tests that time
+ * it. A defended address must stay ours, and the contender must get none,
+ * until that latest time. 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 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, tried 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 round-trip. 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)
+{
+ 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;
+
+ 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);
+
+ 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));
+}
+
+/* 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);
+}
+
+/*
+ * claim_addr() sends with the default priority to the global address, from
+ * the address in *ctx (a uint8_t).
+ */
+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;
+}
+
+/* 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;
+
+ 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");
+
+ EXPECT_TRUE(send_data_after_claim_ok(self->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);
+}
+
+/*
+ * Claim SA_CLAIM with NAME_CLAIM and check that we can send, then forge a claim
+ * for SA_CLAIM with the @contender NAME, sent at *forge_t0. Store the result
+ * of the next send in *tx_ret, with errno as that send set it. Returns 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));
+}
+
+/* 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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (4 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 5/9] selftests: can: add J1939 addressing and concurrency tests Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 7/9] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
` (2 subsequent siblings)
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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 (with the DP bit set) and PDU2: 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 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 | 186 +++++++++++++++++++++
tools/testing/selftests/net/can/test_j1939_wire.sh | 4 +
4 files changed, 192 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 d72095ad7d849d8a35c20e9c5bdaa81bc194bc64..5262f0b8f73f80f524c32665c1fc08a7a0ec7e78 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..ad542ed241c0b0e59c40cc49bccc418031273002
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_wire.c
@@ -0,0 +1,186 @@
+// 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; only PDU class and DP bit matter */
+#define SA_LOCAL 0x90 /* the J1939 socket under test */
+#define SA_PEER 0x20 /* the CAN_RAW peer */
+/* a destination-specific PGN with DP set, and a broadcast PGN */
+#define PGN_PDU1 (J1939_PGN_DP | 0x0ab00)
+#define PGN_PDU2 0x0feee
+_Static_assert(J1939_IS_PDU1(PGN_PDU1) && !J1939_IS_PDU1(PGN_PDU2),
+ "PGN_PDU1 must be PDU1 and PGN_PDU2 must be PDU2");
+
+#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 selects a broadcast (PDU2) socket and frame; @pgn and @da address the
+ * injected frame.
+ */
+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,
+ "# from SA 0x%02x PGN 0x%05x, want 0x%02x 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 a broadcast (PDU2) socket.
+ */
+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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 7/9] selftests: can: add J1939 TP/ETP multi-frame tests against CAN_RAW
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (5 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 6/9] selftests: can: add J1939 wire-format tests against CAN_RAW Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 8/9] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 9/9] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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 check the PGN in each control frame the kernel sends, with a
PGN that has the DP bit set. In the TX tests, the peer's EOMA must end
the session with a TX-completion ACK on the socket's error queue, and no
ACK may come before the EOMA.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
Q: can a multi test fail against a correct kernel?
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 a multi test. The fix
belongs in the stack, not in these tests.
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_tests.h | 8 +
tools/testing/selftests/net/can/test_j1939_multi.c | 618 +++++++++++++++++++++
.../testing/selftests/net/can/test_j1939_multi.sh | 4 +
5 files changed, 632 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 5262f0b8f73f80f524c32665c1fc08a7a0ec7e78..0b9fb4227f07b35a0fbd8a33534754a36b00b73c 100644
--- a/tools/testing/selftests/net/can/Makefile
+++ b/tools/testing/selftests/net/can/Makefile
@@ -9,6 +9,7 @@ LOCAL_HDRS := j1939_tests.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_tests.h b/tools/testing/selftests/net/can/j1939_tests.h
index bb22faa323e3d7de87a0a9a447216caaeb6b4b52..8c3ac4b4538a3948aaa9d843237ef846a49b6c70 100644
--- a/tools/testing/selftests/net/can/j1939_tests.h
+++ b/tools/testing/selftests/net/can/j1939_tests.h
@@ -743,6 +743,14 @@ static inline bool errqueue_read_ack_ok(int s)
return msg_has_tstamp_ack(&msg);
}
+/* Is the error queue of s still empty after J1939_NEG_RECV_TIMEOUT_MS? */
+static inline bool errqueue_empty_ok(int s)
+{
+ struct pollfd pfd = { .fd = s, .events = POLLERR };
+
+ return poll(&pfd, 1, J1939_NEG_RECV_TIMEOUT_MS) == 0;
+}
+
/* 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..88a283a3394e026f69d17cd36222c0812d9b38a1
--- /dev/null
+++ b/tools/testing/selftests/net/can/test_j1939_multi.c
@@ -0,0 +1,618 @@
+// SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause)
+/*
+ * J1939 multi-frame wire selftests: TP and ETP against CAN_RAW.
+ *
+ * A CAN_RAW socket plays the transport peer and drives the handshake against
+ * the kernel J1939 stack, in both directions: for TP the raw side answers
+ * RTS/CTS and exchanges DT frames with EOMA; ETP does the same
+ * with a payload spanning several CTS blocks (DPO windows).
+ *
+ * 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 */
+/* transported message PGN, with the DP bit set so a PGN cut to 16 bits shows */
+#define PGN_DATA (J1939_PGN_DP | 0x0feef)
+_Static_assert(PGN_DATA & J1939_PGN_DP, "PGN_DATA must have the DP bit set");
+
+/*
+ * 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.
+ */
+#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
+#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
+
+/* message sizes: TP of several packets, ETP of several ETP.CTS blocks */
+#define TP_SIZE 200
+_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");
+
+#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");
+
+/* 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 has the control byte cmd and carries 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 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 for cts_pkts packets from the 1-based
+ * packet next. After sent of num_pkts packets, 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 the ETP_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)
+{
+ unsigned int num_pkts = J1939_NUM_PKTS(ETP_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 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, ETP_SIZE,
+ TP_FIRST_PKT))
+ return false;
+
+ sent += cts_pkts;
+ }
+ return true;
+}
+
+/*
+ * 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 0-based
+ * absolute packet the 1-based seq is relative to (TP_BASE_PKT for TP, the DPO
+ * offset for ETP).
+ */
+static void unpack_dt_into(uint8_t *buf, size_t size, unsigned int base_pkt,
+ const struct can_frame *frame)
+{
+ size_t abs_pkt = base_pkt + (frame->data[J1939_DT_SEQ_OFF] - 1);
+ size_t off = abs_pkt * J1939_DT_DATA_LEN;
+
+ /* 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));
+}
+
+/*
+ * 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, see
+ * j1939_tp_tx_dat()) and the sequence number TP_FIRST_PKT + i, as in one ETP
+ * block or a TP CTS window from 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;
+ }
+ 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 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);
+
+ /* 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("TX-completion ACK 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));
+
+ 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 ETP block of cts_pkts packets after the first got
+ * packets, check its DPO and receive the block into rx_buf.
+ */
+static bool etp_tx_serve_block_ok(FIXTURE_DATA(j1939_multi) *self,
+ uint8_t *rx_buf, 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 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, ETP_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 ETP_CTS_MAX_PKTS packets (ETP.CTS); for
+ * each block the kernel sends a DPO followed by the DT frames, acknowledged by
+ * EOMA, which completes the session: the socket 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, 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("TX-completion ACK 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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 8/9] selftests: can: cover J1939 transport abort and timeout paths
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (6 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 7/9] selftests: can: add J1939 TP/ETP multi-frame " Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
2026-11-04 16:17 ` [PATCH can-next v2 9/9] selftests: can: check SO_J1939_PROMISC receive effect Oleksij Rempel
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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: a per-CTS packet limit
below the message's packet count, the T3 timeout (no CTS after RTS) and
the T2 timeout (no DT after CTS), which must end with a Connection Abort,
and a received Abort that cancels an incoming session.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
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 | 253 ++++++++++++++++++++-
1 file changed, 252 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 88a283a3394e026f69d17cd36222c0812d9b38a1..0af15e9ea9550346f943d0c1aa87628877588e61 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 and drives the handshake against
* the kernel J1939 stack, in both directions: for TP the raw side answers
* RTS/CTS and exchanges DT frames with EOMA; ETP does the same
- * with a payload spanning several CTS blocks (DPO windows).
+ * with a payload spanning several CTS blocks (DPO windows). The tests also
+ * cover the TP CTS block limit and the TP abort and timeout paths.
*
* Run by test_j1939_multi.sh, see j1939_tests.h.
*/
@@ -43,6 +44,7 @@ _Static_assert(PGN_DATA & J1939_PGN_DP, "PGN_DATA must have the DP bit set");
#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
@@ -57,6 +59,7 @@ _Static_assert(PGN_DATA & J1939_PGN_DP, "PGN_DATA must have the DP bit set");
#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
@@ -89,6 +92,62 @@ _Static_assert(J1939_NUM_PKTS(ETP_SIZE) > ETP_CTS_MAX_PKTS,
#define TP_CTS_ALL J1939_NUM_PKTS(TP_SIZE)
_Static_assert(TP_CTS_ALL <= UINT8_MAX, "the TP.CM packet counts are one byte");
+/* an RTS per-CTS limit below the packets of TP_SIZE, arbitrary */
+#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");
+
+/*
+ * 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;
@@ -497,6 +556,112 @@ 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");
+}
+
+/* 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
@@ -612,6 +777,92 @@ 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 a full RTS, consume the first CTS. */
+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);
+}
+
+/*
+ * 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 T2 when it gets the RTS, and again when it sends the CTS
+ * and on the CAN echo of that CTS; 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 related [flat|nested] 11+ messages in thread* [PATCH can-next v2 9/9] selftests: can: check SO_J1939_PROMISC receive effect
2026-11-04 16:17 [PATCH can-next v2 0/9] selftests: can: add J1939 socket and transport tests Oleksij Rempel
` (7 preceding siblings ...)
2026-11-04 16:17 ` [PATCH can-next v2 8/9] selftests: can: cover J1939 transport abort and timeout paths Oleksij Rempel
@ 2026-11-04 16:17 ` Oleksij Rempel
8 siblings, 0 replies; 11+ messages in thread
From: Oleksij Rempel @ 2026-11-04 16:17 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.
It sends two frames, once before and once after enabling the flag. Each
misses one of the socket's filters, the bound address or the bound PGN.
Only the frames sent after enabling the flag must arrive.
Signed-off-by: Oleksij Rempel <o.rempel@pengutronix.de>
---
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 | 64 ++++++++++++++++++++++-
1 file changed, 62 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 10d0e379840277cddbe9d1f5090f900ce7dd65dc..bbf2698de7d9491fca4881ade4c63284926ef9eb 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.
*/
@@ -20,6 +21,9 @@
/* 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_TX != SA_RX,
+ "test addresses overlap");
/* two different destination-specific PGNs, otherwise arbitrary */
#define PGN_A 0x0ab00
@@ -130,6 +134,62 @@ TEST_F(j1939_opts, filter_pgn)
TH_LOG("PGN_B was received but should have been filtered out");
}
+/*
+ * Frames that a socket bound to (SA_RX, PGN_A) must not get without
+ * SO_J1939_PROMISC: each misses one of the two, so each filter is tested on
+ * its own.
+ */
+static const struct {
+ uint8_t da;
+ pgn_t pgn;
+} promisc_probes[] = {
+ { SA_RX, PGN_B }, /* the bound address, another PGN */
+ { SA_OTHER, PGN_A }, /* another address, the bound PGN */
+};
+
+/* SO_J1939_PROMISC delivers frames that do not match the bound (addr, pgn). */
+TEST_F(j1939_opts, promisc_receive)
+{
+ uint8_t tx_buf[CAN_MAX_DLEN];
+ uint8_t rx_buf[CAN_MAX_DLEN];
+ int enable = 1;
+ size_t i;
+
+ 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);
+
+ for (i = 0; i < ARRAY_SIZE(promisc_probes); i++) {
+ 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);
+ EXPECT_EQ(recv_timeout(self->rx, rx_buf, sizeof(rx_buf),
+ J1939_NEG_RECV_TIMEOUT_MS), -1)
+ TH_LOG("probe %zu was received without promisc", i);
+ }
+
+ /* SO_J1939_PROMISC returns the flag value (1), not 0, on success. */
+ ASSERT_GE(setsockopt(self->rx, SOL_CAN_J1939, SO_J1939_PROMISC,
+ &enable, sizeof(enable)), 0)
+ TH_LOG("SO_J1939_PROMISC failed: %d", errno);
+
+ for (i = 0; i < ARRAY_SIZE(promisc_probes); i++) {
+ 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);
+ EXPECT_TRUE(recv_verify_ok(self->rx, tx_buf, sizeof(tx_buf),
+ J1939_RECV_TIMEOUT_MS))
+ TH_LOG("probe %zu not received despite promisc", i);
+ }
+}
+
/* 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 related [flat|nested] 11+ messages in thread