Linux USB
 help / color / mirror / Atom feed
* [PATCH v3] usb: typec: tcpci: filter masked alerts and quiesce the IRQ
@ 2026-11-12  8:40 Yaohan Li
  2026-11-12  8:56 ` sashiko-bot
  0 siblings, 1 reply; 2+ messages in thread
From: Yaohan Li @ 2026-11-12  8:40 UTC (permalink / raw)
  To: heikki.krogerus, gregkh
  Cc: badhri, groeck, xu.yang_2, linux-usb, linux-kernel

tcpci_irq() reads TCPC_ALERT and uses the result for the event
callbacks, while only the IRQ return value is masked with
tcpci->alert_mask.  A masked alert is still latched in TCPC_ALERT even
though it does not assert Alert# (TCPCI spec 4.4.3), so the handler can
run TCPM callbacks for events that the port has masked.  This also
defeats the ALERT_MASK = 0 write during teardown when the write fails.

In addition, tcpci_remove() unregisters the TCPM port, which destroys
its kthread worker, while the devm-managed threaded IRQ can still be
running and queue work on that worker.  tcpci_unregister_port() must
not run before in-flight handlers have finished.

There is a second teardown race through tcpm_tcpc_reset(): the IRQ
handler can call tcpm_init(), which calls tcpci_init(), and that
function publishes the normal alert mask.  If it interleaves with the
teardown clearing of the mask, it can restore the mask after
synchronize_irq() returns and let a later shared IRQ reach the TCPM
port after its worker was destroyed.

Filter the alert status with tcpci->alert_mask before processing it,
and again after the final re-read.  Add an alert_mask_lock and an
alerts_quiesced state: tcpci_quiesce_alerts() sets both atomically, and
tcpci_init() only publishes the mask while alerts are not quiesced.
Return IRQ_NONE immediately when the software mask is zero, so a
quiesced port does not perform an I2C read at all.  In tcpci_remove(),
quiesce the alerts, disable the hardware mask, then call
synchronize_irq() before unregistering the port.  Apply the same
ordering on the probe error path when the final ALERT_MASK enable write
fails.  A handler that starts after synchronize_irq(), including one
triggered on a shared line, sees the zero software mask and returns
IRQ_NONE without touching the TCPM port.

The masked-status handling and the unquiesced teardown both date back to
the original TCPCI driver.  The TCPM port worker was later migrated to
kthread_work, which is the path that reports the use-after-free, but the
missing status filter and IRQ synchronization are the underlying
defects.

The probe ordering that requests the IRQ before the final ALERT_MASK
enable write was added later, so the probe error path needs the same
quiesce sequence as removal.

Tested on 7.4-rc1 with a simulated I2C TCPCI device and KASAN:

- Unpatched: a handler parked in tcpm_cc_change()/kthread_queue_work()
  while the device is unbound produces a slab-use-after-free on the
  destroyed TCPM worker.
- Patched: synchronize_irq() waits for the parked handler; the handler
  queues on the still-live worker, then the worker is destroyed; no
  report.
- Normal CC event: the callback still runs and removal completes without
  a report.
- Shared IRQ raised after synchronize_irq(), with a stale CC alert:
  tcpci_irq() filters it with alert_mask = 0 and returns IRQ_NONE
  without calling tcpm_cc_change().
- Hardware ALERT_MASK = 0 write forced to fail during removal, with a
  continuous alert stream: no TCPM callback after the worker is
  destroyed.
- Final ALERT_MASK enable write forced to fail during probe: the error
  path quiesces the IRQ before unregistering the port.
- Reset-race: the IRQ handler is parked in tcpci_init() while remove
  quiesces and waits; after release, the pre-guard code restored the
  mask and a shared IRQ reached tcpm_cc_change() with a destroyed
  worker (KASAN null-pointer dereference).  The guarded code keeps
  alert_mask at zero, returns IRQ_NONE, and completes without a report.
- Masked/unmasked mix: with TX_SUCCESS masked and CC unmasked in the
  same ALERT, only tcpm_cc_change() runs; the masked TX bit is not
  cleared and remains latched.
- Normal TX_SUCCESS and RX_STATUS alerts still reach
  tcpm_pd_transmit_complete() and tcpm_pd_receive(), respectively.

Real TCPCI hardware and PD traffic remain untested.

Fixes: 74e656d6b055 ("staging: typec: Type-C Port Controller Interface driver (tcpci)")
Fixes: 862a9c0f6848 ("usb: typec: tcpci: fix NULL pointer issue on shared irq case")
Signed-off-by: Yaohan Li <shunsheng1218@gmail.com>
---
Changes in v3:
- Filter status with alert_mask before processing it and after the
  final re-read; drop the v2 chip->stopping flag.
- Add alert_mask_lock/alerts_quiesced so tcpci_init() cannot restore
  the software mask after teardown started, and return IRQ_NONE early
  when the software mask is zero.
- Publish the quiesced state before the hardware mask write, then
  synchronize_irq() before unregistering the port; same ordering on the
  probe error path.
- Point Fixes at the original TCPCI driver and the later probe ordering
  change instead of the file move.
- Re-test remove/inject, shared-IRQ, mask-failure, probe-failure, the
  tcpci_init() reset race, masked/unmasked mixed alerts, and normal
  TX/RX events.  The final metadata-only amend does not change the
  tested driver source.

AI tools assisted with source review, patch iteration and test harness
development. The author reviewed the resulting code and test results.

v2: https://lore.kernel.org/r/20261106091725.1334097-1-shunsheng1218@gmail.com/
v1: https://lore.kernel.org/r/179378589227.74015.8259496684787732118@gmail.com/

 drivers/usb/typec/tcpm/tcpci.c | 44 ++++++++++++++++++++++++++++++----
 1 file changed, 39 insertions(+), 5 deletions(-)

diff --git a/drivers/usb/typec/tcpm/tcpci.c b/drivers/usb/typec/tcpm/tcpci.c
index 6717ac914c6a6..1b98940fdaed9 100644
--- a/drivers/usb/typec/tcpm/tcpci.c
+++ b/drivers/usb/typec/tcpm/tcpci.c
@@ -39,6 +39,8 @@ struct tcpci {
 	struct regmap *regmap;
 	unsigned int alert_mask;
 	unsigned int rx_type_mask;
+	spinlock_t alert_mask_lock; /* protects alert_mask and alerts_quiesced */
+	bool alerts_quiesced;
 
 	bool controls_vbus;
 
@@ -648,10 +650,21 @@ static bool tcpci_attempt_vconn_swap_discovery(struct tcpc_dev *tcpc)
 	return false;
 }
 
+static void tcpci_quiesce_alerts(struct tcpci *tcpci)
+{
+	unsigned long flags;
+
+	spin_lock_irqsave(&tcpci->alert_mask_lock, flags);
+	tcpci->alerts_quiesced = true;
+	WRITE_ONCE(tcpci->alert_mask, 0);
+	spin_unlock_irqrestore(&tcpci->alert_mask_lock, flags);
+}
+
 static int tcpci_init(struct tcpc_dev *tcpc)
 {
 	struct tcpci *tcpci = tcpc_to_tcpci(tcpc);
 	unsigned long timeout = jiffies + msecs_to_jiffies(2000); /* XXX */
+	unsigned long flags;
 	unsigned int reg;
 	int ret;
 
@@ -710,7 +723,10 @@ static int tcpci_init(struct tcpc_dev *tcpc)
 			return ret;
 	}
 
-	tcpci->alert_mask = reg;
+	spin_lock_irqsave(&tcpci->alert_mask_lock, flags);
+	if (!tcpci->alerts_quiesced)
+		WRITE_ONCE(tcpci->alert_mask, reg);
+	spin_unlock_irqrestore(&tcpci->alert_mask_lock, flags);
 
 	return 0;
 }
@@ -722,8 +738,12 @@ irqreturn_t tcpci_irq(struct tcpci *tcpci)
 	int irq_ret;
 	unsigned int raw;
 
+	if (!READ_ONCE(tcpci->alert_mask))
+		return IRQ_NONE;
+
 	tcpci_read16(tcpci, TCPC_ALERT, &status);
-	irq_ret = status & tcpci->alert_mask;
+	status &= READ_ONCE(tcpci->alert_mask);
+	irq_ret = status;
 
 process_status:
 	/*
@@ -806,8 +826,9 @@ irqreturn_t tcpci_irq(struct tcpci *tcpci)
 		tcpm_pd_transmit_complete(tcpci->port, TCPC_TX_FAILED);
 
 	tcpci_read16(tcpci, TCPC_ALERT, &status);
+	status &= READ_ONCE(tcpci->alert_mask);
 
-	if (status & tcpci->alert_mask)
+	if (status)
 		goto process_status;
 
 	return IRQ_RETVAL(irq_ret);
@@ -854,6 +875,7 @@ struct tcpci *tcpci_register_port(struct device *dev, struct tcpci_data *data)
 	tcpci->dev = dev;
 	tcpci->data = data;
 	tcpci->regmap = data->regmap;
+	spin_lock_init(&tcpci->alert_mask_lock);
 
 	tcpci->tcpc.init = tcpci_init;
 	tcpci->tcpc.get_vbus = tcpci_get_vbus;
@@ -972,12 +994,15 @@ static int tcpci_probe(struct i2c_client *client)
 	/* Enable chip interrupts at last */
 	err = tcpci_write16(chip->tcpci, TCPC_ALERT_MASK, chip->tcpci->alert_mask);
 	if (err < 0)
-		goto unregister_port;
+		goto quiesce_irq;
 
 	device_set_wakeup_capable(chip->tcpci->dev, true);
 
 	return 0;
 
+quiesce_irq:
+	tcpci_quiesce_alerts(chip->tcpci);
+	synchronize_irq(client->irq);
 unregister_port:
 	tcpci_unregister_port(chip->tcpci);
 	return err;
@@ -988,10 +1013,19 @@ static void tcpci_remove(struct i2c_client *client)
 	struct tcpci_chip *chip = i2c_get_clientdata(client);
 	int err;
 
-	/* Disable chip interrupts before unregistering port */
+	/*
+	 * The port's kthread worker is destroyed by tcpci_unregister_port()
+	 * below, while the threaded IRQ handler can still queue work on it.
+	 * Publish a zero software mask first: tcpci_irq() then ignores any
+	 * alert that is still latched, or asserted again on a shared IRQ,
+	 * even if the hardware mask write fails.  synchronize_irq() waits
+	 * for handlers that already read the old mask.
+	 */
+	tcpci_quiesce_alerts(chip->tcpci);
 	err = tcpci_write16(chip->tcpci, TCPC_ALERT_MASK, 0);
 	if (err < 0)
 		dev_warn(&client->dev, "Failed to disable irqs (%pe)\n", ERR_PTR(err));
+	synchronize_irq(client->irq);
 
 	tcpci_unregister_port(chip->tcpci);
 }
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-11-12  8:56 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12  8:40 [PATCH v3] usb: typec: tcpci: filter masked alerts and quiesce the IRQ Yaohan Li
2026-11-12  8:56 ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox