Technical Analysis: Root Cause of WARNING in pci_alloc_child_bus
1. Overview & Location
The bug manifests in drivers/pci/probe.c within pci_alloc_child_bus() (called during bus allocation in pci_add_new_bus() during rescan via /sys/bus/pci/rescan):
// drivers/pci/probe.c:1263-1275
for (i = 0; i < PCI_BRIDGE_RESOURCE_NUM; i++) {
child->resource[i] = &bridge->resource[PCI_BRIDGE_RESOURCES+i];
child->resource[i]->name = child->name;
}
bridge->subordinate = child;
add_dev:
pci_set_bus_msi_domain(child);
ret = device_register(&child->dev);
if (WARN_ON(ret < 0)) {
put_device(&child->dev);
return NULL;
}
2. Trigger Mechanism & Immediate Cause of WARNING
In the reproducer, fault injection is configured via /proc/thread-self/fail-nth before triggering a PCI bus rescan (rescan_store() -> pci_rescan_bus() -> pci_scan_child_bus_extend() -> pci_scan_bridge_extend() -> pci_add_new_bus() -> pci_alloc_child_bus()).
During device_register(&child->dev), internal allocations (e.g., inside device_add() or kobject_add()) fail, causing device_register() to return -ENOMEM (observed as RBP: 0x00000000fffffff4 in the CPU register dump, which corresponds to -12).
In Linux device core code, device_register() can legitimately fail at runtime due to resource exhaustion or transient allocation failures. Checking its return value with WARN_ON(ret < 0) is incorrect because -ENOMEM is a standard runtime condition rather than an internal kernel invariant violation. This causes the kernel warning to fire whenever allocation fails during device registration.
3. Incomplete Error Cleanup & Dangling Pointer
Prior to attempting device_register(&child->dev), pci_alloc_child_bus() performs the following initialization when bridge is non-NULL:
- Links the bridge's resources to the child:
child->resource[i]->name = child->name (lines 1263–1266).
- Sets the bridge's subordinate pointer:
bridge->subordinate = child (line 1267).
When device_register(&child->dev) returns an error:
put_device(&child->dev) is called to drop the initial reference of child->dev.
- As a result, the bus class release function
release_pcibus_dev() is invoked, which releases resources and calls kfree(pci_bus), freeing the struct pci_bus allocated for child.
- However,
pci_alloc_child_bus() returns NULL without undoing the bridge assignments:
bridge->subordinate is not set back to NULL.
bridge->resource[PCI_BRIDGE_RESOURCES + i].name is left pointing into the freed child->name buffer.
This leaves bridge->subordinate as a dangling pointer to a freed struct pci_bus object, exposing the system to use-after-free and memory corruption during subsequent PCI bridge operations, bus rescans, or bridge device teardown (pci_remove_bus_device()).