* [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
* [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
* 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
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