From: sashiko-bot@kernel.org
To: "Zihan Xi" <zihanx@nebusec.ai>, linux-can@vger.kernel.org
Cc: "Marc Kleine-Budde" <mkl@pengutronix.de>,
"Oliver Hartkopp" <socketcan@hartkopp.net>,
"Vincent Mailhol" <mailhol@kernel.org>,
"Oleksij Rempel" <o.rempel@pengutronix.de>
Subject: Re: [PATCH net v5 3/3] can: j1939: serialize session TX with state changes
Date: Tue, 03 Nov 2026 05:46:32 +0000 [thread overview]
Message-ID: <sashiko-outbox-194597@kernel.org> (raw)
In-Reply-To: <c7ce2c35e7d6a5097363b9b0ec69dabdc17a5326.1793681851.git.zihanx@nebusec.ai>
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] can: j1939: rxtimer left armed on -ESHUTDOWN in j1939_simple_txnext()
- [High] can: j1939: race between j1939_session_cancel() and j1939_tp_rxtimer()
- [High] can: j1939: rxtimer left armed on DONE session in duplicate RTS and abort paths
- [High] can: j1939: reference leak in concurrent j1939_tp_set_rxtimeout() calls
- [Medium] can: j1939: j1939_session_tx_dat() logs alert and arms rxtimer on -ESHUTDOWN
--
commit a6494e93c192c5af789ee93f88a46a4c6b3896d4
Author: Zihan Xi <zihanx@nebusec.ai>
can: j1939: serialize session TX with state changes
This commit adds a per-session tx_send_lock and tx_send_shutdown gate to
serialize J1939 session frame transmissions against terminal state
transitions. It also reorders RX abort timeout setup in j1939_session_cancel()
and updates simple session timeout handling.
Link: https://bugtracker.nebusec.ai/f/11143
> diff --git a/net/can/j1939/transport.c b/net/can/j1939/transport.c
> index c85d7bb3c12d4..7d6ed7fb8c56b 100644
> --- a/net/can/j1939/transport.c
> +++ b/net/can/j1939/transport.c
[ ... ]
> @@ -730,12 +757,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);
> }
[Severity: Medium]
When j1939_tp_tx_dat() returns -ESHUTDOWN in j1939_session_tx_dat() because a
terminal state transition won, won't j1939_session_tx_dat() log a spurious
netdev_alert() and potentially re-arm session->rxtimer on the terminated
session?
In j1939_session_tx_dat():
ret = j1939_tp_tx_dat(session, dat, len + 1);
if (ret < 0) {
/* ENOBUFS == CAN interface TX queue is full */
if (ret != -ENOBUFS)
netdev_alert(priv->ndev,
"%s: 0x%p: queue data error: %i\n",
__func__, session, ret);
break;
}
...
if (pkt_done)
j1939_tp_set_rxtimeout(session, 250);
If an earlier loop iteration sent at least one packet (pkt_done > 0) before a
subsequent iteration failed with -ESHUTDOWN, j1939_tp_set_rxtimeout() will
still run and re-arm session->rxtimer after the session has already left
J1939_SESSION_ACTIVE.
Similarly, in j1939_session_tx_rts(), j1939_session_tx_dpo(),
j1939_session_tx_cts(), and j1939_session_tx_eoma(), j1939_tp_set_rxtimeout()
is called after j1939_session_tx_send() drops session->tx_send_lock, where a
concurrent terminal transition can be overwritten by the new RX timeout.
[ ... ]
> @@ -1173,7 +1203,7 @@ static int j1939_simple_txnext(struct j1939_session *session)
>
> j1939_tp_set_rxtimeout(session, J1939_SIMPLE_ECHO_TIMEOUT_MS);
>
> - ret = j1939_send_one(priv, skb);
> + ret = j1939_session_tx_send(session, skb);
> if (ret)
> goto out_free;
[Severity: High]
What happens to session->rxtimer if j1939_session_tx_send() returns -ESHUTDOWN
here in j1939_simple_txnext()?
If j1939_tp_txtimer() passes its initial J1939_SESSION_ACTIVE check and a
concurrent j1939_cancel_active_session() transitions the session to
J1939_SESSION_DONE before j1939_simple_txnext() runs, j1939_simple_txnext()
arms session->rxtimer for J1939_SIMPLE_ECHO_TIMEOUT_MS (10 seconds) and grabs
a session reference before j1939_session_tx_send() rejects the packet with
-ESHUTDOWN.
In j1939_tp_txtimer(), the new -ESHUTDOWN case breaks out of the switch
without calling j1939_session_rxtimer_cancel(session), unlike the -ENOBUFS
retry-exhaustion and default error branches:
case -ESHUTDOWN:
/* A terminal state transition won before the frame was sent. */
break;
Since no frame was sent and the session is already unlinked from
priv->active_session_list, won't session->rxtimer remain armed for 10 seconds,
pinning the session, socket, and net_device, and then fire j1939_tp_rxtimer()
to log a false timeout alert and overwrite session->err with -ETIME?
[ ... ]
> @@ -1261,19 +1302,36 @@ static void j1939_session_cancel(struct j1939_session *session,
>
> 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;
> + 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);
> + }
> + }
> }
>
> j1939_session_list_unlock(session->priv);
> - if (cancelled)
> + if (cancelled) {
> + bool keep_rxtimer;
> +
> + /* Do not wait for the RX timer while holding the session-list lock. */
> + j1939_tp_set_rxtimeout(session, J1939_XTP_ABORT_TIMEOUT_MS);
> + j1939_session_list_lock(session->priv);
> + keep_rxtimer = session->state == J1939_SESSION_WAITING_ABORT;
> + j1939_session_list_unlock(session->priv);
> + if (!keep_rxtimer)
> + j1939_session_rxtimer_cancel(session);
> j1939_session_txtimer_cancel_async(session);
> + }
[Severity: High]
Can calling j1939_tp_set_rxtimeout() here outside active_session_list_lock
before cancelling txtimer race with a concurrent j1939_tp_set_rxtimeout() call
and leak a session reference?
j1939_tp_set_rxtimeout() performs an unlocked cancel-get-start sequence:
j1939_session_rxtimer_cancel(session);
j1939_session_get(session);
hrtimer_start(&session->rxtimer, ms_to_ktime(msec),
HRTIMER_MODE_REL_SOFT);
Because j1939_session_timers_cancel() was removed from the RX error callers
and txtimer is only cancelled asynchronously after j1939_tp_set_rxtimeout(),
an in-flight j1939_tp_txtimer() callback that just completed
j1939_session_tx_send() (or a concurrent RX path) can call
j1939_tp_set_rxtimeout() concurrently with j1939_session_cancel(). If both
callers pass j1939_session_rxtimer_cancel() before either calls
hrtimer_start(), both increment session->kref and the second hrtimer_start()
overwrites the first without dropping its reference, leaking struct
j1939_session, the socket reference, and the net_device tracker.
Also, in the keep_rxtimer check above, what if j1939_xtp_rx_abort_one() runs
concurrently? In j1939_xtp_rx_abort_one():
j1939_session_timers_cancel(session);
j1939_session_deactivate_activate_next(session);
If j1939_xtp_rx_abort_one() finishes j1939_session_timers_cancel() (which sets
tx_send_shutdown = true and cancels rxtimer) before j1939_session_cancel()
calls j1939_tp_set_rxtimeout(), and j1939_session_cancel() checks keep_rxtimer
before j1939_session_deactivate_activate_next() updates session->state to
J1939_SESSION_DONE, keep_rxtimer will evaluate to true because session->state
is still J1939_SESSION_WAITING_ABORT, leaving rxtimer armed on a DONE session.
Should keep_rxtimer also verify that session->tx_send_shutdown is false?
[ ... ]
> @@ -1403,7 +1464,7 @@ static enum hrtimer_restart j1939_tp_rxtimer(struct hrtimer *hrtimer)
> * be send as soon as CAN bus is in working state again.
> */
> session->err = -ETIME;
> - j1939_session_deactivate(session);
> + j1939_session_deactivate_activate_next(session);
> } else {
> j1939_session_list_lock(session->priv);
> if (session->state >= J1939_SESSION_ACTIVE &&
> @@ -1414,14 +1475,15 @@ static enum hrtimer_restart j1939_tp_rxtimer(struct hrtimer *hrtimer)
> hrtimer_start(&session->rxtimer,
> ms_to_ktime(J1939_XTP_ABORT_TIMEOUT_MS),
> HRTIMER_MODE_REL_SOFT);
> - __j1939_session_cancel(session, J1939_XTP_ABORT_TIMEOUT);
> - cancelled = true;
> + cancelled =
> + __j1939_session_cancel(session,
> + J1939_XTP_ABORT_TIMEOUT);
> }
> j1939_session_list_unlock(session->priv);
> if (cancelled)
> j1939_session_txtimer_cancel_async(session);
>
> - if (!session->sk)
> + if (cancelled && !session->sk)
> j1939_sk_errqueue(session, J1939_ERRQUEUE_RX_ABORT);
> }
[Severity: High]
Could moving j1939_tp_set_rxtimeout() outside active_session_list_lock in
j1939_session_cancel() race with j1939_tp_rxtimer() here in two ways?
First, in j1939_session_cancel(), __j1939_session_cancel() sets session->state
to J1939_SESSION_WAITING_ABORT before the old rxtimer is cancelled in
j1939_tp_set_rxtimeout(). If the pre-existing rxtimer expires in that window,
j1939_tp_rxtimer() sees:
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);
and immediately deactivates the session instead of waiting for the 500ms abort
timeout.
Second, if j1939_tp_rxtimer() passes that initial check while session->state
is still J1939_SESSION_ACTIVE and spins on j1939_session_list_lock() while
j1939_session_cancel() sets session->state = J1939_SESSION_WAITING_ABORT,
then once j1939_session_cancel() drops the lock, j1939_tp_rxtimer() checks:
if (session->state >= J1939_SESSION_ACTIVE &&
session->state < J1939_SESSION_ACTIVE_MAX)
Since J1939_SESSION_WAITING_ABORT is less than J1939_SESSION_ACTIVE_MAX and
tx_send_shutdown is still false, won't j1939_tp_rxtimer() call
__j1939_session_cancel(session, J1939_XTP_ABORT_TIMEOUT) a second time,
overwriting session->err with EHOSTUNREACH and sending a second ABORT frame?
Also, right above __j1939_session_cancel() in j1939_tp_rxtimer(),
j1939_session_get(session) and hrtimer_start(&session->rxtimer, ...) are
called unconditionally before checking whether __j1939_session_cancel()
returns true. If __j1939_session_cancel() returns false because
session->tx_send_shutdown is set, doesn't that re-arm session->rxtimer with a
new reference on a session that has already shut down?
[ ... ]
> @@ -1852,7 +1916,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);
> }
>
> @@ -1862,7 +1925,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: High]
In j1939_xtp_rx_rts_session_active(), when session->last_cmd != 0, does
removing j1939_session_timers_cancel() leave session->rxtimer armed after the
session is deactivated right below?
if (session->last_cmd != 0) {
...
j1939_session_cancel(session, J1939_XTP_ABORT_BUSY);
if (session->transmission) {
j1939_session_deactivate_activate_next(session);
} else if (session->state == J1939_SESSION_WAITING_ABORT) {
j1939_session_deactivate_activate_next(session);
}
return -EBUSY;
}
Previously, j1939_session_timers_cancel() stopped the timers, and on a
receiver session (!session->transmission) the first j1939_session_cancel() put
the session into J1939_SESSION_WAITING_ABORT so the second
j1939_session_cancel() was a no-op after the second j1939_session_timers_cancel()
cancelled the abort timer.
With j1939_session_timers_cancel() removed, j1939_session_cancel() transitions
the session to J1939_SESSION_WAITING_ABORT and starts session->rxtimer for
500ms, and then j1939_session_deactivate_activate_next() immediately moves the
session to J1939_SESSION_DONE and unlinks it from priv->active_session_list
without cancelling session->rxtimer. Won't that leave session->rxtimer armed
with an active reference on a DONE session?
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1793681851.git.zihanx@nebusec.ai?part=3
next prev parent reply other threads:[~2026-11-03 5:46 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-03 5:24 [PATCH net v5 0/3] can: j1939: fix TX timer reference leak Zihan Xi
2026-11-03 5:24 ` [PATCH net v5 1/3] can: j1939: fix TX timer reference accounting Zihan Xi
2026-11-03 5:48 ` sashiko-bot
2026-11-04 18:38 ` netdev-bot+sashiko
2026-11-03 5:24 ` [PATCH net v5 2/3] can: j1939: cancel TX timer after RX timeout Zihan Xi
2026-11-03 5:54 ` sashiko-bot
2026-11-04 18:38 ` netdev-bot+sashiko
2026-11-03 5:24 ` [PATCH net v5 3/3] can: j1939: serialize session TX with state changes Zihan Xi
2026-11-03 5:46 ` sashiko-bot [this message]
2026-11-04 18:38 ` netdev-bot+sashiko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-194597@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=linux-can@vger.kernel.org \
--cc=mailhol@kernel.org \
--cc=mkl@pengutronix.de \
--cc=o.rempel@pengutronix.de \
--cc=sashiko-reviews@lists.linux.dev \
--cc=socketcan@hartkopp.net \
--cc=zihanx@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox