* [PATCH v9 0/4] net: can: isotp-fixes
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 2026-07-12 13:44 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 V8 patchset.
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
Changes in v9:
- patch 3: fix two reported issues and explain why issue 3&4 (-ECOMM sk_err
handling) will not be changed
- Link to v8: https://patch.msgid.link/20260712-isotp-fixes-v8-0-c39220a4e442@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 | 286 +++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 220 insertions(+), 66 deletions(-)
---
base-commit: dd3210c47e8d3ac6b4e9141fc68acc03b38c0ba3
change-id: 20260710-isotp-fixes-ead7480d67bf
Best regards,
--
Oliver Hartkopp <socketcan@hartkopp.net>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v9 0/4] net: can: isotp-fixes
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 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 V8 patchset.
Signed-off-by: Oliver Hartkopp <socketcan@hartkopp.net>
---
Changes in v9:
- patch 3: fix two reported issues and explain why issue 3&4 (-ECOMM sk_err
handling) will not be changed
- Link to v8: https://patch.msgid.link/20260712-isotp-fixes-v8-0-c39220a4e442@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 | 286 +++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 220 insertions(+), 66 deletions(-)
---
base-commit: dd3210c47e8d3ac6b4e9141fc68acc03b38c0ba3
change-id: 20260710-isotp-fixes-ead7480d67bf
Best regards,
--
Oliver Hartkopp <socketcan@hartkopp.net>
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
-1 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, Nico Yip
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] 12+ messages in thread
* [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release()
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 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] 12+ messages in thread
* [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
-1 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot
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] 12+ messages in thread
* [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 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] 12+ messages in thread
* [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
-1 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde, sashiko-bot
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 bump
so->tx_gen, cancel all three TX timers and clear so->cfecho, all in
the same critical section - isotp_rcv_echo() must never be able to
observe the new generation alongside the old, not yet cleared cfecho
tag, or a stale echo could pass its generation check and corrupt the
new transfer. 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.
rx_lock only guarantees a new transfer can't have armed a timer of its
own while its claim is still waiting for the lock; it does not cover
sendmsg()'s later, unlocked hrtimer_start() before can_send(). So the
error paths also recheck so->tx_gen before each individual
hrtimer_cancel() and stop as soon as it no longer matches - at most one
of the three TX timers is ever armed for a given transfer, so the
other two calls are harmless no-ops regardless of the check.
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.
isotp_tx_timeout() still suppresses its ECOMM report on a so->tx_gen
mismatch, so a non-blocking sendmsg() that timed out can lose its only
notification via so->sk_err if a new transfer claims the socket first.
That is accepted as the lesser problem versus tainting an active new
transfer with a stale error.
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 | 188 +++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 152 insertions(+), 36 deletions(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index 44c044eb83e1..c4ca8a64e7ce 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,28 @@ 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. The bump and
+ * the so->cfecho reset happen in the same so->rx_lock section so
+ * isotp_rcv_echo() can never observe the new gen alongside the
+ * old, not-yet-cleared cfecho tag.
+ */
+ spin_lock_bh(&so->rx_lock);
+ gen = READ_ONCE(so->tx_gen) + 1;
+ WRITE_ONCE(so->tx_gen, gen);
+ 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 +1186,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 +1207,17 @@ 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));
- /* no transmission -> no timeout monitoring */
- hrtimer_cancel(&so->txtimer);
-
- /* reset consecutive frame echo tag */
- so->cfecho = 0;
-
- goto err_out_drop;
+ spin_lock_bh(&so->rx_lock);
+ /* no transmission -> no timeout monitoring; skip if a new
+ * claim has since taken over so we don't cancel its timer
+ */
+ if (READ_ONCE(so->tx_gen) == gen)
+ 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 +1229,40 @@ 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 one at a time under so->rx_lock,
+ * rechecking gen after each - a mismatch means our own transfer
+ * already concluded on its own (leaving nothing else of ours
+ * armed), so stop before a further cancel can hit a new transfer's
+ * timer instead
+ */
+ spin_lock_bh(&so->rx_lock);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txfrtimer);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txtimer);
-err_out_drop:
- /* drop this PDU and unlock a potential wait queue */
- so->tx.state = ISOTP_IDLE;
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
+ 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 +1372,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 +1812,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] 12+ messages in thread
* [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 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 bump
so->tx_gen, cancel all three TX timers and clear so->cfecho, all in
the same critical section - isotp_rcv_echo() must never be able to
observe the new generation alongside the old, not yet cleared cfecho
tag, or a stale echo could pass its generation check and corrupt the
new transfer. 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.
rx_lock only guarantees a new transfer can't have armed a timer of its
own while its claim is still waiting for the lock; it does not cover
sendmsg()'s later, unlocked hrtimer_start() before can_send(). So the
error paths also recheck so->tx_gen before each individual
hrtimer_cancel() and stop as soon as it no longer matches - at most one
of the three TX timers is ever armed for a given transfer, so the
other two calls are harmless no-ops regardless of the check.
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.
isotp_tx_timeout() still suppresses its ECOMM report on a so->tx_gen
mismatch, so a non-blocking sendmsg() that timed out can lose its only
notification via so->sk_err if a new transfer claims the socket first.
That is accepted as the lesser problem versus tainting an active new
transfer with a stale error.
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 | 188 +++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 152 insertions(+), 36 deletions(-)
diff --git a/net/can/isotp.c b/net/can/isotp.c
index 44c044eb83e1..c4ca8a64e7ce 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,28 @@ 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. The bump and
+ * the so->cfecho reset happen in the same so->rx_lock section so
+ * isotp_rcv_echo() can never observe the new gen alongside the
+ * old, not-yet-cleared cfecho tag.
+ */
+ spin_lock_bh(&so->rx_lock);
+ gen = READ_ONCE(so->tx_gen) + 1;
+ WRITE_ONCE(so->tx_gen, gen);
+ 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 +1186,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 +1207,17 @@ 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));
- /* no transmission -> no timeout monitoring */
- hrtimer_cancel(&so->txtimer);
-
- /* reset consecutive frame echo tag */
- so->cfecho = 0;
-
- goto err_out_drop;
+ spin_lock_bh(&so->rx_lock);
+ /* no transmission -> no timeout monitoring; skip if a new
+ * claim has since taken over so we don't cancel its timer
+ */
+ if (READ_ONCE(so->tx_gen) == gen)
+ 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 +1229,40 @@ 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 one at a time under so->rx_lock,
+ * rechecking gen after each - a mismatch means our own transfer
+ * already concluded on its own (leaving nothing else of ours
+ * armed), so stop before a further cancel can hit a new transfer's
+ * timer instead
+ */
+ spin_lock_bh(&so->rx_lock);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txfrtimer);
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
hrtimer_cancel(&so->txtimer);
-err_out_drop:
- /* drop this PDU and unlock a potential wait queue */
- so->tx.state = ISOTP_IDLE;
+ if (READ_ONCE(so->tx_gen) != gen)
+ goto err_out_drop_locked;
+ 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 +1372,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 +1812,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] 12+ messages in thread
* [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
-1 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp @ 2026-07-12 13:44 UTC (permalink / raw)
To: linux-can; +Cc: Oliver Hartkopp, Marc Kleine-Budde
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 c4ca8a64e7ce..56618ea1dae4 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] 12+ messages in thread
* [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption
@ 2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
0 siblings, 0 replies; 12+ messages in thread
From: Oliver Hartkopp via B4 Relay @ 2026-07-12 13:44 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 c4ca8a64e7ce..56618ea1dae4 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] 12+ messages in thread
* Re: [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
(?)
@ 2026-07-12 13:58 ` sashiko-bot
-1 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-07-12 13:58 UTC (permalink / raw)
To: Oliver Hartkopp
Cc: Oliver Hartkopp, linux-can, Vincent Mailhol, Oleksij Rempel,
Marc Kleine-Budde
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] The lock-free state claim mechanism in isotp_sendmsg() races with the generation bump, causing state clobbering that leads to concurrent buffer corruption and an Out-Of-Bounds (OOB) read leaking kernel memory to the CAN bus.
--
--- Patch [3]: [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception ---
commit b972a6bcac8d8b55930b5c1372f223dd7d9a46de
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: fix race between RX/TX timers and frame reception
This commit attempts to fix a race by introducing a generation counter
(tx_gen) to distinguish between old and new TX transfers. It also expands
rx_lock to cover the TX side for proper synchronization.
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..c4ca8a64e7ce1 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)
[ ... ]
> + /* 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;
[Severity: Critical]
Does the echo receiver similarly validate against a stale tx_gen if a new
transfer is blocked waiting for rx_lock?
If the generation hasn't been bumped yet, it appears this could process echoes
for the wrong transfer and inadvertently clobber its state.
> @@ -980,6 +1050,24 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
> 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. The bump and
> + * the so->cfecho reset happen in the same so->rx_lock section so
> + * isotp_rcv_echo() can never observe the new gen alongside the
> + * old, not-yet-cleared cfecho tag.
> + */
> + spin_lock_bh(&so->rx_lock);
> + gen = READ_ONCE(so->tx_gen) + 1;
> + WRITE_ONCE(so->tx_gen, gen);
[Severity: Critical]
Because cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING) happens before
this lock is acquired, is there a window where a new transfer has claimed the
state but hasn't bumped tx_gen yet?
> @@ -1142,14 +1231,36 @@ static int isotp_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
>
> return size;
>
> +err_out_drop:
> + spin_lock_bh(&so->rx_lock);
> + goto err_out_drop_locked;
> err_event_drop:
[ ... ]
> +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;
[Severity: Critical]
Can this lock-free claim in isotp_sendmsg() race with the generation bump and
cause state clobbering?
If Thread A is in err_event_drop() and holds rx_lock, and Thread C (a timeout
timer executing isotp_tx_timeout()) locklessly sets the state to ISOTP_IDLE:
Thread C (isotp_tx_timeout)
cmpxchg(&so->tx.state, old_state, ISOTP_IDLE);
Then Thread B calls isotp_sendmsg(), sees ISOTP_IDLE, claims it to
ISOTP_SENDING locklessly, and blocks on rx_lock:
Thread B (isotp_sendmsg)
cmpxchg(&so->tx.state, ISOTP_IDLE, ISOTP_SENDING);
...
spin_lock_bh(&so->rx_lock); // blocks
When Thread A resumes in err_out_drop_locked, it checks tx_gen. Since Thread
B is blocked, tx_gen hasn't been bumped yet. Will Thread A then incorrectly
pass the check and reset the state to ISOTP_IDLE?
Thread A (err_out_drop_locked)
if (READ_ONCE(so->tx_gen) == gen) {
so->tx.state = ISOTP_IDLE;
}
Once Thread A drops the lock, Thread B will bump tx_gen and start writing to
tx.buf. But because the state is now ISOTP_IDLE, Thread D could successfully
call isotp_sendmsg() and concurrently claim the socket.
Could this lead to Threads B and D concurrently modifying tx.idx in
isotp_fill_dataframe(), causing an out-of-bounds read that leaks adjacent
kernel memory over the CAN bus?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v9-0-137840d7f0f4@hartkopp.net?part=3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release()
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
(?)
@ 2026-07-12 14:03 ` sashiko-bot
-1 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-07-12 14:03 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 2 potential issue(s) to consider:
New issues:
- [Medium] Unconditional `synchronize_rcu()` in `isotp_release()` can be triggered by unprivileged users via unbound sockets.
Pre-existing issues:
- [High] Concurrent isotp_sendmsg() callers can forcibly abort another thread's active transmission upon receiving a signal.
--
--- Patch [1]: [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() ---
commit d8a6dd60eabcde6d6f502a2471ec7e04cf918503
Author: Oliver Hartkopp <socketcan@hartkopp.net>
can: isotp: use unconditional synchronize_rcu() in isotp_release()
This patch makes isotp_release() always call synchronize_rcu() before
cancelling timers to close a race condition when a concurrent
NETDEV_UNREGISTER clears so->bound.
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();
> }
> }
> }
>
> + /* 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();
> +
[Severity: Medium]
Could this unconditional synchronize_rcu() be triggered rapidly by
unprivileged users?
If an unprivileged user creates an unbound CAN_ISOTP socket and closes it
immediately, it appears it will now hit this synchronize_rcu() in
isotp_release() because so->bound is no longer checked.
Might a rapid loop of socket creation and destruction allow local
unprivileged users to degrade system performance by constantly blocking for
RCU grace periods?
This is a pre-existing issue, but I noticed a potential race condition in
isotp_sendmsg() when handling signals.
[Severity: High]
Can a thread interrupted by a signal forcibly abort another thread's active
transmission?
If multiple threads call isotp_sendmsg() on the same socket, the function
uses cmpxchg() to gain exclusivity over the TX state machine. If a thread
fails the cmpxchg and waits in wait_event_interruptible(), but is then
interrupted by a signal, it jumps to err_event_drop:
net/can/isotp.c:isotp_sendmsg() {
...
err_event_drop:
/* got signal: force tx state machine to be idle */
so->tx.state = ISOTP_IDLE;
hrtimer_cancel(&so->txfrtimer);
hrtimer_cancel(&so->txtimer);
...
}
Does this forcible state reset to ISOTP_IDLE and timer cancellation happen
even though the interrupted thread does not own the state machine? This
appears to abort the active transmission and could allow a third thread to
pass the cmpxchg, leading to concurrent execution of isotp_sendmsg() on the
same so->tx variables.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260712-isotp-fixes-v9-0-137840d7f0f4@hartkopp.net?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-07-12 14:03 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-12 13:44 [PATCH v9 0/4] net: can: isotp-fixes Oliver Hartkopp
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 1/4] can: isotp: use unconditional synchronize_rcu() in isotp_release() Oliver Hartkopp
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 14:03 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 2/4] can: isotp: fix use-after-free race with concurrent NETDEV_UNREGISTER Oliver Hartkopp
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 13:44 ` [PATCH v9 3/4] can: isotp: fix race between RX/TX timers and frame reception Oliver Hartkopp
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
2026-07-12 13:58 ` sashiko-bot
2026-07-12 13:44 ` [PATCH v9 4/4] can: isotp: fix state machine corruption on signal interruption Oliver Hartkopp
2026-07-12 13:44 ` Oliver Hartkopp via B4 Relay
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.