* [PATCH v9 0/4] net: can: isotp-fixes
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 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] 12+ messages in thread* [PATCH v9 0/4] net: can: isotp-fixes @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay 0 siblings, 0 replies; 12+ 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] 12+ messages in thread
* [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay -1 siblings, 0 replies; 12+ messages in thread From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw) To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, Nico Yip 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] 12+ messages in thread
* [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay 0 siblings, 0 replies; 12+ 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] 12+ messages in thread
* Re: [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay (?) @ 2026-07-12 14:03 ` sashiko-bot -1 siblings, 0 replies; 12+ 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] 12+ messages in thread
* [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay -1 siblings, 0 replies; 12+ messages in thread From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw) To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot 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] 12+ messages in thread
* [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay 0 siblings, 0 replies; 12+ 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] 12+ messages in thread
* [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay -1 siblings, 0 replies; 12+ messages in thread From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw) To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot 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] 12+ messages in thread
* [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay 0 siblings, 0 replies; 12+ 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] 12+ messages in thread
* Re: [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay (?) @ 2026-07-12 13:58 ` sashiko-bot -1 siblings, 0 replies; 12+ 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] 12+ messages in thread
* [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay -1 siblings, 0 replies; 12+ messages in thread From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw) To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde 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] 12+ messages in thread
* [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption @ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay 0 siblings, 0 replies; 12+ 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] 12+ messages in thread
end of thread, other threads:[~2026-07-12 14:03 UTC | newest] Thread overview: 12+ 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 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 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 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 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 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
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.