* [PATCH v9 0/4] net: can: isotp-fixes
@ 2026-07-12 13:44 Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
` (3 more replies)
0 siblings, 4 replies; 7+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, Nico Yip, sashiko-bot
As sashiko-bot was not able to check the second patch this bundle is
re-posted with b4 preparation.
Fixed the remarks reported by sashiko-bot of V8 patchset.
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
Changes in v9:
- patch 3: fix two reported issues and explain why issue 3&4 (-ECOMM sk_err
handling) will not be changed
- Link to v8: https://patch.msgid.link/20260712-isotp-fixes-v8-0-c39220a4e442@hartkopp.net
Changes in v8:
- patch 3: properly terminate the former transmission before starting
- patch 4: formerly patch 5 as the former patch 4 is squashed into patch 3
- Link to v7: https://patch.msgid.link/20260711-isotp-fixes-v7-0-f03af24d8397@hartkopp.net
Changes in v7:
- patch 3: add helper so->tx_gen to monitor new ISOTP_SENDING attempts
- patch 4: prevent missasignment of sk_err report with new so->tx_gen
- Link to v6: https://patch.msgid.link/20260710-isotp-fixes-v6-0-a13c2539093d@hartkopp.net
Changes in v6:
- patch 2: check for rx state and tx state to be IDLE in isotp_bind() (sashiko-bot)
- patch 3: double check rx/tx states after all hrtimer_cancel() calls (sashiko-bot)
move ISOTP_IDLE in err_event_drop for the same reason
- patch 4: use READ_ONCE() to prevent compiler load tearing as remarked by sashiko-bot
- Link to v5: https://patch.msgid.link/20260710-isotp-fixes-v5-0-15174d82d6f1@hartkopp.net
Changes in v5:
- patch 3: new: state change re-check after hrtimer_cancel()
- patch 4: new: handle state transistion with cmpxchg()
- Link to v4: https://patch.msgid.link/20260710-isotp-fixes-v4-0-2a4af437f61b@hartkopp.net
Changes in v4:
- patch 2: use a simpler method to reject a re-binding that can be implemented
inside the lock: No waitqueue waiting just exit.
- Link to v3: https://patch.msgid.link/20260710-isotp-fixes-v3-0-08db68e27d0b@hartkopp.net
Changes in v3:
- patch 2: go back to initial idea without hard resetting the state machine and
timers but let these come to a graceful end. Even a new bind() at netdev
shutdown now leads to this graceful statemachine shutdown via the wait queue.
- Link to v2: https://patch.msgid.link/20260710-isotp-fixes-v2-0-bc57e26594b2@hartkopp.net
Changes in v2:
- patch 2: shutdown state machine to fix sashiko-bot complains
Link: https://lore.kernel.org/linux-can/20260710094807.A50DD1F000E9@smtp.kernel.org/
- Link to v1: https://patch.msgid.link/20260710-isotp-fixes-v1-0-75a1d11d7df9@hartkopp.net
---
Oliver Hartkopp (4):
can: isotp: use unconditional synchronize_rcu() in isotp_release()
can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
can: isotp: fix race between RX/TX timers and frame reception
can: isotp: fix state machine corruption on signal interruption
net/can/isotp.c | 286 +++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 220 insertions(+), 66 deletions(-)
---
base-commit: dd3210c47e8d3ac6b4e9141fc68acc03b38c0ba3
change-id: 20260710-isotp-fixes-ead7480d67bf
Best regards,
--
Oliver Hartkopp <socketcan@hartkopp.net>
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 14:03 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
` (2 subsequent siblings)
3 siblings, 1 reply; 7+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, Nico Yip
From: Oliver Hartkopp <socketcan@hartkopp.net>
isotp_notify() unregisters the (RCU) CAN filters via can_rx_unregister()
and clears so->bound without waiting for a grace period. isotp_release()
uses so->bound to decide whether it needs to call synchronize_rcu()
before cancelling so->rxtimer, so when NETDEV_UNREGISTER runs first it
skips that synchronize_rcu() and can cancel the timer while an
in-flight isotp_rcv() is still executing and about to re-arm it via
isotp_send_fc(), leading to a use-after-free timer callback on the
freed socket.
sakisho-bot remarked a problem with rtnl_lock held in isotp_notify(),
therefore make isotp_release() always call synchronize_rcu() before
cancelling the timers, regardless of so->bound. This still closes the
original race (isotp_notify() clearing so->bound without waiting for
in-flight isotp_rcv() callers before isotp_release() cancels the RX
timer) without adding any RCU wait to the netdevice notifier path.
Fixes: 14a4696bc311 ("can: isotp: isotp_release(): omit unintended hrtimer restart on socket release")
Closes: https://lore.kernel.org/linux-can/20260707085210.6B6C01F000E9@smtp.kernel.org/
Reported-by: Nico Yip <zdi-disclosures@trendmicro.com> (ZDI-CAN-31764)
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
net/can/isotp.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index c48b4a818297..d30937345bcd 100644
--- a/net/can/isotp.c
+++ b/net/can/isotp.c
@@ -1235,15 +1235,22 @@ static int isotp_release(struct socket *sock)
can_rx_unregister(net, dev, so->txid,
SINGLE_MASK(so->txid),
isotp_rcv_echo, sk);
dev_put(dev);
- synchronize_rcu();
}
}
}
+ /* Always wait for a grace period before touching the timers below.
+ * A concurrent NETDEV_UNREGISTER may have already unregistered our
+ * filters and cleared so->bound in isotp_notify() without waiting
+ * for in-flight isotp_rcv() callers to finish, so this call must not
+ * be skipped just because so->bound is already 0 here.
+ */
+ synchronize_rcu();
+
hrtimer_cancel(&so->txfrtimer);
hrtimer_cancel(&so->txtimer);
hrtimer_cancel(&so->rxtimer);
so->ifindex = 0;
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay
3 siblings, 0 replies; 7+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot
From: Oliver Hartkopp <socketcan@hartkopp.net>
isotp_release() looked up the bound network device via dev_get_by_index()
using the stored ifindex. During device unregistration the device is
unlisted from the ifindex hash before the NETDEV_UNREGISTER notifier
chain runs, so a concurrent isotp_release() could find no device, skip
can_rx_unregister() entirely, and still proceed to free the socket.
Since isotp_release() had already removed itself from the isotp
notifier list at that point, isotp_notify() would never get a chance to
clean up either, leaving a stale CAN filter that keeps pointing at the
freed socket.
Fix this the same way raw.c already does: hold a tracked reference to
the bound net_device in the socket (so->dev/so->dev_tracker) from
bind() onward instead of re-resolving it from the ifindex, and
serialize bind()/release() with rtnl_lock() so that so->dev is always
consistent with what the NETDEV_UNREGISTER notifier sees. so->dev
stays valid regardless of ifindex-hash unlisting, and is only ever
cleared by whichever of isotp_release()/isotp_notify() gets there
first, so the filter is always removed exactly once.
isotp_bind() now rejects a (re)bind with -EAGAIN while so->[tx|rx].state
isn't ISOTP_IDLE yet, so a timer left running by a prior
NETDEV_UNREGISTER can't act on a newly bound so->ifindex. Both checks
share the same lock_sock() section, so there is no window in which a
concurrent isotp_notify() clearing so->bound could be missed.
Fixes: e057dd3fc20f ("can: add ISO 15765-2:2016 transport protocol")
Reported-by: sashiko-bot@kernel.org
Link: https://lore.kernel.org/linux-can/20260707101420.47F261F000E9@smtp.kernel.org/
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
net/can/isotp.c | 87 ++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 59 insertions(+), 28 deletions(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index d30937345bcd..44c044eb83e1 100644
--- a/net/can/isotp.c
+++ b/net/can/isotp.c
@@ -150,10 +150,12 @@ struct tpcon {
struct isotp_sock {
struct sock sk;
int bound;
int ifindex;
+ struct net_device *dev;
+ netdevice_tracker dev_tracker;
canid_t txid;
canid_t rxid;
ktime_t tx_gap;
ktime_t lastrxcf_tstamp;
struct hrtimer rxtimer, txtimer, txfrtimer;
@@ -976,10 +978,18 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
err = wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE);
if (err)
goto err_event_drop;
}
+ /* so->bound is only checked once above - a wakeup may have
+ * unbound/rebound the socket meanwhile, so re-validate it
+ */
+ if (!so->bound) {
+ err = -EADDRNOTAVAIL;
+ goto err_out_drop;
+ }
+
/* PDU size > default => try max_pdu_size */
if (size > so->tx.buflen && so->tx.buflen < max_pdu_size) {
u8 *newbuf = kmalloc(max_pdu_size, GFP_KERNEL);
if (newbuf) {
@@ -1217,32 +1227,34 @@ static int isotp_release(struct socket *sock)
spin_lock(&isotp_notifier_lock);
}
list_del(&so->notifier);
spin_unlock(&isotp_notifier_lock);
+ rtnl_lock();
lock_sock(sk);
- /* remove current filters & unregister */
- if (so->bound) {
- if (so->ifindex) {
- struct net_device *dev;
-
- dev = dev_get_by_index(net, so->ifindex);
- if (dev) {
- if (isotp_register_rxid(so))
- can_rx_unregister(net, dev, so->rxid,
- SINGLE_MASK(so->rxid),
- isotp_rcv, sk);
-
- can_rx_unregister(net, dev, so->txid,
- SINGLE_MASK(so->txid),
- isotp_rcv_echo, sk);
- dev_put(dev);
- }
- }
+ /* remove current filters & unregister
+ * tracked reference so->dev is taken at bind() time with rtnl_lock
+ */
+ if (so->bound && so->dev) {
+ if (isotp_register_rxid(so))
+ can_rx_unregister(net, so->dev, so->rxid,
+ SINGLE_MASK(so->rxid),
+ isotp_rcv, sk);
+
+ can_rx_unregister(net, so->dev, so->txid,
+ SINGLE_MASK(so->txid),
+ isotp_rcv_echo, sk);
+ netdev_put(so->dev, &so->dev_tracker);
}
+ so->ifindex = 0;
+ so->bound = 0;
+ so->dev = NULL;
+
+ rtnl_unlock();
+
/* Always wait for a grace period before touching the timers below.
* A concurrent NETDEV_UNREGISTER may have already unregistered our
* filters and cleared so->bound in isotp_notify() without waiting
* for in-flight isotp_rcv() callers to finish, so this call must not
* be skipped just because so->bound is already 0 here.
@@ -1251,13 +1263,10 @@ static int isotp_release(struct socket *sock)
hrtimer_cancel(&so->txfrtimer);
hrtimer_cancel(&so->txtimer);
hrtimer_cancel(&so->rxtimer);
- so->ifindex = 0;
- so->bound = 0;
-
sock_orphan(sk);
sock->sk = NULL;
release_sock(sk);
sock_prot_inuse_add(net, sk->sk_prot, -1);
@@ -1308,17 +1317,29 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l
}
if (!addr->can_ifindex)
return -ENODEV;
+ rtnl_lock();
lock_sock(sk);
if (so->bound) {
err = -EINVAL;
goto out;
}
+ /* A transmission or reception that outlived a previous binding
+ * (unbound by NETDEV_UNREGISTER) may still be draining; the FC/echo
+ * and RX watchdog timers bound how long this takes. Checked together
+ * with so->bound in the same lock_sock() section above, so there is
+ * no window in which a concurrent isotp_notify() could be missed.
+ */
+ if (so->tx.state != ISOTP_IDLE || so->rx.state != ISOTP_IDLE) {
+ err = -EAGAIN;
+ goto out;
+ }
+
/* ensure different CAN IDs when the rx_id is to be registered */
if (isotp_register_rxid(so) && rx_id == tx_id) {
err = -EADDRNOTAVAIL;
goto out;
}
@@ -1327,18 +1348,16 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l
if (!dev) {
err = -ENODEV;
goto out;
}
if (dev->type != ARPHRD_CAN) {
- dev_put(dev);
err = -ENODEV;
- goto out;
+ goto out_put_dev;
}
if (READ_ONCE(dev->mtu) < so->ll.mtu) {
- dev_put(dev);
err = -EINVAL;
- goto out;
+ goto out_put_dev;
}
if (!(dev->flags & IFF_UP))
notify_enetdown = 1;
ifindex = dev->ifindex;
@@ -1352,20 +1371,29 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l
/* register for echo skb's */
can_rx_register(net, dev, tx_id, SINGLE_MASK(tx_id),
isotp_rcv_echo, sk, "isotpe", sk);
- dev_put(dev);
-
/* switch to new settings */
so->ifindex = ifindex;
so->rxid = rx_id;
so->txid = tx_id;
so->bound = 1;
+ /* bind() ok -> hold a reference for so->dev so that isotp_release()
+ * can safely reach the device later, even if a concurrent
+ * NETDEV_UNREGISTER has already unlisted it by ifindex.
+ */
+ so->dev = dev;
+ netdev_hold(so->dev, &so->dev_tracker, GFP_KERNEL);
+
+out_put_dev:
+ /* remove potential reference from dev_get_by_index() */
+ dev_put(dev);
out:
release_sock(sk);
+ rtnl_unlock();
if (notify_enetdown) {
sk->sk_err = ENETDOWN;
if (!sock_flag(sk, SOCK_DEAD))
sk_error_report(sk);
@@ -1564,11 +1592,11 @@ static void isotp_notify(struct isotp_sock *so, unsigned long msg,
struct sock *sk = &so->sk;
if (!net_eq(dev_net(dev), sock_net(sk)))
return;
- if (so->ifindex != dev->ifindex)
+ if (so->dev != dev)
return;
switch (msg) {
case NETDEV_UNREGISTER:
lock_sock(sk);
@@ -1580,14 +1608,16 @@ static void isotp_notify(struct isotp_sock *so, unsigned long msg,
isotp_rcv, sk);
can_rx_unregister(dev_net(dev), dev, so->txid,
SINGLE_MASK(so->txid),
isotp_rcv_echo, sk);
+ netdev_put(so->dev, &so->dev_tracker);
}
so->ifindex = 0;
so->bound = 0;
+ so->dev = NULL;
release_sock(sk);
sk->sk_err = ENODEV;
if (!sock_flag(sk, SOCK_DEAD))
sk_error_report(sk);
@@ -1643,10 +1673,11 @@ static int isotp_init(struct sock *sk)
{
struct isotp_sock *so = isotp_sk(sk);
so->ifindex = 0;
so->bound = 0;
+ so->dev = NULL;
so->opt.flags = CAN_ISOTP_DEFAULT_FLAGS;
so->opt.ext_address = CAN_ISOTP_DEFAULT_EXT_ADDRESS;
so->opt.rx_ext_address = CAN_ISOTP_DEFAULT_EXT_ADDRESS;
so->opt.rxpad_content = CAN_ISOTP_DEFAULT_PAD_CONTENT;
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 13:58 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay
3 siblings, 1 reply; 7+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot
From: Oliver Hartkopp <socketcan@hartkopp.net>
When receiving a Consecutive Frame, Flow Control frame, or local echo
frame, hrtimer_cancel() is called to stop the corresponding watchdog
timer. If the timer handler is already running on another CPU,
hrtimer_cancel() waits for it, and by the time it returns, the timer
handler may have already reported a timeout and reset the state
machine.
so->rx_lock now also covers the TX side: isotp_rcv_echo() takes it
(isotp_rcv_fc()/isotp_rcv_cf() already run under it via isotp_rcv()),
and sendmsg() takes it briefly when claiming a new transfer to bump
so->tx_gen, cancel all three TX timers and clear so->cfecho, all in
the same critical section - isotp_rcv_echo() must never be able to
observe the new generation alongside the old, not yet cleared cfecho
tag, or a stale echo could pass its generation check and corrupt the
new transfer. A concurrent claim cannot get past this point while an
old-generation frame is still being processed under the same lock, so
isotp_rcv_fc()/isotp_rcv_cf() only need a plain state recheck after
hrtimer_cancel().
Two spots additionally need so->tx_gen, because sendmsg()'s initial
claim is a lock-free cmpxchg() to ISOTP_SENDING that can succeed while
the old generation's rx_lock holder is still active:
- isotp_rcv_echo() checks state == ISOTP_SENDING, exactly what a
brand new claim also sets before it ever reaches the lock; so->tx_gen
tells the two apart.
- sendmsg()'s error paths unconditionally wrote so->tx.state =
ISOTP_IDLE; that write is now gated on so->tx_gen too, since a new
claim can succeed while we're still cancelling our own timers.
rx_lock only guarantees a new transfer can't have armed a timer of its
own while its claim is still waiting for the lock; it does not cover
sendmsg()'s later, unlocked hrtimer_start() before can_send(). So the
error paths also recheck so->tx_gen before each individual
hrtimer_cancel() and stop as soon as it no longer matches - at most one
of the three TX timers is ever armed for a given transfer, so the
other two calls are harmless no-ops regardless of the check.
so->tx_gen is otherwise only used by isotp_tx_timer_handler()/
isotp_echo_timer_handler(), which must stay lock-free to avoid
deadlocking against the hrtimer_cancel() calls made under so->rx_lock
elsewhere.
isotp_tx_timeout() still suppresses its ECOMM report on a so->tx_gen
mismatch, so a non-blocking sendmsg() that timed out can lose its only
notification via so->sk_err if a new transfer claims the socket first.
That is accepted as the lesser problem versus tainting an active new
transfer with a stale error.
Fixes: e057dd3fc20f ("can: add ISO 15765-2:2016 transport protocol")
Reported-by: sashiko-bot@kernel.org
Link: https://lore.kernel.org/linux-can/20260710142146.BDAE61F000E9@smtp.kernel.org/
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
net/can/isotp.c | 188 +++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 152 insertions(+), 36 deletions(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index 44c044eb83e1..c4ca8a64e7ce 100644
--- a/net/can/isotp.c
+++ b/net/can/isotp.c
@@ -156,18 +156,19 @@ struct isotp_sock {
netdevice_tracker dev_tracker;
canid_t txid;
canid_t rxid;
ktime_t tx_gap;
ktime_t lastrxcf_tstamp;
- struct hrtimer rxtimer, txtimer, txfrtimer;
+ struct hrtimer rxtimer, txtimer, txfrtimer, echotimer;
struct can_isotp_options opt;
struct can_isotp_fc_options rxfc, txfc;
struct can_isotp_ll_options ll;
u32 frame_txtime;
u32 force_tx_stmin;
u32 force_rx_stmin;
u32 cfecho; /* consecutive frame echo tag */
+ u32 tx_gen; /* generation, bumped per new tx transfer */
struct tpcon rx, tx;
struct list_head notifier;
wait_queue_head_t wait;
spinlock_t rx_lock; /* protect single thread state machine */
};
@@ -376,10 +377,19 @@ static int isotp_rcv_fc(struct isotp_sock *so, struct canfd_frame *cf, int ae)
so->tx.state != ISOTP_WAIT_FIRST_FC)
return 0;
hrtimer_cancel(&so->txtimer);
+ /* isotp_tx_timeout() may have given up on this job while
+ * hrtimer_cancel() above waited for it to finish; so->rx_lock
+ * (held by our caller isotp_rcv()) rules out a concurrent claim,
+ * so a plain recheck is enough here.
+ */
+ if (so->tx.state != ISOTP_WAIT_FC &&
+ so->tx.state != ISOTP_WAIT_FIRST_FC)
+ return 1;
+
if ((cf->len < ae + FC_CONTENT_SZ) ||
((so->opt.flags & ISOTP_CHECK_PADDING) &&
check_pad(so, cf, ae + FC_CONTENT_SZ, so->opt.rxpad_content))) {
/* malformed PDU - report 'not a data message' */
sk->sk_err = EBADMSG;
@@ -422,11 +432,11 @@ static int isotp_rcv_fc(struct isotp_sock *so, struct canfd_frame *cf, int ae)
switch (cf->data[ae] & 0x0F) {
case ISOTP_FC_CTS:
so->tx.bs = 0;
so->tx.state = ISOTP_SENDING;
/* send CF frame and enable echo timeout handling */
- hrtimer_start(&so->txtimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
+ hrtimer_start(&so->echotimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
HRTIMER_MODE_REL_SOFT);
isotp_send_cframe(so);
break;
case ISOTP_FC_WT:
@@ -575,10 +585,18 @@ static int isotp_rcv_cf(struct sock *sk, struct canfd_frame *cf, int ae,
so->lastrxcf_tstamp = skb->tstamp;
}
hrtimer_cancel(&so->rxtimer);
+ /* isotp_rx_timer_handler() may have raced us for so->rx.state
+ * while hrtimer_cancel() above waited for it to finish, already
+ * reporting ETIMEDOUT and resetting the reception; don't process
+ * this CF into a reassembly that has already been given up on.
+ */
+ if (so->rx.state != ISOTP_WAIT_DATA)
+ return 1;
+
/* CFs are never longer than the FF */
if (cf->len > so->rx.ll_dl)
return 1;
/* CFs have usually the LL_DL length */
@@ -868,80 +886,130 @@ static void isotp_create_fframe(struct canfd_frame *cf, struct isotp_sock *so,
static void isotp_rcv_echo(struct sk_buff *skb, void *data)
{
struct sock *sk = (struct sock *)data;
struct isotp_sock *so = isotp_sk(sk);
struct canfd_frame *cf = (struct canfd_frame *)skb->data;
+ u32 gen;
/* only handle my own local echo CF/SF skb's (no FF!) */
- if (skb->sk != sk || so->cfecho != *(u32 *)cf->data)
+ if (skb->sk != sk)
return;
+ /* unlike isotp_rcv_fc()/isotp_rcv_cf(), not already under so->rx_lock
+ * (no isotp_rcv() caller here), so take it ourselves
+ */
+ spin_lock(&so->rx_lock);
+
+ /* so->cfecho may since belong to a new transfer; recheck under lock */
+ if (so->cfecho != *(u32 *)cf->data)
+ goto out_unlock;
+
+ gen = READ_ONCE(so->tx_gen);
+
/* cancel local echo timeout */
- hrtimer_cancel(&so->txtimer);
+ hrtimer_cancel(&so->echotimer);
/* local echo skb with consecutive frame has been consumed */
so->cfecho = 0;
+ /* so->rx_lock stops a new claim from arming a timer of its own
+ * while we're still here, but not from the lock-free cmpxchg()
+ * claim itself: state alone can't tell our SENDING from a new
+ * transfer's, so also gate on gen.
+ */
+ if (so->tx.state != ISOTP_SENDING || READ_ONCE(so->tx_gen) != gen)
+ goto out_unlock;
+
if (so->tx.idx >= so->tx.len) {
/* we are done */
so->tx.state = ISOTP_IDLE;
wake_up_interruptible(&so->wait);
- return;
+ goto out_unlock;
}
if (so->txfc.bs && so->tx.bs >= so->txfc.bs) {
/* stop and wait for FC with timeout */
so->tx.state = ISOTP_WAIT_FC;
hrtimer_start(&so->txtimer, ktime_set(ISOTP_FC_TIMEOUT, 0),
HRTIMER_MODE_REL_SOFT);
- return;
+ goto out_unlock;
}
/* no gap between data frames needed => use burst mode */
if (!so->tx_gap) {
/* enable echo timeout handling */
- hrtimer_start(&so->txtimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
+ hrtimer_start(&so->echotimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
HRTIMER_MODE_REL_SOFT);
isotp_send_cframe(so);
- return;
+ goto out_unlock;
}
/* start timer to send next consecutive frame with correct delay */
hrtimer_start(&so->txfrtimer, so->tx_gap, HRTIMER_MODE_REL_SOFT);
+
+out_unlock:
+ spin_unlock(&so->rx_lock);
}
-static enum hrtimer_restart isotp_tx_timer_handler(struct hrtimer *hrtimer)
+/* shared by so->txtimer's and so->echotimer's callbacks. Both timers get
+ * cancelled under so->rx_lock elsewhere, so this must stay lock-free to
+ * avoid deadlocking with that; uses so->tx_gen instead to avoid tainting
+ * a new transfer with an error from the one that just timed out.
+ */
+static enum hrtimer_restart isotp_tx_timeout(struct isotp_sock *so)
{
- struct isotp_sock *so = container_of(hrtimer, struct isotp_sock,
- txtimer);
struct sock *sk = &so->sk;
+ u32 gen = READ_ONCE(so->tx_gen);
+ u32 old_state = READ_ONCE(so->tx.state);
/* don't handle timeouts in IDLE or SHUTDOWN state */
- if (so->tx.state == ISOTP_IDLE || so->tx.state == ISOTP_SHUTDOWN)
+ if (old_state == ISOTP_IDLE || old_state == ISOTP_SHUTDOWN)
+ return HRTIMER_NORESTART;
+
+ /* only claim the timeout if the state is still unchanged */
+ if (cmpxchg(&so->tx.state, old_state, ISOTP_IDLE) != old_state)
return HRTIMER_NORESTART;
/* we did not get any flow control or echo frame in time */
- /* report 'communication error on send' */
- sk->sk_err = ECOMM;
- if (!sock_flag(sk, SOCK_DEAD))
- sk_error_report(sk);
+ if (READ_ONCE(so->tx_gen) == gen) {
+ /* report 'communication error on send' */
+ sk->sk_err = ECOMM;
+ if (!sock_flag(sk, SOCK_DEAD))
+ sk_error_report(sk);
+ }
- /* reset tx state */
- so->tx.state = ISOTP_IDLE;
wake_up_interruptible(&so->wait);
return HRTIMER_NORESTART;
}
+/* so->txtimer: fires when a Flow Control frame does not arrive in time */
+static enum hrtimer_restart isotp_tx_timer_handler(struct hrtimer *hrtimer)
+{
+ struct isotp_sock *so = container_of(hrtimer, struct isotp_sock,
+ txtimer);
+
+ return isotp_tx_timeout(so);
+}
+
+/* so->echotimer: fires when a sent CF/SF's local echo does not arrive */
+static enum hrtimer_restart isotp_echo_timer_handler(struct hrtimer *hrtimer)
+{
+ struct isotp_sock *so = container_of(hrtimer, struct isotp_sock,
+ echotimer);
+
+ return isotp_tx_timeout(so);
+}
+
static enum hrtimer_restart isotp_txfr_timer_handler(struct hrtimer *hrtimer)
{
struct isotp_sock *so = container_of(hrtimer, struct isotp_sock,
txfrtimer);
/* start echo timeout handling and cover below protocol error */
- hrtimer_start(&so->txtimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
+ hrtimer_start(&so->echotimer, ktime_set(ISOTP_ECHO_TIMEOUT, 0),
HRTIMER_MODE_REL_SOFT);
/* cfecho should be consumed by isotp_rcv_echo() here */
if (so->tx.state == ISOTP_SENDING && !so->cfecho)
isotp_send_cframe(so);
@@ -958,12 +1026,14 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
struct net_device *dev;
struct canfd_frame *cf;
int ae = (so->opt.flags & CAN_ISOTP_EXTEND_ADDR) ? 1 : 0;
int wait_tx_done = (so->opt.flags & CAN_ISOTP_WAIT_TX_DONE) ? 1 : 0;
s64 hrtimer_sec = ISOTP_ECHO_TIMEOUT;
+ struct hrtimer *tx_hrt = &so->echotimer;
int off;
int err;
+ u32 gen;
if (!so->bound || so->tx.state == ISOTP_SHUTDOWN)
return -EADDRNOTAVAIL;
while (cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) != ISOTP_IDLE) {
@@ -978,10 +1048,28 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
err = wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE);
if (err)
goto err_event_drop;
}
+ /* new transfer: bump the generation (gen is used below and by
+ * isotp_tx_timeout()/isotp_rcv_echo()) and drain the old one -
+ * cancel its timers ourselves in case one is still concurrently
+ * completing it, and take so->rx_lock so no isotp_rcv_fc()/
+ * isotp_rcv_echo() call for it is still in flight. The bump and
+ * the so->cfecho reset happen in the same so->rx_lock section so
+ * isotp_rcv_echo() can never observe the new gen alongside the
+ * old, not-yet-cleared cfecho tag.
+ */
+ spin_lock_bh(&so->rx_lock);
+ gen = READ_ONCE(so->tx_gen) + 1;
+ WRITE_ONCE(so->tx_gen, gen);
+ hrtimer_cancel(&so->txtimer);
+ hrtimer_cancel(&so->echotimer);
+ hrtimer_cancel(&so->txfrtimer);
+ so->cfecho = 0;
+ spin_unlock_bh(&so->rx_lock);
+
/* so->bound is only checked once above - a wakeup may have
* unbound/rebound the socket meanwhile, so re-validate it
*/
if (!so->bound) {
err = -EADDRNOTAVAIL;
@@ -1098,17 +1186,18 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
/* standard flow control check */
so->tx.state = ISOTP_WAIT_FIRST_FC;
/* start timeout for FC */
hrtimer_sec = ISOTP_FC_TIMEOUT;
+ tx_hrt = &so->txtimer;
/* no CF echo tag for isotp_rcv_echo() (FF-mode) */
so->cfecho = 0;
}
}
- hrtimer_start(&so->txtimer, ktime_set(hrtimer_sec, 0),
+ hrtimer_start(tx_hrt, ktime_set(hrtimer_sec, 0),
HRTIMER_MODE_REL_SOFT);
/* send the first or only CAN frame */
cf->flags = so->ll.tx_flags;
@@ -1118,17 +1207,17 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
dev_put(dev);
if (err) {
pr_notice_once("can-isotp: %s: can_send_ret %pe\n",
__func__, ERR_PTR(err));
- /* no transmission -> no timeout monitoring */
- hrtimer_cancel(&so->txtimer);
-
- /* reset consecutive frame echo tag */
- so->cfecho = 0;
-
- goto err_out_drop;
+ spin_lock_bh(&so->rx_lock);
+ /* no transmission -> no timeout monitoring; skip if a new
+ * claim has since taken over so we don't cancel its timer
+ */
+ if (READ_ONCE(so->tx_gen) == gen)
+ hrtimer_cancel(tx_hrt);
+ goto err_out_drop_locked;
}
if (wait_tx_done) {
/* wait for complete transmission of current pdu */
err = wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE);
@@ -1140,18 +1229,40 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
return err;
}
return size;
+err_out_drop:
+ spin_lock_bh(&so->rx_lock);
+ goto err_out_drop_locked;
err_event_drop:
- /* got signal: force tx state machine to be idle */
- so->tx.state = ISOTP_IDLE;
+ /* got signal: cancel timers one at a time under so->rx_lock,
+ * rechecking gen after each - a mismatch means our own transfer
+ * already concluded on its own (leaving nothing else of ours
+ * armed), so stop before a further cancel can hit a new transfer's
+ * timer instead
+ */
+ spin_lock_bh(&so->rx_lock);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txfrtimer);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txtimer);
-err_out_drop:
- /* drop this PDU and unlock a potential wait queue */
- so->tx.state = ISOTP_IDLE;
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
+ hrtimer_cancel(&so->echotimer);
+err_out_drop_locked:
+ /* so->rx_lock stops a new claim from cancelling anything of its own
+ * while we're still here, but not from the lock-free cmpxchg()
+ * claim itself; only drop our own PDU if that hasn't happened
+ */
+ if (READ_ONCE(so->tx_gen) == gen) {
+ so->cfecho = 0;
+ so->tx.state = ISOTP_IDLE;
+ }
+ spin_unlock_bh(&so->rx_lock);
wake_up_interruptible(&so->wait);
return err;
}
@@ -1261,10 +1372,11 @@ static int isotp_release(struct socket *sock)
*/
synchronize_rcu();
hrtimer_cancel(&so->txfrtimer);
hrtimer_cancel(&so->txtimer);
+ hrtimer_cancel(&so->echotimer);
hrtimer_cancel(&so->rxtimer);
sock_orphan(sk);
sock->sk = NULL;
@@ -1700,14 +1812,18 @@ static int isotp_init(struct sock *sk)
so->rx.buf = so->rx.sbuf;
so->tx.buf = so->tx.sbuf;
so->rx.buflen = ARRAY_SIZE(so->rx.sbuf);
so->tx.buflen = ARRAY_SIZE(so->tx.sbuf);
- hrtimer_setup(&so->rxtimer, isotp_rx_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
- hrtimer_setup(&so->txtimer, isotp_tx_timer_handler, CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
- hrtimer_setup(&so->txfrtimer, isotp_txfr_timer_handler, CLOCK_MONOTONIC,
- HRTIMER_MODE_REL_SOFT);
+ hrtimer_setup(&so->rxtimer, isotp_rx_timer_handler,
+ CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
+ hrtimer_setup(&so->txtimer, isotp_tx_timer_handler,
+ CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
+ hrtimer_setup(&so->echotimer, isotp_echo_timer_handler,
+ CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
+ hrtimer_setup(&so->txfrtimer, isotp_txfr_timer_handler,
+ CLOCK_MONOTONIC, HRTIMER_MODE_REL_SOFT);
init_waitqueue_head(&so->wait);
spin_lock_init(&so->rx_lock);
spin_lock(&isotp_notifier_lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
` (2 preceding siblings ...)
2026-07-12 13:44 ` [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
3 siblings, 0 replies; 7+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde
From: Oliver Hartkopp <socketcan@hartkopp.net>
If a thread blocked in isotp_sendmsg() waiting for a previous transfer
to finish is interrupted by a signal, jumping to the cleanup labels
incorrectly cancels active timers and forces 'so->tx.state' back to
ISOTP_IDLE. Since this thread never owned the active transmission, it
abruptly aborts another thread's legitimate transfer, so return the
interruption error directly.
Fixes: e057dd3fc20f ("can: add ISO 15765-2:2016 transport protocol")
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
net/can/isotp.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index c4ca8a64e7ce..56618ea1dae4 100644
--- a/net/can/isotp.c
+++ b/net/can/isotp.c
@@ -1045,11 +1045,11 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
return -EADDRNOTAVAIL;
/* wait for complete transmission of current pdu */
err = wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE);
if (err)
- goto err_event_drop;
+ return err;
}
/* new transfer: bump the generation (gen is used below and by
* isotp_tx_timeout()/isotp_rcv_echo()) and drain the old one -
* cancel its timers ourselves in case one is still concurrently
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception
2026-07-12 13:44 ` [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:58 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-12 13:58 UTC (permalink / raw)
To: Oliver Hartkopp
Cc: Oliver Hartkopp, linux-can, Vincent Mailhol, Oleksij Rempel,
Marc Kleine-Budde
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] The lock-free state claim mechanism in isotp_sendmsg() races with the generation bump, causing state clobbering that leads to concurrent buffer corruption and an Out-Of-Bounds (OOB) read leaking kernel memory to the CAN bus.
--
--- Patch [3]: [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception ---
commit b972a6bcac8d8b55930b5c1372f223dd7d9a46de
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: fix race between RX/TX timers and frame reception
This commit attempts to fix a race by introducing a generation counter
(tx_gen) to distinguish between old and new TX transfers. It also expands
rx_lock to cover the TX side for proper synchronization.
Link: https://lore.kernel.org/linux-can/20260710142146.BDAE61F000E9@smtp.kernel.org/
> diff --git a/net/can/isotp.c b/net/can/isotp.c
> index 44c044eb83e12..c4ca8a64e7ce1 100644
> --- a/net/can/isotp.c
> +++ b/net/can/isotp.c
> @@ -870,22 +888,42 @@ static void isotp_rcv_echo(struct sk_buff *skb, void *data)
[ ... ]
> + /* so->rx_lock stops a new claim from arming a timer of its own
> + * while we're still here, but not from the lock-free cmpxchg()
> + * claim itself: state alone can't tell our SENDING from a new
> + * transfer's, so also gate on gen.
> + */
> + if (so->tx.state != ISOTP_SENDING || READ_ONCE(so->tx_gen) != gen)
> + goto out_unlock;
[Severity: Critical]
Does the echo receiver similarly validate against a stale tx_gen if a new
transfer is blocked waiting for rx_lock?
If the generation hasn't been bumped yet, it appears this could process echoes
for the wrong transfer and inadvertently clobber its state.
> @@ -980,6 +1050,24 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
> goto err_event_drop;
> }
>
> + /* new transfer: bump the generation (gen is used below and by
> + * isotp_tx_timeout()/isotp_rcv_echo()) and drain the old one -
> + * cancel its timers ourselves in case one is still concurrently
> + * completing it, and take so->rx_lock so no isotp_rcv_fc()/
> + * isotp_rcv_echo() call for it is still in flight. The bump and
> + * the so->cfecho reset happen in the same so->rx_lock section so
> + * isotp_rcv_echo() can never observe the new gen alongside the
> + * old, not-yet-cleared cfecho tag.
> + */
> + spin_lock_bh(&so->rx_lock);
> + gen = READ_ONCE(so->tx_gen) + 1;
> + WRITE_ONCE(so->tx_gen, gen);
[Severity: Critical]
Because cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) happens before
this lock is acquired, is there a window where a new transfer has claimed the
state but hasn't bumped tx_gen yet?
> @@ -1142,14 +1231,36 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
>
> return size;
>
> +err_out_drop:
> + spin_lock_bh(&so->rx_lock);
> + goto err_out_drop_locked;
> err_event_drop:
[ ... ]
> +err_out_drop_locked:
> + /* so->rx_lock stops a new claim from cancelling anything of its own
> + * while we're still here, but not from the lock-free cmpxchg()
> + * claim itself; only drop our own PDU if that hasn't happened
> + */
> + if (READ_ONCE(so->tx_gen) == gen) {
> + so->cfecho = 0;
> + so->tx.state = ISOTP_IDLE;
> + }
> + spin_unlock_bh(&so->rx_lock);
> wake_up_interruptible(&so->wait);
>
> return err;
[Severity: Critical]
Can this lock-free claim in isotp_sendmsg() race with the generation bump and
cause state clobbering?
If Thread A is in err_event_drop() and holds rx_lock, and Thread C (a timeout
timer executing isotp_tx_timeout()) locklessly sets the state to ISOTP_IDLE:
Thread C (isotp_tx_timeout)
cmpxchg(&so->tx.state, old_state, ISOTP_IDLE);
Then Thread B calls isotp_sendmsg(), sees ISOTP_IDLE, claims it to
ISOTP_SENDING locklessly, and blocks on rx_lock:
Thread B (isotp_sendmsg)
cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING);
...
spin_lock_bh(&so->rx_lock); // blocks
When Thread A resumes in err_out_drop_locked, it checks tx_gen. Since Thread
B is blocked, tx_gen hasn't been bumped yet. Will Thread A then incorrectly
pass the check and reset the state to ISOTP_IDLE?
Thread A (err_out_drop_locked)
if (READ_ONCE(so->tx_gen) == gen) {
so->tx.state = ISOTP_IDLE;
}
Once Thread A drops the lock, Thread B will bump tx_gen and start writing to
tx.buf. But because the state is now ISOTP_IDLE, Thread D could successfully
call isotp_sendmsg() and concurrently claim the socket.
Could this lead to Threads B and D concurrently modifying tx.idx in
isotp_fill_dataframe(), causing an out-of-bounds read that leaks adjacent
kernel memory over the CAN bus?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v9-0-137840d7f0f4@hartkopp.net?part=3
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
@ 2026-07-12 14:03 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-12 14:03 UTC (permalink / raw)
To: Oliver Hartkopp
Cc: Marc Kleine-Budde, Vincent Mailhol, Oleksij Rempel,
Oliver Hartkopp, linux-can
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] Unconditional `synchronize_rcu()` in `isotp_release()` can be triggered by unprivileged users via unbound sockets.
Pre-existing issues:
- [High] Concurrent isotp_sendmsg() callers can forcibly abort another thread's active transmission upon receiving a signal.
--
--- Patch [1]: [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() ---
commit d8a6dd60eabcde6d6f502a2471ec7e04cf918503
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: use unconditional synchronize_rcu() in isotp_release()
This patch makes isotp_release() always call synchronize_rcu() before
cancelling timers to close a race condition when a concurrent
NETDEV_UNREGISTER clears so->bound.
Link: https://lore.kernel.org/linux-can/20260707085210.6B6C01F000E9@smtp.kernel.org/
> diff --git a/net/can/isotp.c b/net/can/isotp.c
> index c48b4a818297e..d30937345bcdc 100644
> --- a/net/can/isotp.c
> +++ b/net/can/isotp.c
[ ... ]
> @@ -1237,11 +1237,18 @@ static int isotp_release(struct socket *sock)
> SINGLE_MASK(so->txid),
> isotp_rcv_echo, sk);
> dev_put(dev);
> - synchronize_rcu();
> }
> }
> }
>
> + /* Always wait for a grace period before touching the timers below.
> + * A concurrent NETDEV_UNREGISTER may have already unregistered our
> + * filters and cleared so->bound in isotp_notify() without waiting
> + * for in-flight isotp_rcv() callers to finish, so this call must not
> + * be skipped just because so->bound is already 0 here.
> + */
> + synchronize_rcu();
> +
[Severity: Medium]
Could this unconditional synchronize_rcu() be triggered rapidly by
unprivileged users?
If an unprivileged user creates an unbound CAN_ISOTP socket and closes it
immediately, it appears it will now hit this synchronize_rcu() in
isotp_release() because so->bound is no longer checked.
Might a rapid loop of socket creation and destruction allow local
unprivileged users to degrade system performance by constantly blocking for
RCU grace periods?
This is a pre-existing issue, but I noticed a potential race condition in
isotp_sendmsg() when handling signals.
[Severity: High]
Can a thread interrupted by a signal forcibly abort another thread's active
transmission?
If multiple threads call isotp_sendmsg() on the same socket, the function
uses cmpxchg() to gain exclusivity over the TX state machine. If a thread
fails the cmpxchg and waits in wait_event_interruptible(), but is then
interrupted by a signal, it jumps to err_event_drop:
net/can/isotp.c:isotp_sendmsg() {
...
err_event_drop:
/* got signal: force tx state machine to be idle */
so->tx.state = ISOTP_IDLE;
hrtimer_cancel(&so->txfrtimer);
hrtimer_cancel(&so->txtimer);
...
}
Does this forcible state reset to ISOTP_IDLE and timer cancellation happen
even though the interrupted thread does not own the state machine? This
appears to abort the active transmission and could allow a third thread to
pass the cmpxchg, leading to concurrent execution of isotp_sendmsg() on the
same so->tx variables.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v9-0-137840d7f0f4@hartkopp.net?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-07-12 14:03 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-12 14:03 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay
2026-07-12 13:58 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox