Technical Analysis and Root Cause Investigation
1. Crash Site and Call Path
The failure occurs when opening /dev/snapshot (snapshot_open() in kernel/power/user.c), which invokes the power management notification chain:
snapshot_open()
pm_notifier_call_chain_robust(PM_HIBERNATION_PREPARE)
notifier_call_chain_robust()
fw_pm_notify() (drivers/base/firmware_loader/main.c:1618)
device_cache_fw_images() (drivers/base/firmware_loader/main.c:1568)
dpm_for_each_dev(NULL, dev_cache_fw_image) (drivers/base/power/main.c:2429)
dev_cache_fw_image(dev, NULL) (drivers/base/firmware_loader/main.c:1498)
devres_for_each_res(dev, fw_name_devm_release, ...) (drivers/base/devres.c:223-224)
In devres_for_each_res() (drivers/base/devres.c):
223: guard(spinlock_irqsave)(&dev->devres_lock);
224: list_for_each_entry_safe_reverse(node, tmp,
225: &dev->devres_head, entry) {
2. Detailed Disassembly and State at Crash
From the register state and disassembly:
x19 (0xffff0000c94427d8) is the target struct device *dev.
x28 (0xffff0000c9442c60) corresponds exactly to &dev->devres_head (offsetof(struct device, devres_head) == 0x488).
x20 (0xffff0000c9442c68) is &dev->devres_head.prev (x28 + 8).
x22 loaded from [x20] is 0x0 (NULL).
Prior to the crash, acquiring &dev->devres_lock triggered:
INFO: trying to register non-static key.
The code is fine but needs lockdep annotation, or maybe
you didn't initialize this object before use?
This indicates that dev->devres_lock was all zeroes (its lockdep_map.key was 0 and unregistered).
Following lock acquisition, list_for_each_entry_safe_reverse() expands to:
pos = list_last_entry(head, typeof(*pos), member); // container_of(head->prev, ...)
n = list_prev_entry(pos, member); // pos->entry.prev
Because dev->devres_head.prev is 0x0 (NULL), pos becomes NULL (since devres_node.entry is at offset 0). The macro then calculates n = pos->entry.prev, dereferencing offset 0x8 (*(NULL + 8)).
On an arm64 KASAN-instrumented kernel, reading address 0x8 accesses KASAN shadow address 0xdfff800000000000 + (0x8 >> 3) = 0xdfff800000000001, producing:
KASAN: null-ptr-deref in range [0x0000000000000008-0x000000000000000f] followed by an unhandled translation fault (kernel Oops).
3. Invariants Checked and Object Lifecycle Findings
device_initialize() vs. device_add() Invariants:
In drivers/base/core.c, device_initialize() initializes dev->devres_lock, INIT_LIST_HEAD(&dev->devres_head), dev->mutex, and calls device_pm_init(dev). Furthermore, device_add() acquires dev->mutex and calls device_pm_check_callbacks(dev) (which locks dev->power.lock). Had device_initialize() not been called before device_add(), lockdep warnings would have triggered earlier during device_add().
- Error unwinding in
device_add():
In device_add(), device_pm_add(dev) adds the device to dpm_list. Any subsequent failure in device_add() jumps to DevAttrError, which properly calls device_pm_remove(dev) to unlink the device from dpm_list.
- Uninitialized Structure Handling in
devres:
Unlike devres_release_all() (drivers/base/devres.c:565), which explicitly guards against uninitialized devices (if (WARN_ON(dev->devres_head.next == NULL)) return -ENODEV;), devres_for_each_res() performs no sanity check on dev->devres_head or dev->devres_lock.
- Dangling / Zeroed Object on
dpm_list:
Because KASAN did not report a slab use-after-free upon accessing dev->devres_lock or dev->devres_head, the memory backing x19 is currently valid/allocated in KASAN shadow memory, yet its device fields are zeroed. This demonstrates that a struct device linked in dpm_list either:
- Had its memory zeroed/overwritten in-place while still linked in
dpm_list, or
- Was freed without being unregistered (
device_del() / device_pm_remove()), leaving a dangling power.entry on dpm_list, and the slab memory was subsequently reallocated (e.g., via kzalloc()) and zeroed before dpm_for_each_dev() was invoked.