mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Iván Ezequiel Rodriguez" <ivanrwcm25@gmail.com>
To: Heikki Krogerus <heikki.krogerus@linux.intel.com>,
	Huang Wei <huangwei@kylinos.cn>
Cc: "Greg Kroah-Hartman" <gregkh@linuxfoundation.org>,
	"Fan Wu" <fanwu01@zju.edu.cn>,
	linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org,
	"Iván Ezequiel Rodriguez" <ivanrwcm25@gmail.com>
Subject: [RFC] usb: typec: ucsi: add software PPM harness for teardown races
Date: Mon,  7 Sep 2026 18:01:58 -0300	[thread overview]
Message-ID: <20260907210158.766980-1-ivanrwcm25@gmail.com> (raw)
In-Reply-To: <20260903232121.271776-1-ivanrwcm25@gmail.com>

Huang Wei asked for the software-PPM harness that produced the v2
numbers, so the ordering comparison is in the archives.

This is an RFC. I am not asking for it to be merged unless it is
useful to keep. The module is gated on DEBUG_KERNEL and defaults
to n.

It drives the real UCSI core through a software PPM that owns the
notify source, the same role as ucsi_acpi.c. The three teardown
orderings and the two deterministic checks:

  scenario=0  quiesce, then unregister, then unlink   ~150 ms, clean
  scenario=1  unlink before unregister (v1)           ~10 s stall
  scenario=2  unregister, then late connector notify  KASAN UAF
  scenario=8  CCI mask + positive control             MASKOK
  scenario=9  notify_lock held by an in-flight notify BARRIEROK

  qemu ... -append 'ucsi_race_test.scenario=N oops=panic'

Link: https://lore.kernel.org/linux-usb/20260903232121.271776-1-ivanrwcm25@gmail.com/
Signed-off-by: Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
---
 drivers/usb/typec/ucsi/Kconfig          |  16 +
 drivers/usb/typec/ucsi/Makefile         |   1 +
 drivers/usb/typec/ucsi/ucsi_race_test.c | 817 ++++++++++++++++++++++++
 3 files changed, 834 insertions(+)
 create mode 100644 drivers/usb/typec/ucsi/ucsi_race_test.c

diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig
index 87dd992a4b9e..87c875bac204 100644
--- a/drivers/usb/typec/ucsi/Kconfig
+++ b/drivers/usb/typec/ucsi/Kconfig
@@ -104,4 +104,20 @@ config UCSI_HUAWEI_GAOKUN
 	  To compile the driver as a module, choose M here: the module will be
 	  called ucsi_huawei_gaokun.
 
+config UCSI_TEARDOWN_RACE_TEST
+	tristate "UCSI teardown race harness"
+	depends on DEBUG_KERNEL
+	help
+	  Software PPM that drives the real UCSI core through the teardown
+	  orderings discussed on the ucsi_acpi use-after-free thread, so they
+	  can be compared under KASAN, lockdep and KCSAN without a PNP0CA0
+	  ACPI device.
+
+	  This is a developer tool, not needed on production systems. Load it
+	  with ucsi_race_test.scenario=N; see the header of ucsi_race_test.c
+	  for the scenario list.
+
+	  To compile this as a module, choose M here: the module will be
+	  called ucsi_race_test.
+
 endif
diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile
index c7e38bf01350..c531aacfc47c 100644
--- a/drivers/usb/typec/ucsi/Makefile
+++ b/drivers/usb/typec/ucsi/Makefile
@@ -28,3 +28,4 @@ obj-$(CONFIG_UCSI_PMIC_GLINK)		+= ucsi_glink.o
 obj-$(CONFIG_CROS_EC_UCSI)		+= cros_ec_ucsi.o
 obj-$(CONFIG_UCSI_LENOVO_YOGA_C630)	+= ucsi_yoga_c630.o
 obj-$(CONFIG_UCSI_HUAWEI_GAOKUN)	+= ucsi_huawei_gaokun.o
+obj-$(CONFIG_UCSI_TEARDOWN_RACE_TEST)	+= ucsi_race_test.o
diff --git a/drivers/usb/typec/ucsi/ucsi_race_test.c b/drivers/usb/typec/ucsi/ucsi_race_test.c
new file mode 100644
index 000000000000..26172fc61e7d
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi_race_test.c
@@ -0,0 +1,817 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Software PPM for UCSI teardown-ordering races
+ *
+ * Copyright (C) 2026 Iván Ezequiel Rodriguez <ivanrwcm25@gmail.com>
+ *
+ * Drives the real UCSI core (ucsi_create / ucsi_register / ucsi_unregister /
+ * ucsi_destroy / ucsi_notify_common) through a software PPM so that the
+ * teardown orderings discussed on the ucsi_acpi UAF thread can be compared
+ * under KASAN, lockdep and KCSAN without a PNP0CA0 ACPI device.
+ *
+ * The backend plays the same role as ucsi_acpi.c: it owns the notify source
+ * that signals ucsi->complete, and it is the thing that must be quiesced
+ * correctly during teardown.
+ *
+ * Module parameter ucsi_race_test.scenario:
+ *   0  quiesce connector changes, keep completions, then unlink (the fix)
+ *   1  unlink the notify source before ucsi_unregister() (v1; stalls)
+ *   2  no quiesce; late connector notify after ucsi_unregister() (UAF)
+ *   3  baseline: nothing in flight, correct order
+ *   4  concurrent notify storm during the fixed teardown
+ *   5  mid-init teardown with the fix
+ *   6  mid-init teardown with the v1 ordering (stalls)
+ *   8  CCI mask: completion kept, connector change dropped + positive control
+ *   9  notify_lock is a real barrier, not just a flag
+ */
+
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/platform_device.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/workqueue.h>
+#include <linux/delay.h>
+#include <linux/ktime.h>
+#include <linux/spinlock.h>
+#include <linux/atomic.h>
+#include <linux/kthread.h>
+
+#include "ucsi.h"
+
+#define RACE_NUM_CONNECTORS	2
+#define RACE_TAG		"UCSI-RACE"
+
+/* Connector Change Indication field of the CCI. */
+#define UCSI_RACE_CONNECTOR_FIELD	GENMASK(7, 1)
+
+static int scenario;
+module_param(scenario, int, 0444);
+MODULE_PARM_DESC(scenario,
+		 "0=fixed 1=v1-unlink-first 2=late-notify-uaf 3=baseline 4=storm 5=mid-init 6=mid-init-v1 8=cci-mask 9=lock-barrier");
+
+static int cycles = 5;
+module_param(cycles, int, 0444);
+MODULE_PARM_DESC(cycles, "teardown cycles for the storm scenario");
+
+static int storm_delay_ms = 300;
+module_param(storm_delay_ms, int, 0444);
+MODULE_PARM_DESC(storm_delay_ms, "in-flight command delay per storm cycle");
+
+struct race_ppm {
+	struct device *dev;
+	struct ucsi *ucsi;
+
+	/* Mirrors ucsi_acpi: serialises notify against start of teardown. */
+	struct mutex notify_lock;
+	bool quiescing;
+	bool unlinked;
+
+	/*
+	 * Scenario 9 only: hold notify_lock for this long inside the handler,
+	 * so that the start of teardown provably has to wait for a notify that
+	 * entered the barrier first.
+	 */
+	unsigned int block_ms;
+	bool in_notify;
+
+	/* Protects cci, last_cmd and oneshot_delay_ms. */
+	spinlock_t cci_lock;
+	u32 cci;
+	u64 last_cmd;
+
+	struct ucsi_capability cap;
+	atomic_t cmds;
+
+	struct workqueue_struct *notify_wq;
+	struct delayed_work notify_work;
+	/*
+	 * Delay applied to the next command only, so that exactly one command
+	 * is left in flight when teardown starts. Every other command is
+	 * answered synchronously, which keeps the measured baseline low and
+	 * makes a missing completion show up as the full UCSI_TIMEOUT_MS.
+	 */
+	unsigned int oneshot_delay_ms;
+};
+
+/*
+ * CCI is a single register in which the connector-change field and the
+ * command status bits coexist. Raising a connector change must therefore
+ * not clobber a command completion that the OS has not acknowledged yet,
+ * otherwise the emulator invents timeouts that no real PPM would produce.
+ */
+static void race_raise_connector_change(struct race_ppm *rp, u8 num)
+{
+	unsigned long flags;
+
+	spin_lock_irqsave(&rp->cci_lock, flags);
+	rp->cci = (rp->cci & ~UCSI_RACE_CONNECTOR_FIELD) |
+		  (((u32)num << 1) & UCSI_RACE_CONNECTOR_FIELD);
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+}
+
+static u32 race_get_cci(struct race_ppm *rp)
+{
+	unsigned long flags;
+	u32 cci;
+
+	spin_lock_irqsave(&rp->cci_lock, flags);
+	cci = rp->cci;
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+	return cci;
+}
+
+/*
+ * The backend notify path. This is the code under test: it is the exact
+ * shape of ucsi_acpi_notify() after the fix.
+ */
+static void race_deliver_notify(struct race_ppm *rp)
+{
+	u32 cci;
+
+	mutex_lock(&rp->notify_lock);
+
+	/* Emulates the ACPI handler having been unlinked already. */
+	if (rp->unlinked)
+		goto out_unlock;
+
+	if (rp->block_ms) {
+		unsigned int block = rp->block_ms;
+
+		rp->block_ms = 0;
+		WRITE_ONCE(rp->in_notify, true);
+		msleep(block);
+	}
+
+	cci = race_get_cci(rp);
+
+	if (rp->quiescing)
+		cci &= UCSI_CCI_BUSY | UCSI_CCI_ACK_COMPLETE |
+		       UCSI_CCI_COMMAND_COMPLETE;
+
+	ucsi_notify_common(rp->ucsi, cci);
+
+out_unlock:
+	mutex_unlock(&rp->notify_lock);
+}
+
+static void race_notify_work(struct work_struct *work)
+{
+	struct race_ppm *rp = container_of(to_delayed_work(work),
+					   struct race_ppm, notify_work);
+
+	race_deliver_notify(rp);
+}
+
+/* ------------------------- emulated PPM operations ----------------------- */
+
+static int race_read_version(struct ucsi *ucsi, u16 *version)
+{
+	*version = UCSI_VERSION_1_2;
+	return 0;
+}
+
+static int race_read_cci(struct ucsi *ucsi, u32 *cci)
+{
+	*cci = race_get_cci(ucsi_get_drvdata(ucsi));
+	return 0;
+}
+
+static int race_poll_cci(struct ucsi *ucsi, u32 *cci)
+{
+	return race_read_cci(ucsi, cci);
+}
+
+static int race_read_message_in(struct ucsi *ucsi, void *val, size_t val_len)
+{
+	struct race_ppm *rp = ucsi_get_drvdata(ucsi);
+	unsigned long flags;
+	u64 cmd;
+
+	spin_lock_irqsave(&rp->cci_lock, flags);
+	cmd = rp->last_cmd;
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+	memset(val, 0, val_len);
+
+	if (UCSI_COMMAND(cmd) == UCSI_GET_CAPABILITY)
+		memcpy(val, &rp->cap, min(val_len, sizeof(rp->cap)));
+
+	return 0;
+}
+
+static int race_write_message_out(struct ucsi *ucsi, void *data, size_t len)
+{
+	return 0;
+}
+
+/*
+ * Accept any command and answer it with the matching completion. The
+ * completion is delivered from notify_wq, which models the ACPI notify
+ * workqueue: it is a context the teardown path has to cooperate with.
+ */
+static int race_async_control(struct ucsi *ucsi, u64 command)
+{
+	struct race_ppm *rp = ucsi_get_drvdata(ucsi);
+	unsigned long flags;
+	unsigned int delay;
+	u32 cci;
+
+	atomic_inc(&rp->cmds);
+
+	spin_lock_irqsave(&rp->cci_lock, flags);
+
+	/* A pending connector change survives until the OS acknowledges it. */
+	cci = rp->cci & UCSI_RACE_CONNECTOR_FIELD;
+
+	switch (UCSI_COMMAND(command)) {
+	case UCSI_PPM_RESET:
+		cci = UCSI_CCI_RESET_COMPLETE;
+		break;
+	case UCSI_ACK_CC_CI:
+		if (command & UCSI_ACK_CONNECTOR_CHANGE)
+			cci &= ~UCSI_RACE_CONNECTOR_FIELD;
+		cci |= UCSI_CCI_ACK_COMPLETE;
+		break;
+	default:
+		cci |= UCSI_CCI_COMMAND_COMPLETE | UCSI_SET_CCI_LENGTH(0x10);
+		break;
+	}
+
+	rp->last_cmd = command;
+	rp->cci = cci;
+
+	/* PPM_RESET is polled, not notified, so it must not eat the delay. */
+	if (UCSI_COMMAND(command) == UCSI_PPM_RESET) {
+		delay = 0;
+	} else {
+		delay = rp->oneshot_delay_ms;
+		rp->oneshot_delay_ms = 0;
+	}
+
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+	/* PPM_RESET is observed by polling, no notify needed. */
+	if (UCSI_COMMAND(command) == UCSI_PPM_RESET)
+		return 0;
+
+	if (delay)
+		queue_delayed_work(rp->notify_wq, &rp->notify_work,
+				   msecs_to_jiffies(delay));
+	else
+		race_deliver_notify(rp);
+
+	return 0;
+}
+
+static const struct ucsi_operations race_ops = {
+	.read_version = race_read_version,
+	.read_cci = race_read_cci,
+	.poll_cci = race_poll_cci,
+	.read_message_in = race_read_message_in,
+	.write_message_out = race_write_message_out,
+	.sync_control = ucsi_sync_control_common,
+	.async_control = race_async_control,
+};
+
+/* ------------------------------ the scenarios ---------------------------- */
+
+/* Fire a connector-change notification for connector @num. */
+static void race_fire_connector_change(struct race_ppm *rp, u8 num)
+{
+	race_raise_connector_change(rp, num);
+	race_deliver_notify(rp);
+}
+
+static void race_unlink_notify(struct race_ppm *rp)
+{
+	/*
+	 * Models acpi_remove_notify_handler(): stop dispatching and then wait
+	 * for anything already dispatched. The flag must not be set while
+	 * holding nothing else, and the flush must happen outside the lock,
+	 * exactly like the real path.
+	 */
+	mutex_lock(&rp->notify_lock);
+	rp->unlinked = true;
+	mutex_unlock(&rp->notify_lock);
+
+	cancel_delayed_work_sync(&rp->notify_work);
+	flush_workqueue(rp->notify_wq);
+}
+
+static void race_teardown(struct race_ppm *rp)
+{
+	unsigned long flags;
+	ktime_t t0;
+	s64 ms;
+
+	/*
+	 * Put a connector work in flight: it issues GET_CONNECTOR_STATUS and
+	 * blocks in wait_for_completion_timeout() until notify_wq answers.
+	 * Scenario 3 skips this to measure the teardown floor.
+	 */
+	if (scenario != 3) {
+		spin_lock_irqsave(&rp->cci_lock, flags);
+		rp->oneshot_delay_ms = 1500;
+		spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+		race_fire_connector_change(rp, 1);
+
+		/* Let the work reach the wait before teardown starts. */
+		msleep(80);
+
+		/*
+		 * The whole test is meaningless unless a connector work is
+		 * really parked in wait_for_completion_timeout() right now.
+		 */
+		pr_info(RACE_TAG ": ntfy=0x%llx flags=0x%lx conn_change_en=%d\n",
+			rp->ucsi->ntfy, rp->ucsi->flags,
+			!!(rp->ucsi->ntfy & UCSI_ENABLE_NTFY_CONNECTOR_CHANGE));
+		pr_info(RACE_TAG ": cmds=%d notify_pending=%d cmd_pending=%d event_pending=%d\n",
+			atomic_read(&rp->cmds),
+			!!delayed_work_pending(&rp->notify_work),
+			test_bit(COMMAND_PENDING, &rp->ucsi->flags),
+			test_bit(EVENT_PENDING, &rp->ucsi->flags));
+
+		if (delayed_work_pending(&rp->notify_work) &&
+		    test_bit(COMMAND_PENDING, &rp->ucsi->flags))
+			pr_info(RACE_TAG ": PRECOND=OK work parked on completion\n");
+		else
+			pr_err(RACE_TAG ": PRECOND=FAIL no work parked on completion\n");
+	}
+
+	pr_info(RACE_TAG ": scenario %d teardown start\n", scenario);
+	t0 = ktime_get();
+
+	switch (scenario) {
+	case 3:
+		/* Baseline: nothing in flight, correct order. */
+		mutex_lock(&rp->notify_lock);
+		rp->quiescing = true;
+		mutex_unlock(&rp->notify_lock);
+
+		ucsi_unregister(rp->ucsi);
+		race_unlink_notify(rp);
+		break;
+	case 1:
+		/*
+		 * The v1 ordering: kill the notify source first. The connector
+		 * work is now waiting for a completion that can never arrive,
+		 * so ucsi_unregister() must sit through UCSI_TIMEOUT_MS.
+		 */
+		race_unlink_notify(rp);
+		ucsi_unregister(rp->ucsi);
+		break;
+	case 2:
+		/*
+		 * Handler kept alive across unregister but connector changes
+		 * are never suppressed. The late notify below reaches
+		 * ucsi_connector_change() after the connector array was freed.
+		 */
+		ucsi_unregister(rp->ucsi);
+		pr_info(RACE_TAG ": firing late connector notify after unregister\n");
+		race_fire_connector_change(rp, 1);
+		race_unlink_notify(rp);
+		break;
+	default:
+		/*
+		 * The fix: suppress connector changes, keep completions, drain,
+		 * then unlink and flush before the object is freed.
+		 */
+		mutex_lock(&rp->notify_lock);
+		rp->quiescing = true;
+		mutex_unlock(&rp->notify_lock);
+
+		ucsi_unregister(rp->ucsi);
+
+		/* A late notify must now be harmless even with connector bits. */
+		pr_info(RACE_TAG ": firing late connector notify after unregister\n");
+		race_fire_connector_change(rp, 1);
+
+		race_unlink_notify(rp);
+		break;
+	}
+
+	ms = ktime_ms_delta(ktime_get(), t0);
+	pr_info(RACE_TAG ": teardown_ms=%lld\n", ms);
+
+	if (ms >= 5000)
+		pr_err(RACE_TAG ": RESULT=STALL teardown blocked %lld ms\n", ms);
+	else
+		pr_info(RACE_TAG ": RESULT=NOSTALL teardown %lld ms\n", ms);
+
+	ucsi_destroy(rp->ucsi);
+	rp->ucsi = NULL;
+
+	pr_info(RACE_TAG ": destroy done\n");
+}
+
+/*
+ * Scenario 4: hammer the notify path from several CPUs while the fixed
+ * teardown runs, repeatedly. Any missing barrier in the quiesce protocol
+ * shows up as a KASAN report on the connector array or on the ucsi object,
+ * or as a lockdep splat on notify_lock.
+ */
+#define RACE_STORM_THREADS	4
+
+static int race_storm_thread(void *data)
+{
+	struct race_ppm *rp = data;
+
+	while (!kthread_should_stop()) {
+		race_raise_connector_change(rp, 1);
+		race_deliver_notify(rp);
+		usleep_range(20, 120);
+	}
+
+	return 0;
+}
+
+static void race_storm_cycle(struct race_ppm *rp, int cycle)
+{
+	struct task_struct *th[RACE_STORM_THREADS];
+	unsigned long flags;
+	int i, n = 0;
+
+	rp->quiescing = false;
+	rp->unlinked = false;
+
+	rp->ucsi = ucsi_create(rp->dev, &race_ops);
+	if (IS_ERR(rp->ucsi)) {
+		pr_err(RACE_TAG ": cycle %d ucsi_create failed\n", cycle);
+		rp->ucsi = NULL;
+		return;
+	}
+	ucsi_set_drvdata(rp->ucsi, rp);
+
+	if (ucsi_register(rp->ucsi)) {
+		pr_err(RACE_TAG ": cycle %d ucsi_register failed\n", cycle);
+		goto out_destroy;
+	}
+
+	for (i = 0; i < 500; i++) {
+		if (rp->ucsi->connector && rp->ucsi->ntfy)
+			break;
+		msleep(20);
+	}
+
+	if (!rp->ucsi->connector || !rp->ucsi->ntfy) {
+		pr_err(RACE_TAG ": cycle %d init did not land\n", cycle);
+		ucsi_unregister(rp->ucsi);
+		race_unlink_notify(rp);
+		goto out_destroy;
+	}
+
+	for (i = 0; i < RACE_STORM_THREADS; i++) {
+		th[n] = kthread_run(race_storm_thread, rp, "ucsi_storm%d", i);
+		if (!IS_ERR(th[n]))
+			n++;
+	}
+
+	/* Park one command mid-flight, as in the single-shot scenarios. */
+	spin_lock_irqsave(&rp->cci_lock, flags);
+	rp->oneshot_delay_ms = storm_delay_ms;
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+	msleep(min(storm_delay_ms / 4 + 1, 60));
+
+	/* The fix, under concurrent notifies. */
+	mutex_lock(&rp->notify_lock);
+	rp->quiescing = true;
+	mutex_unlock(&rp->notify_lock);
+
+	ucsi_unregister(rp->ucsi);
+
+	race_unlink_notify(rp);
+
+	for (i = 0; i < n; i++)
+		kthread_stop(th[i]);
+
+out_destroy:
+	ucsi_destroy(rp->ucsi);
+	rp->ucsi = NULL;
+	if (cycles <= 20 || !((cycle + 1) % 100))
+		pr_info(RACE_TAG ": cycle %d done\n", cycle);
+}
+
+/*
+ * Scenario 8: deterministic proof of what the CCI mask does and does not do.
+ *
+ * A single CCI carrying both a connector change and a command completion is
+ * delivered while quiescing. The completion must still be signalled and the
+ * connector path must not be entered. The same CCI is then delivered with
+ * quiescing off as a positive control, so that a test that silently stopped
+ * exercising ucsi_connector_change() cannot pass.
+ */
+static void race_prove_mask(struct race_ppm *rp)
+{
+	struct ucsi *ucsi = rp->ucsi;
+	bool ev_quiesced, cmd_cleared, ev_control;
+	/* UCSI_CCI_CONNECTOR() extracts the field, so build it by hand. */
+	u32 cci = ((u32)1 << 1) | UCSI_CCI_COMMAND_COMPLETE;
+	unsigned long flags;
+
+	/* Deliver the crafted CCI rather than whatever init left behind. */
+	spin_lock_irqsave(&rp->cci_lock, flags);
+	rp->cci = cci;
+	spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+	pr_info(RACE_TAG ": mask test cci=0x%08x connector=%u\n",
+		cci, (unsigned int)UCSI_CCI_CONNECTOR(cci));
+
+	/* Quiescing: completion expected, connector dispatch not. */
+	clear_bit(EVENT_PENDING, &ucsi->flags);
+	set_bit(COMMAND_PENDING, &ucsi->flags);
+	reinit_completion(&ucsi->complete);
+
+	mutex_lock(&rp->notify_lock);
+	rp->quiescing = true;
+	mutex_unlock(&rp->notify_lock);
+
+	race_deliver_notify(rp);
+
+	ev_quiesced = test_bit(EVENT_PENDING, &ucsi->flags);
+	cmd_cleared = !test_bit(COMMAND_PENDING, &ucsi->flags);
+
+	pr_info(RACE_TAG ": quiesced: event_pending=%d command_completed=%d\n",
+		ev_quiesced, cmd_cleared);
+
+	/* Positive control: same CCI, quiescing off, connector must dispatch. */
+	clear_bit(EVENT_PENDING, &ucsi->flags);
+	set_bit(COMMAND_PENDING, &ucsi->flags);
+	reinit_completion(&ucsi->complete);
+
+	mutex_lock(&rp->notify_lock);
+	rp->quiescing = false;
+	mutex_unlock(&rp->notify_lock);
+
+	race_deliver_notify(rp);
+
+	ev_control = test_bit(EVENT_PENDING, &ucsi->flags);
+
+	pr_info(RACE_TAG ": control: event_pending=%d\n", ev_control);
+
+	if (!ev_quiesced && cmd_cleared && ev_control)
+		pr_info(RACE_TAG ": RESULT=MASKOK completion kept, connector dropped\n");
+	else
+		pr_err(RACE_TAG ": RESULT=MASKFAIL quiesced_ev=%d cmd=%d control_ev=%d\n",
+		       ev_quiesced, cmd_cleared, ev_control);
+
+	/* Let the control's connector work settle before tearing down. */
+	msleep(200);
+
+	mutex_lock(&rp->notify_lock);
+	rp->quiescing = true;
+	mutex_unlock(&rp->notify_lock);
+
+	ucsi_unregister(rp->ucsi);
+	race_unlink_notify(rp);
+	ucsi_destroy(rp->ucsi);
+	rp->ucsi = NULL;
+}
+
+static int race_block_thread(void *data)
+{
+	race_deliver_notify(data);
+	return 0;
+}
+
+/*
+ * Scenario 9: prove that notify_lock is a real barrier and not just a flag.
+ *
+ * A notify enters the handler before teardown and stays inside it. Setting
+ * quiescing must not be able to complete until that notify has returned,
+ * which is what makes "its schedule_work() happens before ucsi_unregister()
+ * starts cancelling" true rather than merely likely.
+ */
+#define RACE_BLOCK_MS	1200
+
+static void race_prove_barrier(struct race_ppm *rp)
+{
+	struct task_struct *th;
+	ktime_t t0;
+	s64 waited;
+	int i;
+
+	race_raise_connector_change(rp, 1);
+
+	mutex_lock(&rp->notify_lock);
+	rp->block_ms = RACE_BLOCK_MS;
+	rp->in_notify = false;
+	mutex_unlock(&rp->notify_lock);
+
+	th = kthread_run(race_block_thread, rp, "ucsi_block");
+	if (IS_ERR(th)) {
+		pr_err(RACE_TAG ": RESULT=BARRIERFAIL cannot start notify\n");
+		return;
+	}
+
+	/* Wait until the notify is provably inside the critical section. */
+	for (i = 0; i < 500; i++) {
+		if (READ_ONCE(rp->in_notify))
+			break;
+		usleep_range(1000, 2000);
+	}
+
+	if (!READ_ONCE(rp->in_notify)) {
+		pr_err(RACE_TAG ": RESULT=BARRIERFAIL notify never entered\n");
+		return;
+	}
+
+	pr_info(RACE_TAG ": notify inside barrier, starting teardown\n");
+
+	t0 = ktime_get();
+	mutex_lock(&rp->notify_lock);
+	rp->quiescing = true;
+	mutex_unlock(&rp->notify_lock);
+	waited = ktime_ms_delta(ktime_get(), t0);
+
+	pr_info(RACE_TAG ": quiesce waited %lld ms for the in-flight notify\n",
+		waited);
+
+	if (waited >= RACE_BLOCK_MS / 2)
+		pr_info(RACE_TAG ": RESULT=BARRIEROK teardown blocked %lld ms\n",
+			waited);
+	else
+		pr_err(RACE_TAG ": RESULT=BARRIERFAIL teardown raced past, %lld ms\n",
+		       waited);
+
+	ucsi_unregister(rp->ucsi);
+	race_unlink_notify(rp);
+	ucsi_destroy(rp->ucsi);
+	rp->ucsi = NULL;
+}
+
+static struct platform_device *race_pdev;
+
+static int __init race_init(void)
+{
+	struct race_ppm *rp;
+	int ret;
+
+	race_pdev = platform_device_register_simple("ucsi_race", -1, NULL, 0);
+	if (IS_ERR(race_pdev))
+		return PTR_ERR(race_pdev);
+
+	rp = kzalloc_obj(*rp);
+	if (!rp) {
+		ret = -ENOMEM;
+		goto err_pdev;
+	}
+
+	rp->dev = &race_pdev->dev;
+	mutex_init(&rp->notify_lock);
+	spin_lock_init(&rp->cci_lock);
+	INIT_DELAYED_WORK(&rp->notify_work, race_notify_work);
+	rp->cap.num_connectors = RACE_NUM_CONNECTORS;
+
+	/* kacpi_notify_wq is WQ_PERCPU with default concurrency. */
+	rp->notify_wq = alloc_workqueue("ucsi_race_notify", WQ_PERCPU, 0);
+	if (!rp->notify_wq) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	if (scenario == 4) {
+		int c;
+
+		for (c = 0; c < cycles; c++)
+			race_storm_cycle(rp, c);
+
+		pr_info(RACE_TAG ": RESULT=STORMDONE %d cycles %d threads\n",
+			cycles, RACE_STORM_THREADS);
+		goto out_clean;
+	}
+
+	rp->ucsi = ucsi_create(rp->dev, &race_ops);
+	if (IS_ERR(rp->ucsi)) {
+		ret = PTR_ERR(rp->ucsi);
+		goto err_wq;
+	}
+	ucsi_set_drvdata(rp->ucsi, rp);
+
+	ret = ucsi_register(rp->ucsi);
+	if (ret) {
+		pr_err(RACE_TAG ": RESULT=REGFAIL ucsi_register returned %d\n", ret);
+		ucsi_destroy(rp->ucsi);
+		goto err_wq;
+	}
+
+	/*
+	 * Scenario 5: tear down while ucsi_init_work() is still running its
+	 * command sequence. ucsi_unregister() starts with
+	 * cancel_delayed_work_sync(&ucsi->work), so the init work must be able
+	 * to finish its in-flight command, which again needs the notify path
+	 * to stay alive across ucsi_unregister().
+	 */
+	if (scenario == 5 || scenario == 6) {
+		unsigned long flags;
+		ktime_t t0;
+		s64 ms;
+
+		/* Park one of the init commands in wait_for_completion(). */
+		spin_lock_irqsave(&rp->cci_lock, flags);
+		rp->oneshot_delay_ms = 1500;
+		spin_unlock_irqrestore(&rp->cci_lock, flags);
+
+		msleep(40);
+
+		pr_info(RACE_TAG ": mid-init connector=%p ntfy=0x%llx cmds=%d notify_pending=%d cmd_pending=%d\n",
+			rp->ucsi->connector, rp->ucsi->ntfy,
+			atomic_read(&rp->cmds),
+			!!delayed_work_pending(&rp->notify_work),
+			test_bit(COMMAND_PENDING, &rp->ucsi->flags));
+
+		if (delayed_work_pending(&rp->notify_work) &&
+		    test_bit(COMMAND_PENDING, &rp->ucsi->flags))
+			pr_info(RACE_TAG ": PRECOND=OK init command parked\n");
+		else
+			pr_err(RACE_TAG ": PRECOND=FAIL no init command parked\n");
+
+		t0 = ktime_get();
+
+		if (scenario == 6) {
+			/* v1 ordering: the parked init command loses its notify. */
+			race_unlink_notify(rp);
+			ucsi_unregister(rp->ucsi);
+		} else {
+			mutex_lock(&rp->notify_lock);
+			rp->quiescing = true;
+			mutex_unlock(&rp->notify_lock);
+
+			ucsi_unregister(rp->ucsi);
+			race_unlink_notify(rp);
+		}
+
+		ms = ktime_ms_delta(ktime_get(), t0);
+		pr_info(RACE_TAG ": teardown_ms=%lld\n", ms);
+		if (ms >= 5000)
+			pr_err(RACE_TAG ": RESULT=STALL mid-init %lld ms\n", ms);
+		else
+			pr_info(RACE_TAG ": RESULT=NOSTALL mid-init %lld ms\n", ms);
+
+		ucsi_destroy(rp->ucsi);
+		rp->ucsi = NULL;
+		goto out_clean;
+	}
+
+	/*
+	 * ucsi_register() only queues ucsi_init_work(); the connectors and the
+	 * notification mask appear later. Wait for init to land, otherwise
+	 * ucsi_connector_change() would bail out on the ntfy gate and no work
+	 * would ever be queued.
+	 */
+	for (ret = 0; ret < 500; ret++) {
+		if (rp->ucsi->connector && rp->ucsi->ntfy)
+			break;
+		msleep(20);
+	}
+
+	if (!rp->ucsi->connector || !rp->ucsi->ntfy) {
+		pr_err(RACE_TAG ": RESULT=INITFAIL ntfy=0x%llx connector=%p\n",
+		       rp->ucsi->ntfy, rp->ucsi->connector);
+		ucsi_unregister(rp->ucsi);
+		race_unlink_notify(rp);
+		ucsi_destroy(rp->ucsi);
+		ret = -ENODEV;
+		goto err_wq;
+	}
+
+	pr_info(RACE_TAG ": init complete, %u connectors, ntfy=0x%llx\n",
+		rp->ucsi->cap.num_connectors, rp->ucsi->ntfy);
+
+	if (scenario == 8)
+		race_prove_mask(rp);
+	else if (scenario == 9)
+		race_prove_barrier(rp);
+	else
+		race_teardown(rp);
+
+out_clean:
+	destroy_workqueue(rp->notify_wq);
+	kfree(rp);
+	platform_device_unregister(race_pdev);
+
+	pr_info(RACE_TAG ": DONE\n");
+	return 0;
+
+err_wq:
+	destroy_workqueue(rp->notify_wq);
+err_free:
+	kfree(rp);
+err_pdev:
+	platform_device_unregister(race_pdev);
+	return ret;
+}
+
+static void __exit race_exit(void)
+{
+}
+
+module_init(race_init);
+module_exit(race_exit);
+
+MODULE_DESCRIPTION("UCSI teardown race harness");
+MODULE_LICENSE("GPL v2");
-- 
2.43.0


  parent reply	other threads:[~2026-09-07 21:02 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
2026-09-07 21:01   ` Iván Ezequiel Rodriguez [this message]
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=20260907210158.766980-1-ivanrwcm25@gmail.com \
    --to=ivanrwcm25@gmail.com \
    --cc=fanwu01@zju.edu.cn \
    --cc=gregkh@linuxfoundation.org \
    --cc=heikki.krogerus@linux.intel.com \
    --cc=huangwei@kylinos.cn \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-usb@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 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®