* [PATCH v2 0/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online()
@ 2026-10-29 3:45 Sasha Levin
2026-10-29 3:45 ` [PATCH v2 1/2] ACPI: scan: Print the offline failure message in acpi_bus_offline() Sasha Levin
2026-10-29 3:45 ` [PATCH v2 2/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
0 siblings, 2 replies; 3+ messages in thread
From: Sasha Levin @ 2026-10-29 3:45 UTC (permalink / raw)
To: Rafael J. Wysocki
Cc: Len Brown, linux-acpi, linux-kernel, syzbot+cea7285bc17b76ffe39f,
syzbot+a265816368571f78ab88, syzkaller-bugs, Sasha Levin
syzbot has reported a lockdep cycle between physical_node_lock and the
CPU hotplug locks, which comes from acpi_bus_offline() holding the ACPI
device's physical_node_lock while it offlines a CPU. Patch 2 stops
holding physical_node_lock across device_offline() and device_online().
Patch 1 is new in v2. It makes acpi_bus_offline() print the
"Offline failed." message itself, while the device cannot go away,
instead of passing the device to acpi_scan_try_to_offline() for
printing after the lock has been released.
Changes since v1:
- New patch 1 (Rafael).
- Patch 2: keep physical_node_lock held across put_device() and leave
the loop with break (Rafael).
- Patch 2: move the comments of the helpers next to their loops
(Rafael).
- Patch 2: the changelog no longer claims that physical_node_lock only
protects the list of physical nodes.
Tested in a QEMU q35 guest with lockdep: the reproducer no longer
triggers the report, CPU and DIMM hot-removal through QMP and sysfs
eject work, and when a DIMM cannot be removed because one of its memory
blocks fails to go offline, the other blocks are put back online and
"memory memory32: Offline failed." is printed, as before.
v1: https://lore.kernel.org/r/20261028161600.1524682-1-sashal@kernel.org
Sasha Levin (2):
ACPI: scan: Print the offline failure message in acpi_bus_offline()
ACPI: scan: Do not hold physical_node_lock across device_offline() and
device_online()
drivers/acpi/scan.c | 108 ++++++++++++++++++++++++++++++++--------
include/acpi/acpi_bus.h | 1 +
2 files changed, 89 insertions(+), 20 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v2 1/2] ACPI: scan: Print the offline failure message in acpi_bus_offline()
2026-10-29 3:45 [PATCH v2 0/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
@ 2026-10-29 3:45 ` Sasha Levin
2026-10-29 3:45 ` [PATCH v2 2/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
1 sibling, 0 replies; 3+ messages in thread
From: Sasha Levin @ 2026-10-29 3:45 UTC (permalink / raw)
To: Rafael J. Wysocki
Cc: Len Brown, linux-acpi, linux-kernel, syzbot+cea7285bc17b76ffe39f,
syzbot+a265816368571f78ab88, syzkaller-bugs, Sasha Levin
When a physical node of an ACPI device being hot-removed cannot be
offlined, acpi_bus_offline() passes the physical device back to
acpi_scan_try_to_offline() through ret_p and the latter prints
"Offline failed." for it after the namespace walk. By then
physical_node_lock is not held anymore, so the device may have been
unbound from its ACPI companion and freed in the meantime.
Print the message in acpi_bus_offline() when device_offline() fails in
the second pass, while physical_node_lock keeps the device bound to its
ACPI companion and so the reference taken by acpi_bind_one() is still
held. acpi_scan_try_to_offline() only needs to know that something
could not be offlined, so pass the ACPI device back through ret_p
instead, like in the "Offline disabled." case.
Fixes: 303bfdb1a14d ("ACPI / scan: Add second pass of companion offlining to hot-remove code")
Suggested-by: Rafael J. Wysocki <rafael@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
drivers/acpi/scan.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/acpi/scan.c b/drivers/acpi/scan.c
index 0b17a7e519569..07d32f118631e 100644
--- a/drivers/acpi/scan.c
+++ b/drivers/acpi/scan.c
@@ -165,8 +165,9 @@ static acpi_status acpi_bus_offline(acpi_handle handle, u32 lvl, void *data,
if (ret >= 0) {
pn->put_online = !ret;
} else {
- *ret_p = pn->dev;
+ *ret_p = &device->dev;
if (second_pass) {
+ dev_warn(pn->dev, "Offline failed.\n");
status = AE_ERROR;
break;
}
@@ -235,7 +236,6 @@ static int acpi_scan_try_to_offline(struct acpi_device *device)
(void **)&errdev);
if (errdev) {
- dev_warn(errdev, "Offline failed.\n");
acpi_bus_online(handle, 0, NULL, NULL);
acpi_walk_namespace(ACPI_TYPE_ANY, handle,
ACPI_UINT32_MAX, acpi_bus_online,
--
2.53.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH v2 2/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online()
2026-10-29 3:45 [PATCH v2 0/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
2026-10-29 3:45 ` [PATCH v2 1/2] ACPI: scan: Print the offline failure message in acpi_bus_offline() Sasha Levin
@ 2026-10-29 3:45 ` Sasha Levin
1 sibling, 0 replies; 3+ messages in thread
From: Sasha Levin @ 2026-10-29 3:45 UTC (permalink / raw)
To: Rafael J. Wysocki
Cc: Len Brown, linux-acpi, linux-kernel, syzbot+cea7285bc17b76ffe39f,
syzbot+a265816368571f78ab88, syzkaller-bugs, Sasha Levin
acpi_bus_offline() and acpi_bus_online() hold the physical_node_lock of
the ACPI device being hot-removed while they call device_offline() and
device_online() for its physical nodes. Offlining a CPU this way takes
cpu_add_remove_lock and the CPU hotplug lock under physical_node_lock,
and lockdep connects that to the uevent attribute of platform devices,
whose store method takes physical_node_lock in
acpi_primary_dev_companion():
physical_node_lock (acpi_bus_offline())
-> cpu_add_remove_lock -> cpu_hotplug_lock
-> cpuhp_state-down (mce_cpu_pre_down() unregistering a device)
-> device_links_lock (device_links_purge())
-> kn->active of a uevent attribute (device link unregistration)
-> physical_node_lock (uevent_store() -> acpi_companion_match())
syzbot has reported this cycle twice. The uevent attribute waited for in
the link unregistration is the device link's, not a platform device's,
and the physical_node_lock taken at the end is that of the platform
device's ACPI companion, not of the processor being ejected, so lockdep
only connects them because they share lock classes and this is unlikely
to deadlock. However, lockdep turns itself off after reporting it.
It can be reproduced in a QEMU guest by writing to the uevent attribute
of an ACPI-enumerated platform device, reading the uevent attribute of a
device link while the link is removed, offlining a CPU and ejecting one
through sysfs.
physical_node_lock protects the list of physical nodes. Since
acpi_unbind_one() blocks on it, holding it also keeps device_offline()
and device_online() from running in parallel with whatever happens to a
device after it has been unbound from its ACPI companion, but that is not
necessary: a reference to each device is held while the lock is dropped,
and the removal of the devices that can actually be taken offline here
(CPUs, memory blocks and containers) is serialized by the device hotplug
lock or acpi_scan_lock, both of which acpi_device_hotplug() holds
throughout.
So drop physical_node_lock around device_offline() and device_online().
Mark the nodes to offline with a new offline_pending flag, hold a
reference to each device while the lock is dropped, and look its node up
again afterwards to record whether it needs to be put back online,
because it may have been unbound in the meantime. Physical nodes added
while the lock is not held are not offlined by the current pass, which is
no different from adding them right after it.
Since acpi_container_offline() no longer runs under the container's
physical_node_lock, the nesting annotation added to
acpi_scan_is_offline() by commit 4c533c801d1c ("ACPI / scan: Annotate
physical_node_lock in acpi_scan_is_offline()") is not needed anymore, so
drop it.
Fixes: 683058e315f0 ("ACPI / hotplug: Use device offline/online for graceful hot-removal")
Reported-by: syzbot+cea7285bc17b76ffe39f@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/r/6ae18ed0.6200f071.51519.0f60.GAE@google.com
Reported-by: syzbot+a265816368571f78ab88@syzkaller.appspotmail.com
Closes: https://lore.kernel.org/r/6adf293a.67317f00.38c4bc.00af.GAE@google.com
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
drivers/acpi/scan.c | 106 +++++++++++++++++++++++++++++++++-------
include/acpi/acpi_bus.h | 1 +
2 files changed, 88 insertions(+), 19 deletions(-)
diff --git a/drivers/acpi/scan.c b/drivers/acpi/scan.c
index 07d32f118631e..ae8533e495c1e 100644
--- a/drivers/acpi/scan.c
+++ b/drivers/acpi/scan.c
@@ -114,11 +114,7 @@ bool acpi_scan_is_offline(struct acpi_device *adev, bool uevent)
bool offline = true;
char *envp[] = { "EVENT=offline", NULL };
- /*
- * acpi_container_offline() calls this for all of the container's
- * children under the container's physical_node_lock lock.
- */
- mutex_lock_nested(&adev->physical_node_lock, SINGLE_DEPTH_NESTING);
+ mutex_lock(&adev->physical_node_lock);
list_for_each_entry(pn, &adev->physical_node_list, node)
if (device_supports_offline(pn->dev) && !dev_offline(pn->dev)) {
@@ -133,6 +129,59 @@ bool acpi_scan_is_offline(struct acpi_device *adev, bool uevent)
return offline;
}
+static struct acpi_device_physical_node *
+acpi_find_physical_node(struct acpi_device *adev, struct device *dev)
+{
+ struct acpi_device_physical_node *pn;
+
+ lockdep_assert_held(&adev->physical_node_lock);
+
+ list_for_each_entry(pn, &adev->physical_node_list, node)
+ if (pn->dev == dev)
+ return pn;
+
+ return NULL;
+}
+
+static struct device *acpi_next_offline_pending(struct acpi_device *adev)
+{
+ struct acpi_device_physical_node *pn;
+
+ lockdep_assert_held(&adev->physical_node_lock);
+
+ /*
+ * Find the first physical node that is still to be offlined, clear its
+ * offline_pending flag and return its device with a reference held.
+ */
+ list_for_each_entry(pn, &adev->physical_node_list, node)
+ if (pn->offline_pending) {
+ pn->offline_pending = false;
+ return get_device(pn->dev);
+ }
+
+ return NULL;
+}
+
+static struct device *acpi_next_put_online(struct acpi_device *adev)
+{
+ struct acpi_device_physical_node *pn;
+
+ lockdep_assert_held(&adev->physical_node_lock);
+
+ /*
+ * Find the first physical node that has been offlined by
+ * acpi_bus_offline(), clear its put_online flag and return its device
+ * with a reference held.
+ */
+ list_for_each_entry(pn, &adev->physical_node_list, node)
+ if (pn->put_online) {
+ pn->put_online = false;
+ return get_device(pn->dev);
+ }
+
+ return NULL;
+}
+
static acpi_status acpi_bus_offline(acpi_handle handle, u32 lvl, void *data,
void **ret_p)
{
@@ -140,6 +189,7 @@ static acpi_status acpi_bus_offline(acpi_handle handle, u32 lvl, void *data,
struct acpi_device_physical_node *pn;
bool second_pass = (bool)data;
acpi_status status = AE_OK;
+ struct device *dev;
if (!device)
return AE_OK;
@@ -152,26 +202,43 @@ static acpi_status acpi_bus_offline(acpi_handle handle, u32 lvl, void *data,
mutex_lock(&device->physical_node_lock);
list_for_each_entry(pn, &device->physical_node_list, node) {
- int ret;
-
if (second_pass) {
/* Skip devices offlined by the first pass. */
- if (pn->put_online)
- continue;
+ pn->offline_pending = !pn->put_online;
} else {
+ pn->offline_pending = true;
pn->put_online = false;
}
- ret = device_offline(pn->dev);
+ }
+
+ /*
+ * Drop physical_node_lock around device_offline(), because the latter
+ * acquires locks, like the CPU hotplug ones, that must not nest under
+ * it.
+ */
+ while ((dev = acpi_next_offline_pending(device))) {
+ int ret;
+
+ mutex_unlock(&device->physical_node_lock);
+ ret = device_offline(dev);
+ mutex_lock(&device->physical_node_lock);
+
if (ret >= 0) {
- pn->put_online = !ret;
+ /* The device may have been unbound in the meantime. */
+ pn = acpi_find_physical_node(device, dev);
+ if (pn)
+ pn->put_online = !ret;
} else {
*ret_p = &device->dev;
if (second_pass) {
- dev_warn(pn->dev, "Offline failed.\n");
+ dev_warn(dev, "Offline failed.\n");
status = AE_ERROR;
- break;
}
}
+
+ put_device(dev);
+ if (ACPI_FAILURE(status))
+ break;
}
mutex_unlock(&device->physical_node_lock);
@@ -183,18 +250,19 @@ static acpi_status acpi_bus_online(acpi_handle handle, u32 lvl, void *data,
void **ret_p)
{
struct acpi_device *device = acpi_fetch_acpi_dev(handle);
- struct acpi_device_physical_node *pn;
+ struct device *dev;
if (!device)
return AE_OK;
mutex_lock(&device->physical_node_lock);
- list_for_each_entry(pn, &device->physical_node_list, node)
- if (pn->put_online) {
- device_online(pn->dev);
- pn->put_online = false;
- }
+ while ((dev = acpi_next_put_online(device))) {
+ mutex_unlock(&device->physical_node_lock);
+ device_online(dev);
+ mutex_lock(&device->physical_node_lock);
+ put_device(dev);
+ }
mutex_unlock(&device->physical_node_lock);
diff --git a/include/acpi/acpi_bus.h b/include/acpi/acpi_bus.h
index a755b15af9db9..29c59ea1e3e48 100644
--- a/include/acpi/acpi_bus.h
+++ b/include/acpi/acpi_bus.h
@@ -320,6 +320,7 @@ struct acpi_device_physical_node {
struct device *dev;
unsigned int node_id;
bool put_online:1;
+ bool offline_pending:1;
};
struct acpi_device_properties {
--
2.53.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-29 3:45 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29 3:45 [PATCH v2 0/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
2026-10-29 3:45 ` [PATCH v2 1/2] ACPI: scan: Print the offline failure message in acpi_bus_offline() Sasha Levin
2026-10-29 3:45 ` [PATCH v2 2/2] ACPI: scan: Do not hold physical_node_lock across device_offline() and device_online() Sasha Levin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox