A race condition exists between device_move() and device_del() when
operating concurrently on the same device. For instance, when an HCI
connection disconnects, hci_conn_del_sysfs() iterates over child devices
(such as RFCOMM TTY devices) and calls device_move(dev, NULL,
DPM_ORDER_DEV_LAST) to reparent them to NULL. Concurrently, releasing or
closing the TTY device triggers device_del() on the exact same device.
In device_del(), kill_device(dev) sets dev->p->dead = true under
device_lock(dev). Later, device_del() calls
klist_del(&dev->p->knode_parent) to remove the device from its parent's
child list. When the klist node's reference count drops to 0,
klist_release() clears the node's klist pointer to NULL. Because
device_move() did not hold device_lock(dev) and did not check dev->p->dead,
it could execute concurrently and call klist_remove(&dev->p->knode_parent).
Inside klist_remove() -> klist_del() -> klist_put(), the code attempts to
dereference k->put at offset 0x58 on the NULL klist pointer, triggering a
NULL pointer dereference general protection fault:
Oops: general protection fault, probably for non-canonical address
0xdffffc000000000b: 0000 [#1] SMP KASAN NOPTI
KASAN: null-ptr-deref in range [0x0000000000000058-0x000000000000005f]
RIP: 0010:klist_put lib/klist.c:212 [inline]
RIP: 0010:klist_del lib/klist.c:230 [inline]
RIP: 0010:klist_remove+0x156/0x340 lib/klist.c:249
Call Trace:
device_move+0x18e/0x720 drivers/base/core.c:4702
hci_conn_del_sysfs+0xb8/0x1a0 net/bluetooth/hci_sysfs.c:88
hci_conn_del+0xc3d/0x1200 net/bluetooth/hci_conn.c:1339
hci_disconn_complete_evt+0x5ac/0x890 net/bluetooth/hci_event.c:3508
hci_rx_work+0x3ee/0x1020 net/bluetooth/hci_core.c:4087
process_one_work kernel/workqueue.c:3396 [inline]
process_scheduled_works+0xc3d/0x1630 kernel/workqueue.c:3479
worker_thread+0xa47/0xfb0 kernel/workqueue.c:3560
kthread+0x38b/0x480 kernel/kthread.c:436
ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
Additionally, if device_move() ran before device_del() acquired
device_lock(dev), device_del() previously sampled dev->parent before taking
device_lock(dev). If device_move() reparented the device and dropped the
old parent reference, device_del() proceeded with a stale parent pointer,
resulting in a duplicate klist_del() and put_device(parent) on old_parent,
leading to list corruption and reference count underflow.
Fix this by properly serializing device_move() with device_del(). In
device_del(), sample dev->parent under device_lock(dev) directly after
kill_device(dev) marks the device dead, ensuring that device_del() observes
any prior reparenting. In device_move(), acquire device_lock(dev) at entry
and check whether dev->p is uninitialized or dev->p->dead is true,
returning -ENODEV immediately if deletion has already begun. Furthermore,
narrow the scope of device_pm_lock() to only cover the DPM list reordering
switch block, adhering strictly to the driver core locking hierarchy of
acquiring device_lock(dev) before device_pm_lock() and preventing potential
ABBA deadlocks against probe paths. Finally, consolidate error cleanup
paths in device_move() to properly balance references and remove sysfs glue
directories before releasing device_lock(dev).
Fixes: 8a82472f86bf ("driver core: Introduce device_move(): move a device to a new parent.")
Assisted-by: Gemini:gemini-3.8-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: syzbot+1f4e278e8e1a9b01f95f@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=1f4e278e8e1a9b01f95f
Link: https://syzkaller.appspot.com/ai_job?id=3161b773-18b4-4410-aecf-8a0e46fe46d6
To: "Danilo Krummrich"
To:
To: "Greg Kroah-Hartman"
To: "Rafael J. Wysocki"
To: "Cornelia Huck"
Cc:
---
diff --git a/drivers/base/core.c b/drivers/base/core.c
index 4c0c37399..de90b7753 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -3919,13 +3919,14 @@ EXPORT_SYMBOL_GPL(kill_device);
void device_del(struct device *dev)
{
struct subsys_private *sp;
- struct device *parent = dev->parent;
+ struct device *parent;
struct kobject *glue_dir = NULL;
struct class_interface *class_intf;
unsigned int noio_flag;
device_lock(dev);
kill_device(dev);
+ parent = dev->parent;
device_unlock(dev);
if (dev->fwnode && dev->fwnode->dev == dev)
@@ -4679,23 +4680,26 @@ int device_move(struct device *dev, struct device *new_parent,
if (!dev)
return -EINVAL;
- device_pm_lock();
+ device_lock(dev);
+ if (!dev->p || dev->p->dead) {
+ error = -ENODEV;
+ goto out_unlock;
+ }
+
new_parent = get_device(new_parent);
new_parent_kobj = get_device_parent(dev, new_parent);
if (IS_ERR(new_parent_kobj)) {
error = PTR_ERR(new_parent_kobj);
put_device(new_parent);
- goto out;
+ goto out_unlock;
}
pr_debug("device: '%s': %s: moving to '%s'\n", dev_name(dev),
__func__, new_parent ? dev_name(new_parent) : "");
error = kobject_move(&dev->kobj, new_parent_kobj);
- if (error) {
- cleanup_glue_dir(dev, new_parent_kobj);
- put_device(new_parent);
- goto out;
- }
+ if (error)
+ goto out_cleanup;
+
old_parent = dev->parent;
dev->parent = new_parent;
if (old_parent)
@@ -4721,11 +4725,10 @@ int device_move(struct device *dev, struct device *new_parent,
set_dev_node(dev, dev_to_node(old_parent));
}
}
- cleanup_glue_dir(dev, new_parent_kobj);
- put_device(new_parent);
- goto out;
+ goto out_cleanup;
}
}
+ device_pm_lock();
switch (dpm_order) {
case DPM_ORDER_NONE:
break;
@@ -4742,10 +4745,19 @@ int device_move(struct device *dev, struct device *new_parent,
devices_kset_move_last(dev);
break;
}
+ device_pm_unlock();
+
+ device_unlock(dev);
put_device(old_parent);
-out:
- device_pm_unlock();
+ put_device(dev);
+ return error;
+
+out_cleanup:
+ cleanup_glue_dir(dev, new_parent_kobj);
+ put_device(new_parent);
+out_unlock:
+ device_unlock(dev);
put_device(dev);
return error;
}
base-commit: a90ee4305c4a5df72c11b31dacfdc76e00fcf78a
--
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).
See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at syzkaller@googlegroups.com.