* [PATCH 0/2] net: can: isotp-fixes
@ 2026-07-10 9:32 Oliver Hartkopp via B4 Relay
2026-07-10 9:32 ` [PATCH 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-10 9:32 ` [PATCH 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 9:32 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>
---
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 | 77 +++++++++++++++++++++++++++++++++++----------------------
1 file changed, 48 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 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-10 9:32 [PATCH 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay @ 2026-07-10 9:32 ` Oliver Hartkopp via B4 Relay 2026-07-10 9:49 ` sashiko-bot 2026-07-10 9:32 ` [PATCH 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 9:32 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 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() 2026-07-10 9:32 ` [PATCH 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-10 9:49 ` sashiko-bot 0 siblings, 0 replies; 5+ messages in thread From: sashiko-bot @ 2026-07-10 9:49 UTC (permalink / raw) To: Oliver Hartkopp Cc: Oleksij Rempel, linux-can, Oliver Hartkopp, Marc Kleine-Budde, Vincent Mailhol Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: Pre-existing issues: - [High] Concurrent lockless access to TX state variables leads to buffer corruption and invalid frame transmission. - [Medium] Concurrent socket release and netdevice unregister skips CAN filter unregistration, leading to a memory leak. -- --- Patch [1]: [PATCH 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() --- commit fd9ce93db70bc0c54d097a98764dcfab2ecff812 Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: use unconditional synchronize_rcu() in isotp_release() This commit moves the synchronize_rcu() call in isotp_release() to run unconditionally before timer cancellation. It ensures we always wait for any in-flight isotp_rcv() callers to finish, preventing a use-after-free timer callback race when NETDEV_UNREGISTER clears so->bound early. 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: Medium] This isn't a bug introduced by this patch, but earlier in isotp_release(), there is a check for dev_get_by_index(): 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); If isotp_release() runs concurrently with a NETDEV_UNREGISTER event, dev_get_by_index() can return NULL. When that happens, does this skip the can_rx_unregister() calls and permanently leak the CAN filters? > } > } > > + /* 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); [Severity: High] This is a pre-existing issue, but could there be a lockless state corruption involving the txtimer? If isotp_tx_timer_handler() executes on CPU A, it sets so->tx.state = ISOTP_IDLE and wakes up waiters: so->tx.state = ISOTP_IDLE; wake_up_interruptible(&so->wait); Concurrently, if isotp_rcv_fc() executes on CPU B, it calls hrtimer_cancel(): hrtimer_cancel(&so->txtimer); CPU B will spin waiting for the timer handler on CPU A to finish. While CPU B spins, a blocked task in isotp_sendmsg() on CPU C can wake up, see ISOTP_IDLE, and successfully execute the state exchange: while (cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) != ISOTP_IDLE) { CPU C will then begin writing into so->tx.buf. When the timer handler on CPU A finishes, hrtimer_cancel() on CPU B returns. isotp_rcv_fc() then unconditionally sets the state to ISOTP_SENDING and calls isotp_send_cframe(): case ISOTP_FC_CTS: so->tx.bs = 0; so->tx.state = ISOTP_SENDING; ... isotp_send_cframe(so); Does this lead to isotp_send_cframe() concurrently reading from so->tx.buf while isotp_sendmsg() is still writing to it? > hrtimer_cancel(&so->rxtimer); -- Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v1-0-75a1d11d7df9@hartkopp.net?part=1 ^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-10 9:32 [PATCH 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-10 9:32 ` [PATCH 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay @ 2026-07-10 9:32 ` Oliver Hartkopp via B4 Relay 2026-07-10 9:48 ` sashiko-bot 1 sibling, 1 reply; 5+ messages in thread From: Oliver Hartkopp via B4 Relay @ 2026-07-10 9:32 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. 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 | 68 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/net/can/isotp.c b/net/can/isotp.c index d30937345bcd..9db8f9f12a0d 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; @@ -1217,32 +1219,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 +1255,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 +1309,11 @@ 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; @@ -1327,18 +1329,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 +1352,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 +1573,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 +1589,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 +1654,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 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER 2026-07-10 9:32 ` [PATCH 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay @ 2026-07-10 9:48 ` sashiko-bot 0 siblings, 0 replies; 5+ messages in thread From: sashiko-bot @ 2026-07-10 9:48 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 1 potential issue(s) to consider: Pre-existing issues: - [High] Missing state cleanup on NETDEV_UNREGISTER allows cross-interface data injection during socket rebind. -- --- Patch [2]: [PATCH 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER --- commit 604ae1715e00f0c538a10b8cc5f6f3cb4fbf984c Author: Oliver Hartkopp <socketcan@hartkopp.net> can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER This commit fixes a use-after-free in isotp_release() by holding a tracked reference to the bound net_device in the socket from bind() onward. It also serializes bind() and release() with rtnl_lock() to ensure the device pointer remains consistent with NETDEV_UNREGISTER events. 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..9db8f9f12a0db 100644 > --- a/net/can/isotp.c > +++ b/net/can/isotp.c [ ... ] > @@ -1354,16 +1354,25 @@ static int isotp_bind(struct socket *sock, struct sockaddr_unsized *uaddr, int l > 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); [Severity: High] This is a pre-existing issue, but does rebinding the socket here safely handle old transmission state? If a socket is unbound via a concurrent NETDEV_UNREGISTER, the state machine (so->tx.state) isn't cleared. Could a subsequent bind() to a new interface cause still-active hrtimers to transmit the remaining frames of an old packet onto this newly bound CAN bus? [ ... ] > @@ -1582,10 +1591,12 @@ static void isotp_notify(struct isotp_sock *so, unsigned long msg, > 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; [Severity: High] This isn't a bug introduced by this patch, but does NETDEV_UNREGISTER need to reset the transmission state machine or cancel active hrtimers here? When isotp_notify() handles NETDEV_UNREGISTER, it clears so->bound and so->dev, but leaves high-resolution timers (like isotp_txfr_timer_handler) running. Is it possible for these active timers to inject cross-interface data if the user immediately rebinds the socket? > release_sock(sk); > > sk->sk_err = ENODEV; -- Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v1-0-75a1d11d7df9@hartkopp.net?part=2 ^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-10 9:49 UTC | newest] Thread overview: 5+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-10 9:32 [PATCH 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay 2026-07-10 9:32 ` [PATCH 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay 2026-07-10 9:49 ` sashiko-bot 2026-07-10 9:32 ` [PATCH 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay 2026-07-10 9:48 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox