mac802154: hold an interface reference across the scan worker
Subsystems (4)
PATCH 1/1 mac802154: hold an interface reference across the scan worker
mac802154_scan_worker() captures the scanning sub-interface once under RCU:
sdata = IEEE802154_WPAN_DEV_TO_SUB_IF(scan_req->wpan_dev);
and then, after rcu_read_unlock() and outside the rtnl, keeps dereferencing sdata->dev: in the channel-change and restart failure traces, in mac802154_transmit_beacon_req() (skb->dev = sdata->dev) for active scans, in the final dev_dbg(), and in the end_scan mac802154_scan_cleanup_locked() path. Nothing keeps that netdev alive for the duration of the worker iteration.
A concurrent teardown of the scanning interface -- userspace issuing NL802154_CMD_DEL_INTERFACE (ieee802154_if_remove() -> unregister_netdevice()), or a full PHY removal via ieee802154_unregister_hw() -> ieee802154_remove_interfaces() -- can run as soon as the worker drops the rtnl between its two short drv_set_channel()/drv_start() sections. The netdev is not freed synchronously by unregister_netdevice(): it is queued to net_todo_list and freed later from netdev_run_todo(), which drops the rtnl mutex (__rtnl_unlock()) *before* netdev_wait_allrefs_any()/free_netdev(). The freeing therefore runs with the rtnl not held, on whichever task next drains net_todo_list. Holding the rtnl in the worker does not prevent it, and the per-PHY IEEE802154_IS_SCANNING flag does not identify the specific interface: a subsequent NEW_INTERFACE + TRIGGER_SCAN re-arms the flag, so a stale worker iteration sails past an is-scanning recheck and dereferences the already-freed netdev.
Triggering the race requires CAP_NET_ADMIN: both NL802154_CMD_TRIGGER_SCAN and NL802154_CMD_DEL_INTERFACE are GENL_ADMIN_PERM, reachable only from the initial user namespace, so the attacker is a locally privileged (CAP_NET_ADMIN) user, not an unprivileged local user or a remote peer.
KASAN slab-use-after-free, kworker reading the freed net_device/ieee802154_sub_if_data (kmalloc-cg-4k) allocated and freed by the racing NEW_INTERFACE/DEL_INTERFACE task:
BUG: KASAN: slab-use-after-free in mac802154_scan_worker+0x... [mac802154] Read of size 8 ... by task kworker/u8:N Workqueue: phy0-mac-cmds mac802154_scan_worker [mac802154] mac802154_scan_worker process_one_work (also hit via ieee802154_mlme_tx_locked() from the beacon-request path and via _dev_err()/__dev_printk() formatting sdata->dev->dev)
Fix it by taking a reference on the interface while the RCU read lock is still held -- so the netdev cannot be freed before the refcount is raised -- and releasing it at every exit of the worker past that point. This keeps sdata->dev valid for the whole iteration.
The reference does not defer the free indefinitely and does not deadlock the single-threaded mac_wq. Each worker iteration drops the reference before it requeues itself, and a teardown started while the reference is held simply blocks in netdev_run_todo() until the current iteration returns. It cannot be the worker's own rtnl_unlock() that blocks on the reference either: the unregistering task removes the netdev from net_todo_list under the rtnl (list_replace_init() in netdev_run_todo() runs before __rtnl_unlock()), so it always owns the todo entry, and the worker acquires the rtnl only afterwards -- the blocking netdev_wait_allrefs_any() therefore always runs on the teardown task, never on the worker.
Verified on a v6.19 KASAN build: racing DEL_INTERFACE against an in-flight TRIGGER_SCAN reliably tripped a slab-use-after-free KASAN report inside mac802154_scan_worker() before this patch, and the same reproducer no longer triggers it with the fix applied.
--- net/mac802154/scan.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+)
net/mac802154/scan.c +22 -0
@@ -209,6 +209,26 @@ void mac802154_scan_worker(struct work_struct *work)return ; } +/* From here on sdata->dev is dereferenced after rcu_read_unlock() and +* outside the rtnl: in the dev_err()/dev_dbg() traces below, in +* mac802154_transmit_beacon_req() (skb->dev = sdata->dev) and in the +* end_scan mac802154_scan_cleanup_locked() call. A concurrent teardown +* of that interface (NL802154_CMD_DEL_INTERFACE -> +* ieee802154_if_remove(), or a full PHY removal via +* ieee802154_unregister_hw()) can unregister the netdev; the actual +* free then runs asynchronously from netdev_run_todo() with the rtnl +* already dropped, so neither holding the rtnl nor the per-PHY +* IEEE802154_IS_SCANNING flag keeps sdata->dev alive here. Pin it with +* a reference taken while we still hold the RCU read lock (so the +* netdev cannot be freed before we bump the refcount) and drop it at +* every exit below. This blocks the teardown's netdev_run_todo() until +* this worker iteration is done; it cannot self-deadlock because the +* unregistering task claims the net_todo_list entry under the rtnl, so +* the blocking netdev_wait_allrefs_any() always runs on that task, not +* on this single-threaded worker. +*/ +dev_hold (sdata->dev); + wpan_phy = scan_req->wpan_phy; scan_req_type = scan_req->type; scan_req_duration = scan_req->duration;
@@ -262,12 +282,14 @@ void mac802154_scan_worker(struct work_struct *work)"Scan page %u channel %u for %ums\n" , page, channel, jiffies_to_msecs(scan_duration));queue_delayed_work (local->mac_wq, &local->scan_work, scan_duration); +dev_put (sdata->dev);return ; end_scan:rtnl_lock ();mac802154_scan_cleanup_locked (local, sdata, false);rtnl_unlock (); +dev_put (sdata->dev); } int mac802154_trigger_scan_locked(struct ieee802154_sub_if_data *sdata,
Signature · 1 line
2.50.1 (Apple Git-155)
Miquel Raynal
maintainerHi Ibrahim,
Quoted 8 lines
net/mac802154/scan.c +8
-0+/* From here on sdata->dev is dereferenced after rcu_read_unlock() and +* outside the rtnl: in the dev_err()/dev_dbg() traces below, in +* mac802154_transmit_beacon_req() (skb->dev = sdata->dev) and in the +* end_scan mac802154_scan_cleanup_locked() call. A concurrent teardown +* of that interface (NL802154_CMD_DEL_INTERFACE -> +* ieee802154_if_remove(), or a full PHY removal via +* ieee802154_unregister_hw()) can unregister the netdev; the actual +* free then runs asynchronously from netdev_run_todo() with the rtnl
net/mac802154/scan.c void mac802154_scan_worker(struct work_struct *work) · line 229
Quoted 10 lines
net/mac802154/scan.c +10
-0+* already dropped, so neither holding the rtnl nor the per-PHY +* IEEE802154_IS_SCANNING flag keeps sdata->dev alive here. Pin it with +* a reference taken while we still hold the RCU read lock (so the +* netdev cannot be freed before we bump the refcount) and drop it at +* every exit below. This blocks the teardown's netdev_run_todo() until +* this worker iteration is done; it cannot self-deadlock because the +* unregistering task claims the net_todo_list entry under the rtnl, so +* the blocking netdev_wait_allrefs_any() always runs on that task, not +* on this single-threaded worker. +*/
The patch seems correct, the fix as well, but can we trim down this huge comment please?
Thanks,
Miquèl
PATCH v2 1/1 mac802154: hold an interface reference across the scan worker
In reply to Ibrahim Hashimovmac802154_scan_worker() captures the scanning sub-interface once under RCU:
sdata = IEEE802154_WPAN_DEV_TO_SUB_IF(scan_req->wpan_dev);
and then, after rcu_read_unlock() and outside the rtnl, keeps dereferencing sdata->dev: in the channel-change and restart failure traces, in mac802154_transmit_beacon_req() (skb->dev = sdata->dev) for active scans, in the final dev_dbg(), and in the end_scan mac802154_scan_cleanup_locked() path. Nothing keeps that netdev alive for the duration of the worker iteration.
A concurrent teardown of the scanning interface -- userspace issuing NL802154_CMD_DEL_INTERFACE (ieee802154_if_remove() -> unregister_netdevice()), or a full PHY removal via ieee802154_unregister_hw() -> ieee802154_remove_interfaces() -- can run as soon as the worker drops the rtnl between its two short drv_set_channel()/drv_start() sections. The netdev is not freed synchronously by unregister_netdevice(): it is queued to net_todo_list and freed later from netdev_run_todo(), which drops the rtnl mutex (__rtnl_unlock()) *before* netdev_wait_allrefs_any()/free_netdev(). The freeing therefore runs with the rtnl not held, on whichever task next drains net_todo_list. Holding the rtnl in the worker does not prevent it, and the per-PHY IEEE802154_IS_SCANNING flag does not identify the specific interface: a subsequent NEW_INTERFACE + TRIGGER_SCAN re-arms the flag, so a stale worker iteration sails past an is-scanning recheck and dereferences the already-freed netdev.
Triggering the race requires CAP_NET_ADMIN: both NL802154_CMD_TRIGGER_SCAN and NL802154_CMD_DEL_INTERFACE are GENL_ADMIN_PERM, reachable only from the initial user namespace, so the attacker is a locally privileged (CAP_NET_ADMIN) user, not an unprivileged local user or a remote peer.
KASAN slab-use-after-free, kworker reading the freed net_device/ieee802154_sub_if_data (kmalloc-cg-4k) allocated and freed by the racing NEW_INTERFACE/DEL_INTERFACE task:
BUG: KASAN: slab-use-after-free in mac802154_scan_worker+0x... [mac802154] Read of size 8 ... by task kworker/u8:N Workqueue: phy0-mac-cmds mac802154_scan_worker [mac802154] mac802154_scan_worker process_one_work
Fix it by taking a reference on the interface while the RCU read lock is still held -- so the netdev cannot be freed before the refcount is raised -- and releasing it at every exit of the worker past that point. This keeps sdata->dev valid for the whole iteration. The reference does not defer the free indefinitely: a teardown started while it is held simply blocks in netdev_run_todo() until the current iteration returns, and it cannot self-deadlock the single-threaded mac_wq because the unregistering task claims the net_todo_list entry under the rtnl, so the blocking netdev_wait_allrefs_any() always runs on that task, not on the worker.
Verified on a v6.19 KASAN build: racing DEL_INTERFACE against an in-flight TRIGGER_SCAN reliably tripped a slab-use-after-free KASAN report inside mac802154_scan_worker() before this patch, and the same reproducer no longer triggers it with the fix applied.
---
v2: trim the in-worker comment down to the essentials, as requested by
Miquel Raynal. No functional change.
net/mac802154/scan.c | 10 ++++++++++ 1 file changed, 10 insertions(+)
net/mac802154/scan.c +10 -0
@@ -209,6 +209,14 @@ void mac802154_scan_worker(struct work_struct *work)return ; } +/* +* sdata->dev is dereferenced below after rcu_read_unlock() and outside +* the rtnl, and a concurrent DEL_INTERFACE / PHY teardown can free it +* asynchronously from netdev_run_todo(). Pin it with a reference taken +* while the RCU read lock is still held, and drop it at every exit. +*/ +dev_hold (sdata->dev); + wpan_phy = scan_req->wpan_phy; scan_req_type = scan_req->type; scan_req_duration = scan_req->duration;
@@ -262,12 +270,14 @@ void mac802154_scan_worker(struct work_struct *work)"Scan page %u channel %u for %ums\n" , page, channel, jiffies_to_msecs(scan_duration));queue_delayed_work (local->mac_wq, &local->scan_work, scan_duration); +dev_put (sdata->dev);return ; end_scan:rtnl_lock ();mac802154_scan_cleanup_locked (local, sdata, false);rtnl_unlock (); +dev_put (sdata->dev); } int mac802154_trigger_scan_locked(struct ieee802154_sub_if_data *sdata,
Signature · 1 line
2.50.1 (Apple Git-155)
Hello,
Quoted 4 lines
Fixes 57588c71177f ("mac802154: Handle passive scanning")Signed-off-by Ibrahim Hashimov <[email protected]>Assisted-by AuditCode-AI:2026.07
Thanks,
Miquèl
Jakub Kicinski
maintainernet/mac802154/scan.c void mac802154_scan_worker(struct work_struct *work) · line 218
Quoted 7 lines · On Fri, 17 Jul 2026 12:58:09 +0200 Ibrahim Hashimov wrote:
net/mac802154/scan.c +7
-0+/* +* sdata->dev is dereferenced below after rcu_read_unlock() and outside +* the rtnl, and a concurrent DEL_INTERFACE / PHY teardown can free it +* asynchronously from netdev_run_todo(). Pin it with a reference taken +* while the RCU read lock is still held, and drop it at every exit. +*/ +dev_hold (sdata->dev);
Please use netdev_hold() with a tracker on the stack. Please don't post new versions in reply to old ones.