mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: sre@kernel.org
Cc: linux-pm@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: power: supply: core: changed work can run after unregister and  use freed LED triggers
Date: Tue, 10 Nov 2026 21:21:48 +0800 (GMT+08:00)	[thread overview]
Message-ID: <62ee92da.22b1d.1a1c59f387a.Coremail.xuhx@hust.edu.cn> (raw)
In-Reply-To: <2026102727-data-sequel-ce41@gregkh>

[-- Attachment #1: Type: text/plain, Size: 6156 bytes --]

power_supply_changed() has no removing check and always schedules
psy->changed_work.  power_supply_unregister() sets removing and calls
cancel_work_sync() once, but a driver can call power_supply_changed()
after that point (from its own work or IRQ) and requeue the work.
changed_work then runs after power_supply_remove_triggers() has freed
the LED triggers; the trigger pointers are not cleared, so update_leds()
writes through freed memory.  KASAN reproduced the trigger UAF on 7.3.

This is a 7.3 report.  The 9f7f7a859451 guard on the get/set wrappers is
not an explicit changed_work fix, but it masks the LED dereference below
because update_leds() first calls power_supply_get_property(); on a
kernel with 9f7f that call returns -ENODEV and the trigger write is
skipped.  changed_work still runs after unregister and the trigger
pointers are still dangling, so I report this as 7.3 exposure and as an
explicit changed_work/trigger cleanup gap, not as a new mainline UAF.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 (unmodified kernel.org 7.3 source tree), gcc 13.3.0,
  x86_64, SMP PREEMPT_DYNAMIC, KASAN, QEMU/KVM.
  CONFIG_POWER_SUPPLY=y, CONFIG_POWER_SUPPLY_HWMON=y,
  CONFIG_LEDS_TRIGGERS=y, CONFIG_MODULES=y.  No source changes.
  The 7.3 tree I tested has no removing check in power_supply_changed()
  and no NULL store in power_supply_remove_triggers().  I did not run a
  9f7f-patched kernel; the masking statement above follows from the
  guard in power_supply_get_property() and the first call in
  update_leds().

* Mechanism

  power_supply_changed() (:150-162):

      spin_lock_irqsave(&psy->changed_lock, flags);
      psy->changed = true;
      pm_stay_awake(&psy->dev);
      spin_unlock_irqrestore(&psy->changed_lock, flags);
      schedule_work(&psy->changed_work);

  There is no psy->removing check.

  unregister (:1928-1951):

      WARN_ON(atomic_dec_return(&psy->use_cnt));
      scoped_guard(rwsem_write, &psy->epc_sem)
          psy->removing = true;
      cancel_work_sync(&psy->changed_work);
      cancel_delayed_work_sync(&psy->deferred_register_work);
      ...
      power_supply_remove_triggers(psy);
      ...
      device_unregister(&psy->dev);

  The cancel only cancels work queued before the call.  Any
  power_supply_changed() after :1942 queues it again; changed_work has
  no removing check either (:78-124).

  power_supply_remove_triggers() (power_supply_leds.c:207-215)
  unregisters and frees the online/status/etc triggers but does not set
  psy->online_trig/psy->status_trig to NULL.  update_online_leds()
  (:157-167) calls led_trigger_event(psy->online_trig, ...) after a
  successful get_property(ONLINE); update_status_leds() (:90-134) does
  the same for status.  A consumer reference can keep the struct
  power_supply alive, but it does not keep the triggers alive: they are
  freed by unregister.  changed_work then writes to freed trigger
  memory.

  If the caller has no reference, the struct power_supply itself can be
  freed first; that is the driver-side 5A variant (bq25890/bq24190).
  This report is the core-side 5B variant: the psy is still referenced,
  but the triggers are gone.

* Concrete corruption / impact

  KASAN reports a slab-use-after-free write in led_trigger_event() from
  power_supply_changed_work:

      BUG: KASAN: slab-use-after-free in led_trigger_event+0x1f/0x80
      Write of size 4 at addr ff1100000368b918
      ...
      BUG: KASAN: slab-use-after-free in led_trigger_event+0x2f/0x80
      Read of size 8

  The freed object is the LED trigger allocated by
  power_supply_register_led_trigger() and freed by
  power_supply_remove_triggers().  The write can corrupt the LED
  trigger list or the LED class device; at minimum it is a KASAN
  splat/panic.  I have not demonstrated privilege escalation.

* Trigger model

  A consumer must hold a power_supply reference (so use_cnt > 0 and the
  get_property(ONLINE) in update_leds() can succeed), and a driver must
  call power_supply_changed() after unregister.  On 7.3 without the 9f7f
  guard this reaches the freed trigger; with 9f7f the trigger write is
  skipped and the residual issue is changed_work running after
  unregister.  In-tree asynchronous
  callers include bq25890's pump work and bq24190's threaded IRQ; both
  are reported separately as 5A because they can also free the psy
  itself.  The core path is generic and can be exercised by any driver
  that calls changed from a work/IRQ without a remove-order guarantee.
  The test module is loaded by root; real triggers are unbind/remove or
  hardware events, not a generic unprivileged interface.

* Reproducer / results

  psrace.c (attached), plan=ps5.  The module registers a USB
  psy with ONLINE, keeps a get_by_name() reference, unregisters the psy
  (freeing the online trigger), calls power_supply_changed(), sleeps for
  changed_work, and finally puts the reference.

  Observed on 7.3:

      online_trig=ff1100000368b900
      held=ff11000003707000
      unregister
      WARNING: ... power_supply_core.c:1930 ...
      after unregister online_trig=ff1100000368b900
      BUG: KASAN: slab-use-after-free in led_trigger_event+0x1f/0x80
      Write of size 4 at addr ff1100000368b918
      task kworker/3:0/34, Workqueue: power_supply_changed_work

  A control run (control=1, no changed after unregister) reports no
  KASAN.  Full logs are available on request.



* Suggested direction

  If 9f7f is backported, the LED dereference is already masked; the
  remaining cleanup is still worth doing.  Under psy->changed_lock, have
  power_supply_changed() reject psy->removing; set removing under the
  same lock in unregister before cancel_work_sync(); and recheck
  removing at the top of power_supply_changed_work().  Also set the
  trigger pointers to NULL in power_supply_remove_triggers() and make
  update_leds() tolerate NULL.  This is defense in depth; drivers must
  still quiesce their async callers before unregister.  Report-first,
  no patch attached; happy to test a proposal.


  Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: psrace.c --]
[-- Type: text/plain, Size: 9089 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * psrace.c - minimal power_supply lifetime-race reproducers for the
 * v7.3 audit report docs/manual-review-drivers-power-2026-11-3.md.
 *
 * action=3 : Case 3, get_property() after power_supply_unregister()/kfree(drvdata)
 * action=4 : Case 4, get_system_batteries() resurrects an unregistered battery
 * action=5 : Case 5B, power_supply_changed() after unregister frees LED triggers
 *
 * The module only uses EXPORT_SYMBOL_GPL power_supply APIs; the test psy is a
 * normal class device whose private data is deliberately freed after unregister.
 */
#define pr_fmt(fmt) "PSRACE: " fmt

#include <linux/atomic.h>
#include <linux/completion.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/kthread.h>
#include <linux/module.h>
#include <linux/power_supply.h>
#include <linux/slab.h>
#include <linux/wait.h>

static int action = 3;
module_param(action, int, 0444);
MODULE_PARM_DESC(action, "3=get_property UAF, 4=system-battery resurrection, 5=changed-after-unregister");

static bool control;
module_param(control, bool, 0444);
MODULE_PARM_DESC(control, "1=negative control: do not free/trigger the racy object");

struct ps_priv {
	int magic;
};

static struct device *root;
static struct power_supply *psy;
static struct ps_priv *priv;
static struct task_struct *thr;
static struct completion entered;
static struct completion finished;
static atomic_t armed = ATOMIC_INIT(0);
static atomic_t scope_calls = ATOMIC_INIT(0);
static struct power_supply **sysbatt;
static int sysbatt_cnt;

static int ps_get_property(struct power_supply *psy,
			   enum power_supply_property psp,
			   union power_supply_propval *val)
{
	struct ps_priv *p = power_supply_get_drvdata(psy);

	switch (psp) {
	case POWER_SUPPLY_PROP_PRESENT:
		if (atomic_read(&armed)) {
			pr_info("case3 get_property entered, drvdata=%px\n", p);
			complete(&entered);
			msleep(500);
			val->intval = READ_ONCE(p->magic);
			pr_info("case3 get_property read %#x after unregister\n",
				val->intval);
			complete(&finished);
			return 0;
		}
		/* Case 4 reads drvdata after the battery has been unregistered. */
		val->intval = (action == 4) ? READ_ONCE(p->magic) : 1;
		return 0;
	case POWER_SUPPLY_PROP_ONLINE:
		val->intval = 1;
		return 0;
	case POWER_SUPPLY_PROP_SCOPE:
		/* The first call is the count pass; the second is populate. */
		if (atomic_inc_return(&scope_calls) == 2) {
			pr_info("case4 populate scope callback entered, drvdata=%px\n", p);
			complete(&entered);
			msleep(500);
		}
		val->intval = POWER_SUPPLY_SCOPE_SYSTEM;
		return 0;
	default:
		return -EINVAL;
	}
}

static const enum power_supply_property props3[] = {
	POWER_SUPPLY_PROP_PRESENT,
};
static const enum power_supply_property props4[] = {
	POWER_SUPPLY_PROP_SCOPE,
	POWER_SUPPLY_PROP_PRESENT,
};
static const enum power_supply_property props5[] = {
	POWER_SUPPLY_PROP_ONLINE,
};

static const struct power_supply_desc desc3 = {
	.name = "psrace3",
	.type = POWER_SUPPLY_TYPE_USB,
	.properties = props3,
	.num_properties = ARRAY_SIZE(props3),
	.get_property = ps_get_property,
};
static const struct power_supply_desc desc4 = {
	.name = "psrace4",
	.type = POWER_SUPPLY_TYPE_BATTERY,
	.properties = props4,
	.num_properties = ARRAY_SIZE(props4),
	.get_property = ps_get_property,
};
static const struct power_supply_desc desc5 = {
	.name = "psrace5",
	.type = POWER_SUPPLY_TYPE_USB,
	.properties = props5,
	.num_properties = ARRAY_SIZE(props5),
	.get_property = ps_get_property,
};

static int consumer3(void *unused)
{
	union power_supply_propval val;
	struct power_supply *p;

	msleep(100);
	p = power_supply_get_by_name("psrace3");
	if (!p) {
		pr_err("case3 get_by_name failed\n");
		complete(&finished);
		return 0;
	}
	pr_info("case3 consumer got %px\n", p);
	atomic_set(&armed, 1);
	smp_mb();
	power_supply_get_property(p, POWER_SUPPLY_PROP_PRESENT, &val);
	power_supply_put(p);
	pr_info("case3 consumer put, done\n");
	return 0;
}

static int sysbatt_thread(void *unused)
{
	sysbatt_cnt = power_supply_get_system_batteries(root, &sysbatt);
	pr_info("case4 get_system_batteries -> %d\n", sysbatt_cnt);
	complete(&finished);
	return 0;
}

static int run_case3(void)
{
	int ret;

	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	priv->magic = 0x12345678;

	psy = power_supply_register(root, &desc3, &(struct power_supply_config){
		.drv_data = priv,
		.no_wakeup_source = true,
	});
	if (IS_ERR(psy)) {
		ret = PTR_ERR(psy);
		pr_err("case3 register failed %d\n", ret);
		kfree(priv);
		return ret;
	}

	thr = kthread_run(consumer3, NULL, "psrace3c");
	if (IS_ERR(thr))
		return PTR_ERR(thr);

	ret = wait_for_completion_timeout(&entered, 5 * HZ);
	if (!ret) {
		pr_err("case3 callback did not enter\n");
		return -ETIMEDOUT;
	}

	if (control) {
		wait_for_completion_timeout(&finished, 5 * HZ);
		pr_info("case3 control: callback completed before free\n");
		msleep(500);
		power_supply_unregister(psy);
		kfree(priv);
		priv = NULL;
		pr_info("case3 control done (no KASAN expected)\n");
		return 0;
	}

	pr_info("case3 unregister + kfree(drvdata)\n");
	power_supply_unregister(psy);
	kfree(priv);
	priv = NULL;

	/* callback wakes up and reads freed drvdata */
	msleep(1000);
	pr_info("case3 init done\n");
	return 0;
}

static int run_case4(void)
{
	int ret;
	union power_supply_propval val;

	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	priv->magic = 0x4b4b4b4b;

	psy = power_supply_register(root, &desc4, &(struct power_supply_config){
		.drv_data = priv,
		.no_wakeup_source = true,
	});
	if (IS_ERR(psy)) {
		ret = PTR_ERR(psy);
		pr_err("case4 register failed %d\n", ret);
		kfree(priv);
		return ret;
	}

	thr = kthread_run(sysbatt_thread, NULL, "psrace4s");
	if (IS_ERR(thr))
		return PTR_ERR(thr);

	ret = wait_for_completion_timeout(&entered, 5 * HZ);
	if (!ret) {
		pr_err("case4 populate callback did not enter\n");
		return -ETIMEDOUT;
	}

	if (control) {
		wait_for_completion_timeout(&finished, 5 * HZ);
		if (sysbatt_cnt > 0) {
			pr_info("case4 control: read array[0]=%px before free\n",
				sysbatt[0]);
			power_supply_get_property(sysbatt[0],
						  POWER_SUPPLY_PROP_PRESENT,
						  &val);
			power_supply_put_system_batteries(sysbatt, sysbatt_cnt);
		}
		power_supply_unregister(psy);
		kfree(priv);
		priv = NULL;
		pr_info("case4 control done (no KASAN expected)\n");
		return 0;
	}

	pr_info("case4 unregister + kfree(drvdata) during populate\n");
	power_supply_unregister(psy);
	kfree(priv);
	priv = NULL;

	ret = wait_for_completion_timeout(&finished, 5 * HZ);
	if (!ret) {
		pr_err("case4 get_system_batteries did not finish\n");
		return -ETIMEDOUT;
	}

	if (sysbatt_cnt <= 0) {
		pr_err("case4 no battery resurrected (%d)\n", sysbatt_cnt);
		return -ENOENT;
	}

	pr_info("case4 array[0]=%px (unregistered battery), now get_property\n",
		sysbatt[0]);
	power_supply_get_property(sysbatt[0], POWER_SUPPLY_PROP_PRESENT, &val);
	power_supply_put_system_batteries(sysbatt, sysbatt_cnt);
	pr_info("case4 init done\n");
	return 0;
}

static int run_case5(void)
{
	int ret;

	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	priv->magic = 0x5a5a5a5a;

	psy = power_supply_register(root, &desc5, &(struct power_supply_config){
		.drv_data = priv,
		.no_wakeup_source = true,
	});
	if (IS_ERR(psy)) {
		ret = PTR_ERR(psy);
		pr_err("case5 register failed %d\n", ret);
		kfree(priv);
		return ret;
	}

	pr_info("case5 online_trig=%px\n", psy->online_trig);
	if (!psy->online_trig) {
		pr_err("case5 no online trigger, abort\n");
		return -ENOENT;
	}

	/*
	 * Emulate a consumer that keeps both the struct power_supply and a
	 * positive use_cnt alive.  Without the use_cnt, get_property() in
	 * changed_work would return -ENODEV before reaching the trigger.
	 */
	{
		struct power_supply *held = power_supply_get_by_name("psrace5");

		if (!held) {
			pr_err("case5 get_by_name failed\n");
			return -ENOENT;
		}
		pr_info("case5 held=%px use_cnt now >0\n", held);
	}
	pr_info("case5 unregister (frees LED trigger), then changed\n");
	power_supply_unregister(psy);
	pr_info("case5 after unregister online_trig=%px\n", psy->online_trig);
	if (control) {
		msleep(1000);
		power_supply_put(psy);
		pr_info("case5 control done (no changed, no KASAN expected)\n");
		return 0;
	}
	power_supply_changed(psy);
	msleep(1000);
	/* Drop the consumer reference only after changed_work has run. */
	power_supply_put(psy);
	pr_info("case5 init done\n");
	return 0;
}

static int __init psrace_init(void)
{
	int ret;

	root = root_device_register("psrace-root");
	if (IS_ERR(root))
		return PTR_ERR(root);
	init_completion(&entered);
	init_completion(&finished);

	switch (action) {
	case 3:
		ret = run_case3();
		break;
	case 4:
		ret = run_case4();
		break;
	case 5:
		ret = run_case5();
		break;
	default:
		pr_err("unknown action %d\n", action);
		ret = -EINVAL;
	}
	pr_info("action=%d init ret=%d\n", action, ret);
	return ret;
}

module_init(psrace_init);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("power_supply v7.3 lifetime-race reproducer");

  parent reply	other threads:[~2026-11-10 13:21 UTC|newest]

Thread overview: 32+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
2026-10-27 14:12 ` Greg KH
2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
2026-11-04  9:41     ` Qing Wang
2026-11-04 10:07       ` Qing Wang
2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
2026-11-03 14:51     ` Sasha Levin
2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
2026-11-03 15:49     ` Eric Dumazet
2026-11-04 15:03     ` Sasha Levin
2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
2026-11-05  8:52     ` Linus Walleij
2026-11-04 12:54   ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
2026-11-04 13:04     ` Greg KH
2026-11-04 12:57   ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
2026-11-04 13:28     ` Greg KH
2026-11-04 12:58   ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
2026-11-04 13:00   ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
2026-11-04 13:01   ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
2026-11-04 13:02   ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
2026-11-10 12:56   ` driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF) 徐晗翔
2026-11-10 13:00   ` 徐晗翔
2026-11-10 13:05   ` ACPI: CPPC: per-CPU cpc_desc is freed on processor unbind without clearing the published pointer, causing UAF in cpufreq accessors 徐晗翔
2026-11-10 13:07   ` ACPI: CPPC: shared PCC subspace data can be freed while another CPU probes, reads, or tears it down 徐晗翔
2026-11-10 13:11   ` ACPI: ACPICA: Reset can delete an event semaphore while another thread is blocked in Wait 徐晗翔
2026-11-10 13:14   ` power: supply: bq25890: pump-express work can call power_supply_changed after the psy is unregistered 徐晗翔
2026-11-10 13:17   ` power: supply: core: get/set property callbacks can run after unregister and read freed driver data 徐晗翔
2026-11-10 13:18   ` power: supply: core: system-battery enumeration can return an unregistered battery 徐晗翔
2026-11-10 13:21   ` 徐晗翔 [this message]
2026-11-10 13:23   ` power: supply: bq24190: IRQ handler can call power_supply_changed after the psy is unregistered 徐晗翔

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=62ee92da.22b1d.1a1c59f387a.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pm@vger.kernel.org \
    --cc=sre@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®