* [PATCH v2 0/2] net: can: isotp-fixes
@ 2026-07-10 11:31 Oliver Hartkopp via B4 Relay
2026-07-10 11:31 ` [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-10 11:31 ` [PATCH v2 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 11:31 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 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 | 103 ++++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 74 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 v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-10 11:31 [PATCH v2 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
@ 2026-07-10 11:31 ` Oliver Hartkopp via B4 Relay
2026-07-10 11:48 ` sashiko-bot
2026-07-10 11:31 ` [PATCH v2 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 11:31 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
* [PATCH v2 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
2026-07-10 11:31 [PATCH v2 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
2026-07-10 11:31 ` [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
@ 2026-07-10 11:31 ` Oliver Hartkopp via B4 Relay
2026-07-10 11:46 ` sashiko-bot
1 sibling, 1 reply; 5+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-10 11:31 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_notify()'s NETDEV_UNREGISTER path left the tx/rx state machine
and hrtimers running after clearing so->bound, letting a stale timer
leak leftover data if the socket got rebound; both are now reset
under synchronize_net(), and isotp_sendmsg() re-validates so->bound.
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 | 94 ++++++++++++++++++++++++++++++++++++++++-----------------
1 file changed, 66 insertions(+), 28 deletions(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index d30937345bcd..d076226ac713 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,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 +1337,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 +1360,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 +1581,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 +1597,34 @@ 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;
+
+ /* wait for in-flight isotp_rcv()/isotp_rcv_echo() calls to
+ * finish so they can't leak stale tx data via a re-armed
+ * timer after a rebind.
+ * synchronize_net() is expedited here since rtnl_lock is held
+ */
+ synchronize_net();
+
+ hrtimer_cancel(&so->txfrtimer);
+ hrtimer_cancel(&so->txtimer);
+ hrtimer_cancel(&so->rxtimer);
+
+ /* reset tx/rx state so a following bind() starts clean */
+ so->tx.state = ISOTP_IDLE;
+ so->rx.state = ISOTP_IDLE;
+ so->cfecho = 0;
+ wake_up_interruptible(&so->wait);
+
release_sock(sk);
sk->sk_err = ENODEV;
if (!sock_flag(sk, SOCK_DEAD))
sk_error_report(sk);
@@ -1643,10 +1680,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 v2 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
2026-07-10 11:31 ` [PATCH v2 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
@ 2026-07-10 11:46 ` sashiko-bot
0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-07-10 11:46 UTC (permalink / raw)
To: Oliver Hartkopp
Cc: Marc Kleine-Budde, Vincent Mailhol, Oleksij Rempel,
Oliver Hartkopp, linux-can
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] Forcibly resetting `so->tx.state = ISOTP_IDLE` in `isotp_notify()` breaks the lockless mutual exclusion mechanism used by `isotp_sendmsg()`, leading to concurrent execution, memory corruption, and an Out-of-Bounds (OOB) write.
--
--- Patch [2]: [PATCH v2 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER ---
commit 92f3ac957278bcac43777c1cbfa4e2bf1989b7c2
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
This patch fixes a use-after-free race condition by holding a tracked
reference to the bound net_device in the socket from bind() onward. It
serializes bind()/release() with rtnl_lock() and resets the tx/rx state
machine and hrtimers in isotp_notify() during NETDEV_UNREGISTER.
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..d076226ac713f 100644
> --- a/net/can/isotp.c
> +++ b/net/can/isotp.c
[ ... ]
> @@ -1582,10 +1599,30 @@ 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;
> +
> + /* wait for in-flight isotp_rcv()/isotp_rcv_echo() calls to
> + * finish so they can't leak stale tx data via a re-armed
> + * timer after a rebind.
> + * synchronize_net() is expedited here since rtnl_lock is held
> + */
> + synchronize_net();
> +
> + hrtimer_cancel(&so->txfrtimer);
> + hrtimer_cancel(&so->txtimer);
> + hrtimer_cancel(&so->rxtimer);
> +
> + /* reset tx/rx state so a following bind() starts clean */
> + so->tx.state = ISOTP_IDLE;
> + so->rx.state = ISOTP_IDLE;
[Severity: Critical]
Does forcibly resetting so->tx.state to ISOTP_IDLE here break the lockless
mutual exclusion mechanism used by isotp_sendmsg()?
isotp_sendmsg() relies on cmpxchg() to protect the state machine without
holding lock_sock():
while (cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) != ISOTP_IDLE) {
If a thread is actively sleeping inside isotp_sendmsg() (for instance,
waiting for buffer space in sock_alloc_send_skb()), a concurrent
NETDEV_UNREGISTER will invoke isotp_notify() which clears the state here.
Because the state is externally reset, a subsequent bind() and sendmsg()
could successfully claim ISOTP_SENDING again, allowing two threads to execute
isotp_sendmsg() concurrently on the same socket.
Could this concurrent execution corrupt the shared so->tx state machine,
allowing so->tx.idx to exceed so->tx.len?
If so, later in isotp_send_cframe(), the space calculation would become
negative:
space = so->tx.len - so->tx.idx;
for (i = space; i < so->ll.tx_dl - SF_PCI_SZ4 - ae - off; i++)
cf->data[SF_PCI_SZ4 + ae + off + i] = so->opt.txpad_content;
Can this cause a backward out-of-bounds write into the SKB headroom?
> + so->cfecho = 0;
> + wake_up_interruptible(&so->wait);
> +
> release_sock(sk);
>
> sk->sk_err = ENODEV;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v2-0-bc57e26594b2@hartkopp.net?part=2
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-10 11:31 ` [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
@ 2026-07-10 11:48 ` sashiko-bot
0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-07-10 11:48 UTC (permalink / raw)
To: Oliver Hartkopp
Cc: Oliver Hartkopp, Oleksij Rempel, Vincent Mailhol,
Marc Kleine-Budde, linux-can
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [Critical] Race condition between `isotp_release()` and netdevice unregistration skips CAN filter unregistration, leading to a permanent memory leak and potential use-after-free.
--
--- Patch [1]: [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() ---
commit 554fcaa84431e4410ac9203b4f4a5bfeadc94ccf
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: use unconditional synchronize_rcu() in isotp_release()
This commit makes isotp_release() unconditionally call synchronize_rcu()
before cancelling timers. This closes a race where a concurrent
NETDEV_UNREGISTER could clear so->bound without waiting for in-flight
isotp_rcv() callers.
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);
[Severity: Critical]
This is a pre-existing issue, but is there a race condition between
isotp_release() and netdevice unregistration that can skip CAN filter
unregistration?
If unregister_netdevice_many() unlists the netdevice from net->dev_by_index
using unlist_netdevice(), and isotp_release() runs immediately after this but
before the NETDEV_UNREGISTER notifier is dispatched:
isotp_release() {
...
list_del(&so->notifier);
...
dev = dev_get_by_index(net, so->ifindex);
if (dev) {
...
}
}
Since the device is unlisted, this returns NULL, causing isotp_release() to
silently skip unregistering the CAN RX filters.
When the NETDEV_UNREGISTER notifier eventually runs in isotp_notifier(), it
misses the socket because it was already unlinked from isotp_notifier_list.
Could this permanently leak the CAN filters with a dangling pointer to the
freed socket, leading to a use-after-free? Can this be triggered by an
unprivileged user in a user namespace with CAP_NET_ADMIN by repeatedly
creating and destroying vcan interfaces while simultaneously creating and
closing ISOTP sockets?
> - synchronize_rcu();
> }
> }
> }
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260710-isotp-fixes-v2-0-bc57e26594b2@hartkopp.net?part=1
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-07-10 11:48 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-10 11:31 [PATCH v2 0/2] net: can: isotp-fixes Oliver Hartkopp via B4 Relay
2026-07-10 11:31 ` [PATCH v2 1/2] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp via B4 Relay
2026-07-10 11:48 ` sashiko-bot
2026-07-10 11:31 ` [PATCH v2 2/2] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp via B4 Relay
2026-07-10 11:46 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox