* [PATCH] usb: typec: ucsi: fix teardown races with late notifications
@ 2026-09-03 3:03 Iván Ezequiel Rodriguez
2026-09-03 8:40 ` Huang Wei
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Iván Ezequiel Rodriguez @ 2026-09-03 3:03 UTC (permalink / raw)
To: Heikki Krogerus, Greg Kroah-Hartman
Cc: linux-usb, linux-kernel, Iván Ezequiel Rodriguez
ucsi_acpi_remove() freed the UCSI instance before removing the ACPI
notify handler. A concurrent notify could call into ucsi_acpi_notify()
and use ua->ucsi after it was destroyed.
Clear ucsi->ntfy before disabling PPM notifications and NULL the
connector array after free so ucsi_connector_change() cannot schedule
work on a dangling connector while a backend still delivers events.
Tested: built drivers/usb/typec/ucsi/ with CONFIG_TYPEC_UCSI=m and
CONFIG_UCSI_ACPI=m via docker kbuild; checkpatch clean.
Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
---
drivers/usb/typec/ucsi/ucsi.c | 11 +++++++++++
drivers/usb/typec/ucsi/ucsi_acpi.c | 11 ++++++++---
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
index bef3f9b71d71..3395614764cf 100644
--- a/drivers/usb/typec/ucsi/ucsi.c
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -2369,6 +2369,15 @@ void ucsi_unregister(struct ucsi *ucsi)
ucsi_debugfs_unregister(ucsi);
+ /*
+ * Stop accepting connector-change events before the PPM disable
+ * command and before freeing connectors. Backends may still deliver
+ * a late notification (e.g. ACPI) until their own handler is removed;
+ * with ntfy cleared, ucsi_connector_change() returns early instead of
+ * scheduling work on a connector that is about to be freed.
+ */
+ ucsi->ntfy = 0;
+
/* Disable notifications */
ucsi->ops->async_control(ucsi, cmd);
@@ -2382,6 +2391,8 @@ void ucsi_unregister(struct ucsi *ucsi)
}
kfree(ucsi->connector);
+ ucsi->connector = NULL;
+ memset(&ucsi->cap, 0, sizeof(ucsi->cap));
}
EXPORT_SYMBOL_GPL(ucsi_unregister);
diff --git a/drivers/usb/typec/ucsi/ucsi_acpi.c b/drivers/usb/typec/ucsi/ucsi_acpi.c
index 18286d3e9cc5..5fc485121dbb 100644
--- a/drivers/usb/typec/ucsi/ucsi_acpi.c
+++ b/drivers/usb/typec/ucsi/ucsi_acpi.c
@@ -256,11 +256,16 @@ static void ucsi_acpi_remove(struct platform_device *pdev)
{
struct ucsi_acpi *ua = platform_get_drvdata(pdev);
- ucsi_unregister(ua->ucsi);
- ucsi_destroy(ua->ucsi);
-
+ /*
+ * Drop the ACPI notify handler before tearing down the UCSI instance.
+ * Otherwise a concurrent notify can race into ucsi_acpi_notify() and
+ * use ua->ucsi after it has been freed.
+ */
acpi_remove_notify_handler(ACPI_HANDLE(&pdev->dev), ACPI_DEVICE_NOTIFY,
ucsi_acpi_notify);
+
+ ucsi_unregister(ua->ucsi);
+ ucsi_destroy(ua->ucsi);
}
static int ucsi_acpi_suspend(struct device *dev)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] usb: typec: ucsi: fix teardown races with late notifications
2026-09-03 3:03 [PATCH] usb: typec: ucsi: fix teardown races with late notifications Iván Ezequiel Rodriguez
@ 2026-09-03 8:40 ` Huang Wei
2026-09-03 23:21 ` [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal Iván Ezequiel Rodriguez
2026-09-04 3:37 ` [PATCH] usb: typec: ucsi: fix teardown races with late notifications Huang Wei
2 siblings, 0 replies; 5+ messages in thread
From: Huang Wei @ 2026-09-03 8:40 UTC (permalink / raw)
To: Iván Ezequiel Rodriguez
Cc: Huang Wei, Heikki Krogerus, Greg Kroah-Hartman, linux-usb,
linux-kernel
Hi Iván,
I've been poking at the ucsi teardown path lately so I took a closer
look at this. The race is real, and moving acpi_remove_notify_handler()
ahead of ucsi_unregister()/ucsi_destroy() is clearly the right call for
the ACPI path.
One thing I couldn't convince myself of: does acpi_remove_notify_handler()
wait for a handler that's already running on another CPU? From what I
can tell ACPICA removes the handler node under the device lock but
doesn't flush an in-flight dispatch, so a notify that's already entered
ucsi_acpi_notify() could still dereference ua->ucsi after the handler
is gone and ucsi_destroy() has freed it. The reorder handles new
notifies fine, I just wasn't sure it handles the one that's already
mid-flight. Did you look at that?
On the ucsi.c changes: setting ntfy to 0 before NULLing the connector
array looks right to me, ucsi_connector_change() bails on ntfy before
it touches the connector. But ucsi_notify_common() also does
complete(&ucsi->complete) and the backend's read_cci(), neither of
which is gated by ntfy. So for the non-ACPI backends (glink, ccg)
that can't be fixed by reordering the ACPI handler -- is the ucsi.c
part actually enough on its own, or do they need their own teardown
ordering? Put differently, is the ACPI reorder the only real fix and
the rest belt-and-suspenders?
Only build-tested too -- would help to know how the race was found
and what makes the reorder sufficient on its own.
Not objecting, I'd just like to understand the in-flight case.
Thanks,
Huang Wei
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal
2026-09-03 3:03 [PATCH] usb: typec: ucsi: fix teardown races with late notifications Iván Ezequiel Rodriguez
2026-09-03 8:40 ` Huang Wei
@ 2026-09-03 23:21 ` Iván Ezequiel Rodriguez
2026-09-04 3:48 ` Huang Wei
2026-09-04 3:37 ` [PATCH] usb: typec: ucsi: fix teardown races with late notifications Huang Wei
2 siblings, 1 reply; 5+ messages in thread
From: Iván Ezequiel Rodriguez @ 2026-09-03 23:21 UTC (permalink / raw)
To: Heikki Krogerus, Greg Kroah-Hartman
Cc: Iván Ezequiel Rodriguez, Fan Wu, Wei Huang, linux-usb,
linux-kernel, stable
ucsi_acpi_remove() destroys the UCSI instance before removing the ACPI
notify handler:
ucsi_unregister(ua->ucsi);
ucsi_destroy(ua->ucsi);
acpi_remove_notify_handler(...);
ucsi_acpi_notify() dereferences ua->ucsi, so a notify arriving after
ucsi_destroy() uses freed memory:
CPU0 CPU1
---- ----
ucsi_acpi_remove()
ucsi_unregister()
ucsi_destroy()
kfree(ucsi)
ucsi_acpi_notify()
ua->ucsi->ops->read_cci() <-- UAF
Simply removing the handler before ucsi_unregister() is not correct
either. ucsi_unregister() drains work that needs the notify path to make
progress: ucsi_handle_connector_change() issues GET_CONNECTOR_STATUS and
ucsi_unregister_port() drains and destroys con->wq, and those commands
block in wait_for_completion_timeout() on ucsi->complete for up to
UCSI_TIMEOUT_MS. That completion is signalled only from
ucsi_notify_common(), i.e. from the notify handler. Tearing the handler
down first would leave cancel_work_sync() and destroy_workqueue()
waiting the full timeout for a completion that can no longer arrive.
Moving the removal between ucsi_unregister() and ucsi_destroy() is not
sufficient on its own: at that point the connector array has already
been freed, so a late notify reaching ucsi_connector_change() would
queue work on a freed connector.
Teardown therefore needs two properties at the same time: no new
connector changes once connectors start going away, but the notify path
still available for command and acknowledge completions until that work
has been drained. Whether the PPM actually produces those completions is
a firmware matter; what changes here is that the path able to deliver
them is no longer torn down first.
Introduce a quiescing state, local to the ACPI backend, that provides
both. ucsi_acpi_remove() sets ua->quiescing under ua->notify_lock before
calling ucsi_unregister(); ucsi_acpi_notify() takes the same lock and,
when quiescing, reduces the CCI to the bits that ucsi_notify_common()
consumes for completions. ucsi_notify_common() looks at exactly
UCSI_CCI_BUSY, the connector number, UCSI_CCI_ACK_COMPLETE and
UCSI_CCI_COMMAND_COMPLETE; keeping the first and the last two preserves
the completion and bogus-data behaviour unchanged, while clearing the
connector number makes ucsi_connector_change() unreachable. The CCI that
the command path inspects is unaffected, because
ucsi_sync_control_common() re-reads it from the interface after the
completion.
The resulting order is:
mutex_lock(&ua->notify_lock);
ua->quiescing = true; -- no new connector work
mutex_unlock(&ua->notify_lock);
ucsi_unregister(); -- drains work, notify path still
available for completions
acpi_remove_notify_handler(); -- unlinks, then flushes
kacpi_notify_wq
ucsi_destroy(); -- no notify can be in flight
which gives the following happens-before chain:
- A notify that acquires notify_lock before ucsi_acpi_remove() runs to
completion while remove() waits on the lock, so any schedule_work() it
performs happens before ucsi_unregister() starts cancelling.
- A notify that acquires notify_lock after remove() released it observes
quiescing == true, so it cannot reach ucsi_connector_change() and
cannot touch ucsi->connector.
- acpi_remove_notify_handler() unlinks the handler and then calls
acpi_os_wait_events_complete(), which flushes kacpi_notify_wq, so a
notify already dispatched on another CPU has returned before
ucsi_destroy() frees the instance.
notify_lock is never held across ucsi_unregister() or
acpi_remove_notify_handler(); holding it there would deadlock against
the notify work those calls wait for. It is only ever taken as a leaf:
ucsi_notify_common() and ucsi_connector_change() take no locks, so it
cannot invert against ucsi->ppm_lock or con->lock, which the drained
work holds while waiting for the completion. The handler runs from
kacpi_notify_wq via acpi_os_execute(OSL_NOTIFY_HANDLER, ...), i.e. in
process context, so sleeping on the mutex is allowed.
The probe error path already removes the handler before ucsi_destroy()
and is left unchanged.
Fixes: f56de278e8ec ("usb: typec: ucsi: acpi: Move to the new API")
Cc: stable@vger.kernel.org
Reported-by: Fan Wu <fanwu01@zju.edu.cn>
Link: https://lore.kernel.org/all/20260718021142.3146566-1-fanwu01@zju.edu.cn/
Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
---
Notes:
Hi Heikki, Greg,
v2 is a rewrite rather than an incremental fixup. v1 moved
acpi_remove_notify_handler() ahead of the teardown, and that ordering is
wrong for the reason Fan Wu had already documented in [1]: the work that
ucsi_unregister() drains can be waiting for a completion that only the
notify path delivers. This version keeps the handler installed across
ucsi_unregister() and adds an ACPI-local quiescing state instead.
Wei Huang asked on v1 whether acpi_remove_notify_handler() waits for a
callback that already entered on another CPU. It does: for
ACPI_DEVICE_NOTIFY it calls acpi_os_wait_events_complete() after
unlinking the handler (drivers/acpi/acpica/evxface.c), that flushes
kacpi_notify_wq (drivers/acpi/osl.c), and device-notify dispatch runs on
that same workqueue through acpi_os_execute(OSL_NOTIFY_HANDLER, ...).
So the raw call is already the barrier, and acpi_dev_remove_notify_handler()
would only add a second flush. Chasing that question is what surfaced the
harder half of the problem, which is what this version is about.
On [1]: that patch kept the handler installed across ucsi_unregister()
for exactly the right reason, and this version preserves that property.
What it did not cover is the window you described in that thread, where a
notify arriving after ucsi_unregister() has freed the connectors still
reaches ucsi_connector_change(). You also asked to keep the solution
inside ucsi_acpi.c rather than redesigning the core, which is what this
does.
Changes since v1:
- Do not remove the notify handler before ucsi_unregister().
- Add the quiescing state, so connector changes stop while the notify
path stays available for completions.
- Drop the ucsi.c changes from v1 (ntfy = 0, connector = NULL, cap = 0).
I could not show they were needed for the other backends, and they do
not belong in the same patch as the ACPI lifetime fix.
- Correct the Fixes: tag. v1 pointed at 8243edf44152, which added the
driver; the current ordering came from f56de278e8ec.
- Credit Fan Wu, who reported the underlying use-after-free first.
- Use mutex_init() rather than devm_mutex_init(), which only appeared in
4cd47222e435 (2024) and would be a needlessly modern dependency for a
fix tagged for stable from a 2019 commit.
Testing
I have no machine that exercises the UCSI ACPI path, so this was tested
with a software PPM backend that drives the real UCSI core
(ucsi_create/ucsi_register/ucsi_unregister/ucsi_destroy/
ucsi_notify_common) and reproduces the ACPI notify protocol, including
the deferral to a percpu workqueue. All three candidate teardown
orderings were run against it: the one from v1, the one from [1] and the
one in this patch. v7.3-rc2, KASAN + lockdep + PROVE_LOCKING +
DEBUG_MUTEXES, QEMU, oops=panic.
Each case below parks a connector work in wait_for_completion_timeout()
before teardown starts, and asserts that precondition rather than
assuming it.
teardown ordering result
------------------------------------ ----------------------------
quiesce, unregister, unlink (this) 148 ms, clean, with a late
connector notify fired after
ucsi_unregister() returned
unlink, unregister (v1) 10595 ms stall
unregister, unlink (as in [1]), a KASAN slab-use-after-free in
connector notify landing in the queue_work_on(), then a GP
window fault in the kworker that
picked up the freed work
nothing in flight (this) 146 ms, clean
Repeated with the teardown starting while ucsi_init_work() is still
running, so that cancel_delayed_work_sync(&ucsi->work) has to drain an
init command parked on the completion: this patch takes 2299 ms and
completes cleanly, of which 1500 ms is the injected command delay, while
the v1 ordering stalls for 10089 ms and the init gives up with
-ETIMEDOUT.
Two deterministic checks of the properties the patch claims:
- CCI mask. A single CCI carrying both connector 1 and COMMAND_COMPLETE
(0x80000002) is delivered while quiescing: the completion is signalled
and EVENT_PENDING stays clear, i.e. ucsi_connector_change() is not
reached. The same CCI with quiescing off sets EVENT_PENDING, so the
check is sensitive to the path it claims to block.
- notify_lock as a barrier. A notify that has entered the handler is
held inside it for 1200 ms; the store of quiescing in the teardown
path blocks for 1215 ms behind it. This is what makes "a notify that
started before teardown finishes its schedule_work() before
ucsi_unregister() begins cancelling" an ordering guarantee rather
than a likelihood.
Soak: 1000 teardown cycles with four threads hammering the notify path
concurrently with the quiesce sequence, repeated at 1, 2, 4 and 8 vCPUs,
so 4000 teardowns in total. No stall, no KASAN report and no lockdep
splat in any configuration. A separate KCSAN build ran 200 of those
cycles at 4 vCPUs with no data race reported in any UCSI path.
What this does not cover: no real ACPI hardware, so the ACPICA drain
described above is established by reading evxface.c and osl.c rather
than by execution; and the LG gram quirk path is untouched and
unexercised.
The harness is not part of this patch. I can post it separately if it
is useful.
[1] https://lore.kernel.org/all/20260718021142.3146566-1-fanwu01@zju.edu.cn/
drivers/usb/typec/ucsi/ucsi_acpi.c | 53 ++++++++++++++++++++++++++++--
1 file changed, 51 insertions(+), 2 deletions(-)
diff --git a/drivers/usb/typec/ucsi/ucsi_acpi.c b/drivers/usb/typec/ucsi/ucsi_acpi.c
index 18286d3e9cc5..61bba7625d17 100644
--- a/drivers/usb/typec/ucsi/ucsi_acpi.c
+++ b/drivers/usb/typec/ucsi/ucsi_acpi.c
@@ -24,6 +24,15 @@ struct ucsi_acpi {
bool check_bogus_event;
guid_t guid;
u64 cmd;
+ /*
+ * notify_lock serialises ucsi_acpi_notify() against the start of
+ * teardown, so that @quiescing is observed by every notify that has
+ * not yet run. It must not be held across ucsi_unregister(), whose
+ * drained work may depend on the notify path, nor across
+ * acpi_remove_notify_handler(), which flushes notify work.
+ */
+ struct mutex notify_lock;
+ bool quiescing;
};
static int ucsi_acpi_dsm(struct ucsi_acpi *ua, int func)
@@ -179,11 +188,31 @@ static void ucsi_acpi_notify(acpi_handle handle, u32 event, void *data)
u32 cci;
int ret;
+ mutex_lock(&ua->notify_lock);
+
ret = ua->ucsi->ops->read_cci(ua->ucsi, &cci);
if (ret)
- return;
+ goto out_unlock;
+
+ /*
+ * Once teardown has started the connectors are being unregistered and
+ * freed, so a connector change must not be reported any more. Command
+ * and acknowledge completions must still be able to reach the core:
+ * ucsi_unregister() drains connector and partner work that can be
+ * blocked in wait_for_completion_timeout() on ucsi->complete, and that
+ * completion is only signalled from here. Keep exactly the bits that
+ * ucsi_notify_common() needs for that, which drops the connector
+ * number and with it the path to ucsi_connector_change(). The busy
+ * indicator is kept so that bogus CCI data is still ignored.
+ */
+ if (ua->quiescing)
+ cci &= UCSI_CCI_BUSY | UCSI_CCI_ACK_COMPLETE |
+ UCSI_CCI_COMMAND_COMPLETE;
ucsi_notify_common(ua->ucsi, cci);
+
+out_unlock:
+ mutex_unlock(&ua->notify_lock);
}
static int ucsi_acpi_probe(struct platform_device *pdev)
@@ -219,6 +248,8 @@ static int ucsi_acpi_probe(struct platform_device *pdev)
ua->dev = &pdev->dev;
+ mutex_init(&ua->notify_lock);
+
id = dmi_first_match(ucsi_acpi_quirks);
if (id)
ops = id->driver_data;
@@ -256,11 +287,29 @@ static void ucsi_acpi_remove(struct platform_device *pdev)
{
struct ucsi_acpi *ua = platform_get_drvdata(pdev);
+ /*
+ * Stop reporting connector changes, but keep the notify handler
+ * installed so that the work ucsi_unregister() drains can still be
+ * reached by the command completions it may be waiting for. Any notify
+ * that already passed this point runs to completion first, so no
+ * connector work can be queued once ucsi_unregister() starts.
+ */
+ mutex_lock(&ua->notify_lock);
+ ua->quiescing = true;
+ mutex_unlock(&ua->notify_lock);
+
ucsi_unregister(ua->ucsi);
- ucsi_destroy(ua->ucsi);
+ /*
+ * Now that no work is left to serve, drop the handler. This unlinks it
+ * and then calls acpi_os_wait_events_complete(), which flushes
+ * kacpi_notify_wq, so a notify running on another CPU has returned
+ * before ucsi_destroy() frees the instance that it dereferences.
+ */
acpi_remove_notify_handler(ACPI_HANDLE(&pdev->dev), ACPI_DEVICE_NOTIFY,
ucsi_acpi_notify);
+
+ ucsi_destroy(ua->ucsi);
}
static int ucsi_acpi_suspend(struct device *dev)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH] usb: typec: ucsi: fix teardown races with late notifications
2026-09-03 3:03 [PATCH] usb: typec: ucsi: fix teardown races with late notifications Iván Ezequiel Rodriguez
2026-09-03 8:40 ` Huang Wei
2026-09-03 23:21 ` [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal Iván Ezequiel Rodriguez
@ 2026-09-04 3:37 ` Huang Wei
2 siblings, 0 replies; 5+ messages in thread
From: Huang Wei @ 2026-09-04 3:37 UTC (permalink / raw)
To: Iván Ezequiel Rodriguez
Cc: Huang Wei, Heikki Krogerus, Greg Kroah-Hartman, linux-usb,
linux-kernel
Hi Iván,
I've been poking at the ucsi teardown path lately so I took a closer
look at this. The race is real, and moving acpi_remove_notify_handler()
ahead of ucsi_unregister()/ucsi_destroy() is clearly the right call for
the ACPI path.
One thing I couldn't convince myself of: does acpi_remove_notify_handler()
wait for a handler that's already running on another CPU? From what I
can tell ACPICA removes the handler node under the device lock but
doesn't flush an in-flight dispatch, so a notify that's already entered
ucsi_acpi_notify() could still dereference ua->ucsi after the handler
is gone and ucsi_destroy() has freed it. The reorder handles new
notifies fine, I just wasn't sure it handles the one that's already
mid-flight. Did you look at that?
On the ucsi.c changes: setting ntfy to 0 before NULLing the connector
array looks right to me, ucsi_connector_change() bails on ntfy before
it touches the connector. But ucsi_notify_common() also does
complete(&ucsi->complete) and the backend's read_cci(), neither of
which is gated by ntfy. So for the non-ACPI backends (glink, ccg)
that can't be fixed by reordering the ACPI handler -- is the ucsi.c
part actually enough on its own, or do they need their own teardown
ordering? Put differently, is the ACPI reorder the only real fix and
the rest belt-and-suspenders?
Only build-tested too -- would help to know how the race was found
and what makes the reorder sufficient on its own.
Not objecting, I'd just like to understand the in-flight case.
Thanks,
Huang Wei
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal
2026-09-03 23:21 ` [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal Iván Ezequiel Rodriguez
@ 2026-09-04 3:48 ` Huang Wei
0 siblings, 0 replies; 5+ messages in thread
From: Huang Wei @ 2026-09-04 3:48 UTC (permalink / raw)
To: Iván Ezequiel Rodriguez
Cc: Huang Wei, Heikki Krogerus, Greg Kroah-Hartman, Fan Wu, linux-usb,
linux-kernel, stable
Hi Iván,
This is a nice piece of work. (And apologies for the duplicate of my
earlier note on v1 -- that was an accidental resend.)
I went and checked the ACPICA side after reading your notes:
acpi_remove_notify_handler() does call acpi_os_wait_events_complete()
on the device-notify path, so the flush argument holds up. The stall
you measured for the v1 ordering is a good catch too -- I was happy
enough with the plain reorder on v1 and would have missed that
ucsi_unregister() depends on the notify path for its completions.
The quiescing + CCI masking looks right to me, and keeping the fix
ACPI-local answers my question about the other backends.
If you do post the harness separately I'd find it useful -- the
ordering comparison alone is worth having in the archives.
Reviewed-by: Huang Wei <huangwei@kylinos.cn>
Thanks,
Huang Wei
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-04 3:48 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-03 3:03 [PATCH] usb: typec: ucsi: fix teardown races with late notifications Iván Ezequiel Rodriguez
2026-09-03 8:40 ` Huang Wei
2026-09-03 23:21 ` [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal Iván Ezequiel Rodriguez
2026-09-04 3:48 ` Huang Wei
2026-09-04 3:37 ` [PATCH] usb: typec: ucsi: fix teardown races with late notifications Huang Wei
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox