[PATCH 2/2] ACPI: battery: Merge consecutive battery notifications

From: Rong Zhang

Date: Tue May 26 2026 - 14:35:42 EST


It's a very common pattern to emit consecutive battery notifications,
for example:

Method (_Qxx, 0, NotSerialized)
{
Notify (BAT0, 0x80) // Status Change
Notify (BAT0, 0x81) // Information Change
}

In this case, the current code path will update battery state twice
within a short period, which is not optimal, as the same data are
fetched twice. Moreover, both notifications are likely to call
power_supply_changed(), causing power_supply_uevent() to read all
battery properties in order to assemble uevents. Even worse, after the
first uevent reaches userspace, some userspace processes start to read
all battery properties in order to refresh their internal states, which
competes with the second notification's handling and uevent assembling.

This generates significant pressure on _STA, _BST and _BIX/_BIF methods.
Not only that, power_supply_ext properties may also rely on some other
ACPI methods, so both uevent assembling and userspace processes call
them. It becomes a nightmare when all these methods share the same ACPI
mutex protecting EC accesses and hence vulnerable to lock starvation.
This is exactly the case of some Lenovo devices, where the mentioned EC
query pattern eventually leads to a catastrophic situation that a bunch
of ACPI methods (including but not limited to the mentioned ones) fail
to acquire the same mutex due to timeout. These devices don't handle
mutex acquisition failure gracefully and return garbage data, causing
even more chaos.

Improve battery notification handling by merging consecutive battery
notifications within 10ms using a delayed work, so that they only
refresh and/or update battery state once. ACPI netlink event and
notifier call chain are still triggered multiple times in order not to
break other components. Finally, call power_supply_changed() once and
lead to a single uevent instead of a bunch, preventing userspace
programs from causing too much pressure on power supply properties and
underlying ACPI methods.

Tested-by: Jeffrey Wälti <jeffrey@xxxxxxxxxx>
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: Rick <rickk1166@xxxxxxxxx>
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221065
Signed-off-by: Rong Zhang <i@xxxxxxxx>
---
drivers/acpi/battery.c | 73 +++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 63 insertions(+), 10 deletions(-)

diff --git a/drivers/acpi/battery.c b/drivers/acpi/battery.c
index 5f06841b48a1..f6e2b0d8e878 100644
--- a/drivers/acpi/battery.c
+++ b/drivers/acpi/battery.c
@@ -14,6 +14,7 @@
#include <linux/dmi.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
+#include <linux/kfifo.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -21,6 +22,7 @@
#include <linux/slab.h>
#include <linux/suspend.h>
#include <linux/types.h>
+#include <linux/workqueue.h>

#include <linux/unaligned.h>

@@ -43,6 +45,9 @@

#define MAX_STRING_LENGTH 64

+#define MAX_QUEUED_EVENTS 16
+#define NOTIF_MERGING_MS 10
+
MODULE_AUTHOR("Paul Diefenbaugh");
MODULE_AUTHOR("Alexey Starikovskiy <astarikovskiy@xxxxxxx>");
MODULE_DESCRIPTION("ACPI Battery Driver");
@@ -95,6 +100,8 @@ struct acpi_battery {
struct power_supply_desc bat_desc;
struct acpi_device *device;
struct device *phys_dev;
+ struct kfifo acpi_notif_fifo;
+ struct delayed_work acpi_notif_dwork;
struct notifier_block pm_nb;
struct list_head list;
unsigned long update_time;
@@ -1067,14 +1074,22 @@ static void acpi_battery_refresh(struct acpi_battery *battery)
}

/* Driver Interface */
-static void acpi_battery_notify(acpi_handle handle, u32 event, void *data)
+static void acpi_battery_notification_worker(struct work_struct *work)
{
- struct acpi_battery *battery = data;
+ struct acpi_battery *battery = container_of(work, struct acpi_battery,
+ acpi_notif_dwork.work);
struct acpi_device *device = battery->device;
+ u32 events[MAX_QUEUED_EVENTS];
struct power_supply *old;
+ unsigned int count, i;

guard(mutex)(&battery->update_lock);

+ count = kfifo_out(&battery->acpi_notif_fifo, events, sizeof(events));
+ count /= sizeof(events[0]);
+
+ pr_debug("merged %u battery notifications within %dms\n", count, NOTIF_MERGING_MS);
+
old = battery->bat;
/*
* On Acer Aspire V5-573G notifications are sometimes triggered too
@@ -1084,19 +1099,45 @@ static void acpi_battery_notify(acpi_handle handle, u32 event, void *data)
*/
if (battery_notification_delay_ms > 0)
msleep(battery_notification_delay_ms);
- if (event == ACPI_BATTERY_NOTIFY_INFO)
- acpi_battery_refresh(battery);
+
+ for (i = 0; i < count; i++) {
+ if (events[i] == ACPI_BATTERY_NOTIFY_INFO) {
+ acpi_battery_refresh(battery);
+ break;
+ }
+ }
+
acpi_battery_update(battery, false);
- acpi_bus_generate_netlink_event(ACPI_BATTERY_CLASS,
- dev_name(&device->dev), event,
- acpi_battery_present(battery));
- acpi_notifier_call_chain(ACPI_BATTERY_CLASS, acpi_device_bid(device),
- event, acpi_battery_present(battery));
+
+ for (i = 0; i < count; i++) {
+ acpi_bus_generate_netlink_event(ACPI_BATTERY_CLASS,
+ dev_name(&device->dev), events[i],
+ acpi_battery_present(battery));
+ acpi_notifier_call_chain(ACPI_BATTERY_CLASS, acpi_device_bid(device),
+ events[i], acpi_battery_present(battery));
+ }
+
/* acpi_battery_update could remove power_supply object */
if (old && battery->bat)
power_supply_changed(battery->bat);
}

+static void acpi_battery_notify(acpi_handle handle, u32 event, void *data)
+{
+ struct acpi_battery *battery = data;
+
+ guard(mutex)(&battery->update_lock);
+
+ if (kfifo_avail(&battery->acpi_notif_fifo) < sizeof(event)) {
+ pr_err("too many battery notifications within %dms", NOTIF_MERGING_MS);
+ return;
+ }
+
+ kfifo_in(&battery->acpi_notif_fifo, &event, sizeof(event));
+
+ schedule_delayed_work(&battery->acpi_notif_dwork, msecs_to_jiffies(NOTIF_MERGING_MS));
+}
+
static int battery_notify(struct notifier_block *nb,
unsigned long mode, void *_unused)
{
@@ -1264,13 +1305,22 @@ static int acpi_battery_probe(struct platform_device *pdev)

device_init_wakeup(&pdev->dev, true);

+ result = kfifo_alloc(&battery->acpi_notif_fifo,
+ MAX_QUEUED_EVENTS * sizeof(u32), GFP_KERNEL);
+ if (result)
+ goto fail_pm;
+
+ INIT_DELAYED_WORK(&battery->acpi_notif_dwork, acpi_battery_notification_worker);
+
result = acpi_dev_install_notify_handler(device, ACPI_ALL_NOTIFY,
acpi_battery_notify, battery);
if (result)
- goto fail_pm;
+ goto fail_kfifo;

return 0;

+fail_kfifo:
+ kfifo_free(&battery->acpi_notif_fifo);
fail_pm:
device_init_wakeup(&pdev->dev, false);
unregister_pm_notifier(&battery->pm_nb);
@@ -1287,6 +1337,9 @@ static void acpi_battery_remove(struct platform_device *pdev)
acpi_dev_remove_notify_handler(battery->device, ACPI_ALL_NOTIFY,
acpi_battery_notify);

+ cancel_delayed_work_sync(&battery->acpi_notif_dwork);
+ kfifo_free(&battery->acpi_notif_fifo);
+
device_init_wakeup(&pdev->dev, false);
unregister_pm_notifier(&battery->pm_nb);


--
2.53.0