* [PATCH v8 0/4] net: can: isotp-fixes
@ 2026-07-12 12:41 Oliver Hartkopp via B4 Relay
2026-07-12 12:41 ` [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 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 V7 patchset.
Complete concept change than former fix approach by using so->rx_lock
for the cfecho and in sendmsg to properly shutdown and drain the former
(maybe faulty) PDU transmission before starting a new sending attempt.
Signed-off-by: Oliver Hartkopp <socketcan@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 | 271 ++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 206 insertions(+), 65 deletions(-)
---
base-commit: dd3210c47e8d3ac6b4e9141fc68acc03b38c0ba3
change-id: 20260710-isotp-fixes-ead7480d67bf
Best regards,
--
Oliver Hartkopp <socketcan@hartkopp.net>
^ permalink raw reply [flat|nested] 9+ messages in thread* [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-12 12:41 [PATCH v8 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 ` Oliver Hartkopp via B4 Relay 2026-07-12 12:54 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 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; 9+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 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] 9+ messages in thread
* Re: [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-12 12:41 ` [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-12 12:54 ` sashiko-bot 0 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-07-12 12:54 UTC (permalink / raw) To: Oliver Hartkopp Cc: Vincent Mailhol, Marc Kleine-Budde, linux-can, Oliver Hartkopp, Oleksij Rempel Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider: Pre-existing issues: - [Critical] Race condition between isotp_sendmsg() error paths and isotp_rcv_fc() allows leaking uninitialized kernel heap memory over the CAN bus. - [High] CAN filter leak due to a race condition between socket release and netdevice unregistration. - [High] State Machine Leak and Cross-Interface Data Leakage on NETDEV_UNREGISTER. - [High] Lockless updates to the TX state machine in isotp_sendmsg() race with concurrent CAN frame reception. - [Medium] Missing Error Handling in isotp_bind() ignores can_rx_register() failures. -- --- Patch [1]: [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() --- commit 6e5426def22a6e6426722ba05c1e7413dd0ccd86 Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: use unconditional synchronize_rcu() in isotp_release() isotp_notify() unregisters the (RCU) CAN filters and clears so->bound without waiting for a grace period. This commit updates isotp_release() to unconditionally call synchronize_rcu() before cancelling timers, fixing a race condition where NETDEV_UNREGISTER could lead to a use-after-free timer callback on the freed socket. 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(); > } > } > } [Severity: High] This is a pre-existing issue, but does a race condition exist between socket release and netdevice unregistration that can leak filters? If isotp_release() runs during the window where unregister_netdevice_many() has unlisted the device from the IDR but before notifiers are invoked: net/can/isotp.c:isotp_release() { ... list_del(&so->notifier); ... dev = dev_get_by_index(net, so->ifindex); ... } Because the socket is unlinked from the notifier list, the notifier won't clean it up. However, dev_get_by_index() returns NULL since the device is unlisted, causing isotp_release() to silently skip calling can_rx_unregister() and permanently leak the receiver memory. > + /* 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); [Severity: High] This isn't a bug introduced by this patch, but does the NETDEV_UNREGISTER handler fail to stop active state machines? When the unregister event is handled: net/can/isotp.c:isotp_notify() { ... case NETDEV_UNREGISTER: lock_sock(sk); /* remove current filters & unregister */ ... } The code clears so->bound and so->ifindex but omits explicitly aborting active state machines (so->tx.state) or cancelling tx timers. If a thread is blocked in isotp_sendmsg(), it remains hung until a timer expires. Additionally, if userspace calls bind() on a new interface, the state isn't reset, and when the active timer fires, it could transmit remaining frames of the old PDU out of the newly bound network interface. [Severity: Medium] This is a pre-existing issue, but does isotp_bind() ignore filter registration failures? net/can/isotp.c:isotp_bind() { ... if (isotp_register_rxid(so)) can_rx_register(net, dev, rx_id, SINGLE_MASK(rx_id), isotp_rcv, sk, "isotp", sk); ... } If can_rx_register() fails with -ENOMEM, isotp_bind() ignores the return value, sets so->bound = 1, and returns 0. The socket then silently fails to receive or echo frames, and later isotp_release() will trigger a warning splat when attempting to unregister the missing filter. [Severity: High] This isn't a bug introduced by this patch, but is there a race with concurrent CAN frame reception when updating the state locklessly? net/can/isotp.c:isotp_sendmsg() { ... so->tx.state = ISOTP_WAIT_FIRST_FC; ... } If a spoofed or leftover Flow Control frame is received concurrently on another CPU: net/can/isotp.c:isotp_rcv_fc() { ... if (so->tx.state != ISOTP_WAIT_FC && so->tx.state != ISOTP_WAIT_FIRST_FC) ... } The softirq path can see ISOTP_WAIT_FIRST_FC and immediately transition to ISOTP_SENDING, transmitting a Consecutive Frame before isotp_sendmsg() has sent the First Frame, scrambling the protocol state. [Severity: Critical] This is a pre-existing issue, but can the signal error path in isotp_sendmsg() leak uninitialized kernel memory? If isotp_sendmsg() is interrupted by a signal, it sets the state to idle: net/can/isotp.c:isotp_sendmsg() { ... so->tx.state = ISOTP_IDLE; hrtimer_cancel(&so->txfrtimer); ... } If a concurrent isotp_rcv_fc() already passed the state check, and userspace immediately calls sendmsg() again with a large size: net/can/isotp.c:isotp_sendmsg() { ... u8 *newbuf = kmalloc(max_pdu_size, GFP_KERNEL); ... } The delayed isotp_rcv_fc() could resume, transition the state, and transmit the newly allocated, uninitialized so->tx.buf over the CAN bus before the data has been copied from userspace. -- Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v8-0-c39220a4e442@hartkopp.net?part=1 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-12 12:41 [PATCH v8 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-12 12:41 ` [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 ` Oliver Hartkopp via B4 Relay 2026-07-12 12:59 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay 2026-07-12 12:41 ` [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay 3 siblings, 1 reply; 9+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 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] 9+ messages in thread
* Re: [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-12 12:41 ` [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay @ 2026-07-12 12:59 ` sashiko-bot 0 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-07-12 12:59 UTC (permalink / raw) To: Oliver Hartkopp Cc: linux-can, Vincent Mailhol, Oliver Hartkopp, Oleksij Rempel, Marc Kleine-Budde Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: Pre-existing issues: - [Critical] A TOCTOU race condition in `isotp_sendmsg` allows concurrent modification of socket options, leading to an SKB heap overflow and `skb_over_panic`. - [High] The check in `isotp_bind()` to prevent rebinding during an active state machine does not fully prevent cross-interface packet injection from RCU-delayed softirqs. -- --- Patch [2]: [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER --- commit 508c0cfe152e6936d33176beb93ef03692ea4ea7 Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER This tracks the bound net_device with a reference instead of re-resolving it from ifindex during isotp_release(). It also rejects isotp_bind() with -EAGAIN if the state machines aren't idle yet, avoiding timers acting on a newly bound interface. Link: https://lore.kernel.org/linux-can/20260707101420.47F261F000E9@smtp.kernel.org/ > diff --git a/net/can/isotp.c b/net/can/isotp.c > index d30937345bcdc..44c044eb83e12 100644 > --- a/net/can/isotp.c > +++ b/net/can/isotp.c > @@ -978,6 +980,14 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) > 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; > + } [Severity: Critical] This is a pre-existing issue, but does a time-of-check to time-of-use race still exist here if sock_alloc_send_skb() blocks later in isotp_sendmsg()? If sock_alloc_send_skb(sk, so->ll.mtu) blocks during allocation, a concurrent NETDEV_UNREGISTER could trigger isotp_notify(), which acquires lock_sock() and sets so->bound = 0. Userspace could then call setsockopt(CAN_ISOTP_LL_OPTS), which passes its !so->bound check and modifies so->ll.mtu and so->tx.ll_dl. When isotp_sendmsg() resumes, it has a smaller SKB but could use the new larger MTU, or isotp_fill_dataframe() could use the new so->tx.ll_dl to copy too many bytes into the small SKB: net/can/isotp.c:isotp_fill_dataframe() { ... for (i = 0; i < num; i++) cf->data[pcilen + i] = so->tx.buf[so->tx.idx++]; ... } Could this lead to a heap out-of-bounds write overwriting skb_shared_info? [ ... ] > @@ -1317,6 +1327,17 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l > 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; > + } [Severity: High] This is a pre-existing issue, but does this state machine check fully prevent cross-interface packet injection from RCU-delayed softirqs? If a packet arrives on an old device and is queued via can_rx_register()'s softirq, and concurrently the device is unregistered (clearing so->bound), userspace could call bind() for a new device. Since isotp_rcv() hasn't acquired so->rx_lock to change the state yet, the lockless check here passes and so->dev is updated. Could the delayed softirq then acquire so->rx_lock, transition the state, and send a Flow Control frame using the newly bound so->ifindex? Would isotp_rcv() need to verify that skb->dev matches the currently bound so->dev after acquiring so->rx_lock? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v8-0-c39220a4e442@hartkopp.net?part=2 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception 2026-07-12 12:41 [PATCH v8 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-12 12:41 ` [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay 2026-07-12 12:41 ` [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 ` Oliver Hartkopp via B4 Relay 2026-07-12 12:53 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay 3 siblings, 1 reply; 9+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 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 cancel all three TX timers and clear so->cfecho itself. 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. The use of rx_lock guarantees a new transfer can't have armed anything of its own yet while we hold it. 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. 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 | 173 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 138 insertions(+), 35 deletions(-) diff --git a/net/can/isotp.c b/net/can/isotp.c index 44c044eb83e1..c5b44d298618 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,26 @@ 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. + */ + gen = READ_ONCE(so->tx_gen) + 1; + WRITE_ONCE(so->tx_gen, gen); + + spin_lock_bh(&so->rx_lock); + 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 +1184,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 +1205,14 @@ 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)); + spin_lock_bh(&so->rx_lock); /* no transmission -> no timeout monitoring */ - hrtimer_cancel(&so->txtimer); - - /* reset consecutive frame echo tag */ - so->cfecho = 0; - - goto err_out_drop; + 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 +1224,32 @@ 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 under so->rx_lock, held through + * err_out_drop_locked below, so a concurrent claim's own drain + * step can't proceed until we're done + */ + spin_lock_bh(&so->rx_lock); hrtimer_cancel(&so->txfrtimer); hrtimer_cancel(&so->txtimer); -err_out_drop: - /* drop this PDU and unlock a potential wait queue */ - so->tx.state = ISOTP_IDLE; + 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 +1359,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 +1799,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] 9+ messages in thread
* Re: [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception 2026-07-12 12:41 ` [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay @ 2026-07-12 12:53 ` sashiko-bot 0 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-07-12 12:53 UTC (permalink / raw) To: Oliver Hartkopp Cc: Oleksij Rempel, Oliver Hartkopp, Marc Kleine-Budde, Vincent Mailhol, linux-can Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: New issues: - [High] Error paths in `isotp_sendmsg()` unconditionally cancel hrtimers, which can inadvertently destroy the active timers of a newly started concurrent transfer. - [High] A delayed local echo frame from a previous transfer can bypass the `tx_gen` validation and corrupt a newly started transfer due to a TOCTOU race with `so->cfecho`. - [High] The timeout handler incorrectly suppresses the socket timeout error (`ECOMM`) if a concurrent transfer claims the socket, causing the timed-out `sendmsg()` to silently drop data and falsely report success. Pre-existing issues: - [High] Concurrent `sendmsg` calls cause the active sender to hang waiting for a new sender to finish, leading to hangs or cross-transfer status leaks. -- --- Patch [3]: [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception --- commit eb4cbaa1b7868a1c6cac62f072196eefb97302d2 Author: Oliver Hartkopp <socketcan@hartkopp.net> Subject: can: isotp: fix race between RX/TX timers and frame reception This commit introduces so->rx_lock to protect the TX state machine against concurrent hrtimer execution and frame reception. It also adds a tx_gen generation counter to safely distinguish between old and new TX transfers when cancelling timers or processing echo frames locklessly. 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..c5b44d298618b 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) > 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); [Severity: High] Can a delayed echo frame from a previous transfer bypass the validation and corrupt a newly started transfer here? If transfer 1 times out, it leaves its echo tag in so->cfecho. If transfer 2 then claims the socket, it locklessly increments so->tx_gen in isotp_sendmsg() before acquiring so->rx_lock to clear so->cfecho. If a delayed echo for transfer 1 arrives in that small window, isotp_rcv_echo() will acquire so->rx_lock, match the old so->cfecho (which hasn't been cleared yet), and read the new so->tx_gen. Does this allow the stale echo to pass the generation check below and disrupt the protocol state machine of the new transfer? > > /* 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; [ ... ] > -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); > + } [Severity: High] Does this suppress the socket timeout error if a concurrent transfer claims the socket right after the state transitions to ISOTP_IDLE? Once cmpxchg() sets so->tx.state to ISOTP_IDLE, a concurrent thread calling isotp_sendmsg() can immediately claim the state and increment so->tx_gen. If that happens before the READ_ONCE(so->tx_gen) == gen check, the error reporting is skipped. Does this cause the original timed-out isotp_sendmsg() call to falsely report success to userspace instead of returning an error? > > - /* reset tx state */ > - so->tx.state = ISOTP_IDLE; > wake_up_interruptible(&so->wait); > > return HRTIMER_NORESTART; > } [ ... ] > @@ -1120,13 +1207,10 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) > pr_notice_once("can-isotp: %s: can_send_ret %pe\n", > __func__, ERR_PTR(err)); > > + spin_lock_bh(&so->rx_lock); > /* no transmission -> no timeout monitoring */ > - hrtimer_cancel(&so->txtimer); > - > - /* reset consecutive frame echo tag */ > - so->cfecho = 0; > - > - goto err_out_drop; > + hrtimer_cancel(tx_hrt); [Severity: High] Can this unconditionally cancel the timer of a newly started concurrent transfer? If can_send() fails, this path cancels tx_hrt before verifying ownership via tx_gen. If a concurrent sendmsg() has already claimed the socket and started its own timers, will this cancel the new transfer's timers and cause it to hang indefinitely without timeout monitoring? > + goto err_out_drop_locked; > } > > if (wait_tx_done) { > @@ -1142,14 +1226,28 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) > > return size; [Severity: High] This isn't a bug introduced by this patch, but does the wait condition here handle concurrent senders correctly? When using CAN_ISOTP_WAIT_TX_DONE, isotp_sendmsg() waits on: wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE); If transfer 1's transmission completes (setting ISOTP_IDLE and waking waiters), but transfer 2 concurrently claims ISOTP_SENDING before transfer 1 wakes up, transfer 1 will evaluate so->tx.state == ISOTP_IDLE as false. Will transfer 1 then go back to sleep, incorrectly hanging while waiting on transfer 2's completion? > > +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 under so->rx_lock, held through > + * err_out_drop_locked below, so a concurrent claim's own drain > + * step can't proceed until we're done > + */ > + spin_lock_bh(&so->rx_lock); > hrtimer_cancel(&so->txfrtimer); > hrtimer_cancel(&so->txtimer); > -err_out_drop: > - /* drop this PDU and unlock a potential wait queue */ > - so->tx.state = ISOTP_IDLE; > + hrtimer_cancel(&so->echotimer); [Severity: High] Similar to the can_send() error path above, this unconditionally cancels all TX timers without checking if the socket is still owned by the current transfer's generation. If a timeout already occurred and a new transfer is active, will cancelling these timers destroy the active timers of the new transfer? > +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; -- Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v8-0-c39220a4e442@hartkopp.net?part=3 ^ permalink raw reply [flat|nested] 9+ messages in thread
* [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption 2026-07-12 12:41 [PATCH v8 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay ` (2 preceding siblings ...) 2026-07-12 12:41 ` [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 ` Oliver Hartkopp via B4 Relay 2026-07-12 12:56 ` sashiko-bot 3 siblings, 1 reply; 9+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-12 12:41 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 c5b44d298618..ceb590ae058e 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] 9+ messages in thread
* Re: [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption 2026-07-12 12:41 ` [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay @ 2026-07-12 12:56 ` sashiko-bot 0 siblings, 0 replies; 9+ messages in thread From: sashiko-bot @ 2026-07-12 12:56 UTC (permalink / raw) To: Oliver Hartkopp Cc: linux-can, Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol, Oleksij Rempel Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: Pre-existing issues: - [High] The `so->tx_gen` mechanism fails to protect against delayed local echoes from aborted transfers if the CAN payload matches, leading to protocol pacing violations and timeouts. - [High] Signal interruption during `wait_tx_done` causes racy state clobbering of concurrent transfers due to a stolen wakeup and lockless state update. -- --- Patch [4]: [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption --- commit 7dee50f317c5e02a3ea6610e45f4dde065c0b96c Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: fix state machine corruption on signal interruption This commit fixes a bug where a signal interrupting a thread waiting to start a new transfer incorrectly aborts an ongoing transfer. By returning the error directly, it avoids executing the cleanup path that forces the state back to ISOTP_IDLE. > diff --git a/net/can/isotp.c b/net/can/isotp.c > index c5b44d298618b..ceb590ae058eb 100644 > --- a/net/can/isotp.c > +++ b/net/can/isotp.c > @@ -1047,7 +1047,7 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size) > /* 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; [Severity: High] This is a pre-existing issue, but does a similar race exist later in isotp_sendmsg() when handling wait_tx_done? If Thread A waits in wait_tx_done with a signal pending, and concurrently the transfer finishes (setting state to ISOTP_IDLE and waking waiters), Thread B could wake up and claim the socket locklessly: Thread B: while (cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) != ISOTP_IDLE) { Before Thread B increments so->tx_gen, if Thread A wakes up, evaluates its wait condition, returns -ERESTARTSYS, and jumps to err_event_drop: Thread A: if (wait_tx_done) { err = wait_event_interruptible(so->wait, so->tx.state == ISOTP_IDLE); if (err) goto err_event_drop; } Would Thread A then incorrectly reset the state back to ISOTP_IDLE while Thread B is actively using the socket? Thread A in err_event_drop: if (READ_ONCE(so->tx_gen) == gen) { so->cfecho = 0; so->tx.state = ISOTP_IDLE; } > } > > /* new transfer: bump the generation (gen is used below and by [Severity: High] This isn't a bug introduced by this patch, but does the so->tx_gen mechanism fully protect against delayed local echoes from aborted transfers? If a transfer is aborted while its CAN frame is already queued, and a new transfer begins with matching first 4 bytes of payload, could the delayed local echo from the aborted frame arrive and be falsely validated? In isotp_rcv_echo(): if (so->cfecho != *(u32 *)cf->data) goto out_unlock; gen = READ_ONCE(so->tx_gen); ... if (so->tx.state != ISOTP_SENDING || READ_ONCE(so->tx_gen) != gen) goto out_unlock; Since gen is read directly from the current socket state (so->tx_gen) rather than being persistently attached to the SKB, does this allow a stale echo to prematurely advance the new transfer's state machine, leading to pacing (STmin) violations and timeouts? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v8-0-c39220a4e442@hartkopp.net?part=4 ^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-07-12 13:00 UTC | newest] Thread overview: 9+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-12 12:41 [PATCH v8 0/4] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-12 12:41 ` [PATCH v8 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay 2026-07-12 12:54 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay 2026-07-12 12:59 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp via B4 Relay 2026-07-12 12:53 ` sashiko-bot 2026-07-12 12:41 ` [PATCH v8 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp via B4 Relay 2026-07-12 12:56 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox