From: Heikki Krogerus <heikki.krogerus@linux.intel.com>
To: "Iván Ezequiel Rodriguez" <ivanrwcm25@gmail.com>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Fan Wu <fanwu01@zju.edu.cn>, Wei Huang <huangwei@kylinos.cn>,
linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org,
stable@vger.kernel.org
Subject: Re: [PATCH v2] usb: typec: ucsi: acpi: fix use-after-free on driver removal
Date: Mon, 7 Sep 2026 13:55:10 +0200 [thread overview]
Message-ID: <ap6mHnPJKKJFapsO@black.igk.intel.com> (raw)
In-Reply-To: <20260903232121.271776-1-ivanrwcm25@gmail.com>
On Thu, Sep 03, 2026 at 08:21:21PM -0300, Iván Ezequiel Rodriguez wrote:
> 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>
You could have used guard(mutex) in ucsi_acpi_notify(), but that's
not a huge problem.
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.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
--
heikki
next prev parent reply other threads:[~2026-09-07 11:55 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
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-07 11:55 ` Heikki Krogerus [this message]
2026-09-07 21:01 ` [RFC] usb: typec: ucsi: add software PPM harness for teardown races Iván Ezequiel Rodriguez
2026-09-04 3:37 ` [PATCH] usb: typec: ucsi: fix teardown races with late notifications Huang Wei
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=ap6mHnPJKKJFapsO@black.igk.intel.com \
--to=heikki.krogerus@linux.intel.com \
--cc=fanwu01@zju.edu.cn \
--cc=gregkh@linuxfoundation.org \
--cc=huangwei@kylinos.cn \
--cc=ivanrwcm25@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=stable@vger.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.