* [PATCH v3 0/2] net: can: isotp-fixes
@ 2026-07-10 12:56 Oliver Hartkopp via B4 Relay
2026-07-10 12:56 ` [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-10 12:56 ` [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
0 siblings, 2 replies; 5+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-10 12:56 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.
It covers two fixes reported by Nico Yip and sashiko-bot.
Signed-off-by: Oliver Hartkopp <socketcan@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 (2):
can: isotp: use unconditional synchronize_rcu() in isotp_release()
can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
net/can/isotp.c | 96 ++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 67 insertions(+), 29 deletions(-)
---
base-commit: a635d6748234582ea287c5ffeae28b9b23f91c7e
change-id: 20260710-isotp-fixes-ead7480d67bf
Best regards,
--
Oliver Hartkopp <socketcan@hartkopp.net>
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-10 12:56 [PATCH v3 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay @ 2026-07-10 12:56 ` Oliver Hartkopp via B4 Relay 2026-07-10 13:18 ` sashiko-bot 2026-07-10 12:56 ` [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay 1 sibling, 1 reply; 5+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-10 12:56 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] 5+ messages in thread
* Re: [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-10 12:56 ` [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-10 13:18 ` sashiko-bot 0 siblings, 0 replies; 5+ messages in thread From: sashiko-bot @ 2026-07-10 13:18 UTC (permalink / raw) To: Oliver Hartkopp Cc: Marc Kleine-Budde, Vincent Mailhol, Oleksij Rempel, linux-can, Oliver Hartkopp Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: New issues: - [High] Unconditional `synchronize_rcu()` in `isotp_release()` introduces a local Denial of Service vulnerability. Pre-existing issues: - [Critical] Race condition between `isotp_release()` and `NETDEV_UNREGISTER` leads to a permanently leaked CAN filter and a critical Use-After-Free. -- --- Patch [1]: [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() --- commit 1f00ee97fef2fda17e134f34cc9af1db10cbadf6 Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: use unconditional synchronize_rcu() in isotp_release() This commit forces an unconditional synchronize_rcu() in isotp_release() regardless of whether the socket was bound. This is intended to close a race condition with NETDEV_UNREGISTER clearing so->bound and cancelling timers while an in-flight isotp_rcv() is executing. 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: Critical] This is a pre-existing issue, but can a race condition between isotp_release() and namespace migration lead to a leaked CAN filter and a use-after-free? If a CAN device is moved to a different network namespace while concurrently closing a bound ISOTP socket, dev_change_net_namespace() calls unlist_netdevice(dev), synchronize_net(), and then broadcasts NETDEV_UNREGISTER. If isotp_release() runs during synchronize_net(), dev_get_by_index() returns NULL because the device is already unlisted: dev = dev_get_by_index(net, so->ifindex); if (dev) { if (isotp_register_rxid(so)) can_rx_unregister(...); This causes isotp_release() to silently skip can_rx_unregister(). When NETDEV_UNREGISTER is subsequently broadcast, isotp_notify() skips the socket since it is no longer in the list. Does this leave the CAN filters in the device's dev_rcv_lists with a dangling pointer to the freed socket, allowing a received CAN frame to trigger a use-after-free in isotp_rcv() when the device is brought up in the new netns? > > + /* 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] Does moving synchronize_rcu() outside the bound-checking block introduce a denial of service vector for unbound sockets? Because the wait is now unconditional, it is executed for every closed socket. Unbound sockets never arm timers. Could an unprivileged user trivially write a loop that repeatedly creates and closes unbound CAN_ISOTP sockets to spam the RCU subsystem with back-to-back grace period requests? Would skipping this wait via a flag indicating if the socket was ever bound prevent this performance degradation? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v3-0-08db68e27d0b@hartkopp.net?part=1 ^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-10 12:56 [PATCH v3 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-10 12:56 ` [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-10 12:56 ` Oliver Hartkopp via B4 Relay 2026-07-10 13:07 ` sashiko-bot 1 sibling, 1 reply; 5+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-10 12:56 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 waits for so->tx.state to reach ISOTP_IDLE before allowing a (re)bind, bounded by the existing FC/echo watchdog timers, so a stale timer left running by isotp_notify() can never act on a newly bound so->ifindex; isotp_sendmsg() re-validates so->bound too. 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..d98863bad0d9 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,10 +1317,22 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l } if (!addr->can_ifindex) return -ENODEV; + if (!so->bound) { + /* A transmission that outlived a previous binding (unbound + * by NETDEV_UNREGISTER) must settle before so->tx can be + * reused; the FC/echo watchdog timers bound this wait. + */ + err = wait_event_interruptible(so->wait, + so->tx.state == ISOTP_IDLE); + if (err) + return err; + } + + rtnl_lock(); lock_sock(sk); if (so->bound) { err = -EINVAL; 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] 5+ messages in thread
* Re: [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-10 12:56 ` [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay @ 2026-07-10 13:07 ` sashiko-bot 0 siblings, 0 replies; 5+ messages in thread From: sashiko-bot @ 2026-07-10 13:07 UTC (permalink / raw) To: Oliver Hartkopp Cc: Marc Kleine-Budde, Oliver Hartkopp, linux-can, Oleksij Rempel, Vincent Mailhol Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] Lockless check of `so->bound` in `isotp_bind()` creates a TOCTOU race that bypasses the newly added wait for `ISOTP_IDLE`, allowing cross-device packet injection. -- --- Patch [2]: [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER --- commit e4c5bbbd972ba76bfee06412e3abf69c164cec65 Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER This commit fixes a use-after-free race in isotp_release() and isotp_notify() by taking a tracked reference to the net_device and serializing bind/release with rtnl_lock(). It also adds a wait in isotp_bind() to ensure any active transmissions settle if the socket was unbound by a concurrent device unregistration. 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..d98863bad0d9f 100644 > --- a/net/can/isotp.c > +++ b/net/can/isotp.c [ ... ] > @@ -1310,6 +1319,18 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l > if (!addr->can_ifindex) > return -ENODEV; > > + if (!so->bound) { > + /* A transmission that outlived a previous binding (unbound > + * by NETDEV_UNREGISTER) must settle before so->tx can be > + * reused; the FC/echo watchdog timers bound this wait. > + */ > + err = wait_event_interruptible(so->wait, > + so->tx.state == ISOTP_IDLE); > + if (err) > + return err; > + } > + > + rtnl_lock(); > lock_sock(sk); > > if (so->bound) { [Severity: High] Can this lockless check of so->bound bypass the wait if the socket is unbound just before acquiring the locks? If a user initiates a bind to a new device while the socket is currently bound and transmitting, the initial if (!so->bound) check will be false, skipping the wait for ISOTP_IDLE. If a concurrent NETDEV_UNREGISTER runs and clears so->bound before this thread acquires rtnl_lock(), the inner if (so->bound) check will then evaluate to false. Does this allow the bind to succeed while the state machine is still transmitting, potentially injecting the remaining payload onto the newly bound CAN device? -- Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v3-0-08db68e27d0b@hartkopp.net?part=2 ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-10 13:18 UTC | newest] Thread overview: 5+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-10 12:56 [PATCH v3 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-10 12:56 ` [PATCH v3 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay 2026-07-10 13:18 ` sashiko-bot 2026-07-10 12:56 ` [PATCH v3 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay 2026-07-10 13:07 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox