* soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work)
2026-10-27 14:12 ` Greg KH
@ 2026-11-04 12:54 ` 徐晗翔
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 徐晗翔
` (4 subsequent siblings)
5 siblings, 1 reply; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:54 UTC (permalink / raw)
To: andersson, konradybcio; +Cc: linux-arm-msm, linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5410 bytes --]
pmic_glink_altmode schedules two works from asynchronous glink/pdr
notifications -- the per-port alt_port->work (USBC/HPD state machine:
typec_mux_set/typec_switch_set/typec_retimer_set with 5-second ack
waits, pmic_glink_altmode_request() taking altmode->lock and
wait_for_completion_timeout(&altmode->pan_ack, 5*HZ), and
drm_aux_hpd_bridge_notify()) and the global enable_work (scheduled by
pmic_glink_altmode_pdr_notify() on ADSP service state changes) -- but
the driver has NO .remove and NO cancel_work_sync/flush anywhere in
the file: everything (the typec mux/switch/retimer handles, the
pmic_glink client registration, the struct pmic_glink_altmode) is
released by devm actions in reverse order while a queued or running
work is untouched by any of those steps. A USBC notification or a
pdr state transition in flight at unbind/rmmod/ADSP-restart time
therefore makes the worker run on freed handles, a freed client
binding and a freed altmode structure -- an in-tree use-after-free
with a window stretched to seconds by the 5 s ack wait. Notably the
ADSP restart (pdr down->up) is precisely the scenario that triggers
the auxiliary re-enumeration/teardown, so the enable_work instance's
reachability is as high as the port-work one. There is also a
non-UAF data race worth fixing in the same pass: the notify callback
(rpmsg/rx context) writes alt_port->orientation/svid/mode/hpd_state/
mux_ctrl and the 8-byte tbt_data struct copy, while the worker reads
the same fields with no common lock (altmode->lock is only taken
inside pmic_glink_altmode_request) -- KCSAN-visible, with tbt_data
tearable and worst case a transient wrong mux configuration.
The detailed description:
* Teardown sequence with nothing stopping the works (Linux 7.3)
No .remove (pmic_glink_altmode.c:709-713); on auxiliary unbind /
rmmod qcom_pmic_glink / ADSP restart re-enumeration, devm runs in
reverse: glink client release (pmic_glink.c:69-78; only list_del --
does not wait for driver-private works), aux-bridge unregister
(aux-bridge.c:82 devm action), then per-port
typec_switch_put/typec_retimer_put/typec_mux_put (:672-677/:657-662/
:642-647), then the devm kzalloc'd struct pmic_glink_altmode goes
last (:580).
Meanwhile instance 1, alt_port->work (worker :349-394): touches the
switch (:355), mux (:196/254/292/314/336), retimer (:204/262/300/
322/344) handles after their puts -- UAF; pmic_glink_altmode_request
(:393 -> :144-175) takes altmode->lock (:154) and waits up to 5 s on
pan_ack (:168) -- the window is seconds; drm_aux_hpd_bridge_notify
(:377) on a freed bridge.
Instance 2, enable_work: scheduled at :561 by pdr_notify
(pmic_glink.c:216-232 walks clients and calls client->pdr_notify
:230 -- the last batch before the client's list_del can queue it);
the worker (:546-554) runs pmic_glink_altmode_request
(ALTMODE_PAN_EN), touching the same lock/client/pan_ack set -- no
cancel anywhere either.
* Closed-loop exclusions (why nothing protects this today)
The client list_del only stops new callbacks (system_wq has no
relation to pg->client_lock); the devm order only decides which
freed object is hit first; the mux layer's try_module_get pins
code, not these objects; the device-model reference pins the
provider device struct, not the devm data. The probe-failure
path is safe (returns before client registration, no work ever
scheduled) -- the exposure is exactly unbind/restart with a work
in flight, and USBC notifications are high-frequency (plug/HPD/
IRQ_HPD), the worker can reside up to 5 s.
* Evidence (verification; instrument attached)
Qualcomm pmic_glink firmware/ADSP is not available in the test VM,
so the dynamic verification ran the structural isomorph of this
interleave on the native typec class layer (kmod/typcrace.c: an
anx7411-shaped worker with the same "unlocked unregister vs work
holding objects" shape, driven against the native
typec_register/unregister paths):
BUG: KASAN: slab-use-after-free in typec_altmode_update_active
(14 reports across six access points + sysfs_notify + the full
kobject_uevent chain, then a strlen wild-pointer Oops)
alloc: typec_register_altmode <- (provider probe)
free: kfree <- device_release <- kobject_put <- unregister
(the audited "handle freed while work runs" shape)
use: the parked work's continuation
natural (no instrumentation): an Oops in klist_del <- device_del
<- device_unregister <- the work function, ~minutes into a
create/work/remove loop
The pmic_glink side itself is a line-level source finding: the
above isomorph exercises the identical structure (async work vs
devm-released handles and body, no stop/wait), and the driver
file contains no cancel/flush to contradict it.
* Suggested fix
Register a devm action that cancels both works FIRST (i.e. the
last-registered devm action = first executed), or implement
.remove:
cancel_work_sync(&altmode->enable_work);
for (port...) if (altmode->ports[port].altmode)
cancel_work_sync(&altmode->ports[port].work);
placed before any put. For the data race: fold the notify-side
field writes and the worker's reads under altmode->lock (or
snapshot the fields into the work), as a separate small patch.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread* usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove
2026-10-27 14:12 ` Greg KH
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 12:57 ` 徐晗翔
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 徐晗翔
` (3 subsequent siblings)
5 siblings, 1 reply; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:57 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5160 bytes --]
anx7411_i2c_remove() does its teardown in exactly the wrong order:
it first unregisters the partner and its altmodes
(anx7411_partner_unregister_altmode at anx7411.c:1566 and
anx7411_unregister_partner at :1567 -- both UNLOCKED walks of
amode[]/partner) and only afterwards disables the IRQ (:1570) and
cancels the work (:1571). The IRQ thread (devm-requested at
:1527-1531) can enqueue anx7411_work_func() (which runs holding
ctx->lock, :916) at any moment before that, and the remove side does
not take ctx->lock -- so the two sides race on the check-then-use of
amode[] and partner with no common synchronization. Three concrete
interleavings, all verified on a pristine Linux 7.3 KASAN kernel
against the NATIVE typec class and device core with a statement-level
replica of this driver's worker/remove pair: (1) the work
dereferences an altmode already freed by the remove-side unregister
-- 14 KASAN slab-use-after-free reports across the six access points
of typec_altmode_update_active plus sysfs_notify and the whole
kobject_uevent chain, ending in a strlen wild-pointer Oops that kills
the worker; (2) a double typec_unregister_altmode (the work's
POW_OFF branch unregistering at :943/:946 against the remove-side
unregister) -- 24 KASAN reports of the device core executing on a
freed device (device_del internals, dpm_sysfs_remove, freed locks)
and two Oopses; (3) the work re-registers a partner on the already
unregistered port (orphan). The natural, uninstrumented variant of
interleaving 2 hit on its own after ~138 s of create/work/remove
hammering: an Oops in klist_del <- device_del <- device_unregister
<- the work function. The fix is to reorder the remove (stop
IRQ/work first) and take ctx->lock around the unregister walks.
The detailed description:
* The wrong order (Linux 7.3, drivers/usb/typec/anx7411.c)
anx7411_i2c_remove():
:1566 anx7411_partner_unregister_altmode() /* :535-549, NO
lock: walks amode[], typec_unregister_altmode + NULL */
:1567 anx7411_unregister_partner() /* :585-591, NO
lock */
:1570 disable_irq(...)
:1571 cancel_work_sync(&ctx->work) /* too late */
The IRQ thread (:986-993; devm IRQ :1527-1531) queues
anx7411_work_func (:906-984) at any time before :1570; the worker
holds ctx->lock, the remove side does not -- the "check then use"
of amode[i] (:603 -> :606-610), partner (:625) and the
unregister/NULL pairs (:545/:589, :943/:946) have no atomicity
across the two contexts.
* The three interleavings (each dynamically verified; module attached)
1. Work uses an altmode freed by remove: the remove side's
unregister drops the last reference (class.c:606-616 kfree);
the worker's stale amode[i] feeds typec_altmode_update_active/
notify (:606-610) -> UAF, and the resulting update cascade
(sysfs_notify -> kobject_uevent_env -> dev_uevent ->
kobject_get_path -> strlen) Oopses the worker. 14 KASAN
reports; three stacks closed:
alloc: typec_register_altmode <- probe (1024B altmode)
free: kfree <- device_release <- kobject_put <-
unregister (the audited :544 shape)
use: the parked work's continuation
2. Double unregister: the worker's POW_OFF branch (:943/:946) and
the remove side (:1566/:1567) unregister the same altmode/partner
concurrently: the device core (device_del's internals,
dpm_sysfs_remove, sysfs unmerge, bus_notify, freed mutexes,
kernfs lookups) executes on the freed device -- 24 KASAN reports
and two Oopses.
3. Orphan re-registration: the worker's register_altmode (:618-645)
uses the stale partner pointer (:625 check is TOCTOU) after the
remove side already unregistered it.
Natural reachability: with no instrumentation, a create -> queue
work -> immediate remove loop hit interleaving 2's natural shape
after ~138 s: Oops at klist_del+0x22 <- device_del+0x168 <-
device_unregister <- the work function; the worker died (exited
with irqs disabled) and the module hung. So under plain admin
unbind pressure the hit rate is nontrivial, no debugging tools
needed.
Fidelity: the class layer (typec_register/unregister_altmode/
partner, device core, kobject/klist, sysfs/uevent) is 100% native
kernel code; anx7411's worker state machine and remove sequence are
replicated statement-by-statement in the module (anx_work_func/
anx_unregister_*_replica) per the audited line numbers.
Module usage: insmod typcrace.ko; then select the action from the
evidence section's plans (tc0/tc0b/tcnat for this report's three
forms); a parked-worker kprobe (in the module) turns the natural
windows deterministic.
* Suggested fix
Move the stop/wait block (:1569-1573: disable_irq + cancel_work_
sync) BEFORE the unregister side (:1566/:1567), and take ctx->lock
around the amode[]/partner unregister walks. The mux/switch
unregister at :1577-1579 is already after the cancel and correct --
the impact is confined to partner/altmode.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove
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
0 siblings, 0 replies; 10+ messages in thread
From: Greg KH @ 2026-11-04 13:28 UTC (permalink / raw)
To: 徐晗翔; +Cc: heikki.krogerus, linux-usb, linux-kernel
On Wed, Nov 04, 2026 at 08:57:07PM +0800, 徐晗翔 wrote:
<snip>
Sorry, but none of this is helpful at all. Please send patches, not
walls of LLM-generated text that no one is going to read.
greg k-h
^ permalink raw reply [flat|nested] 10+ messages in thread
* 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-10-27 14:12 ` Greg KH
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 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 12:58 ` 徐晗翔
2026-11-04 13:00 ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 12:58 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 6320 bytes --]
On cros_ec UCSI platforms (ALT_MODE_OVERRIDE firmware), unbinding the
platform driver deadlocks: ucsi_unregister_port() takes con->lock
(ucsi.c:2003) and then reaches typec_mode_selection_delete() ->
cancel_delayed_work_sync(&sel->work) (mode_selection.c:277), which
blocks waiting for the mode-selection work to return -- but that work
must, in its DP/TBT activate callbacks, acquire the very same
con->lock via ucsi_con_mutex_lock() before it can return; the unbind
path never refreshes the connector status, the polling condition
stays true, and the loop never exits. The polling is unbounded
(msleep(20) retry loop, ucsi.c:2272-2277) and its condition is a
stale in-memory snapshot (UCSI_CONSTAT reads con->status as latched
at the last connector update, ucsi.h:457-458) -- since the unbind
neither releases the lock nor updates the snapshot, the work spins
forever and the unbind thread waits forever. I reproduced the hang
deterministically on a pristine Linux 7.3 KASAN kernel with a
structural replica of the four ingredients (mutex +
cancel_delayed_work_sync + trylock/msleep polling + stale snapshot),
and closed the loop with two control runs: cancelling a merely-pending
work returns immediately, and a work that got its first trylock before
unbind finishes normally -- the deadlock is a genuine race on "the
work is executing its activate chain while unbind takes the lock
first". Consequences: the unbind/rmmod (or init-failure rollback)
hangs permanently, a system_wq worker is lost, and every later
contender for con->lock (connector-change work, poll worker, role
swap) piles up behind it -- USB-C/DP dead until reboot. Not a memory
safety issue; a liveness bug.
The detailed description:
* The deadlock path (Linux 7.3 line numbers)
cros_ucsi_remove (cros_ec_ucsi.c:313-319, :317) -> ucsi_unregister
(ucsi.c:2372-2397; :2390 only cancels the connector-change work) ->
ucsi_unregister_port (:1998-2032): :2003 mutex_lock(&con->lock) ->
:2004 ucsi_unregister_partner -> :1317-1318 -> (cros_ec_ucsi.c:157)
typec_mode_selection_delete() -> mode_selection.c:277
cancel_delayed_work_sync(&sel->work) -- which is cancel_work_sync's
delayed variant (kernel/workqueue.c:4637-4640 -> :4552 -> :4569
__flush_work(work, true), documented "guaranteed to be not pending
or executing ... even if the work re-queues itself"): the unbind
thread now WAITS for the running work.
The work (mode_selection.c:130-174) -> mode_selection_activate()
(:148/:156/:160) -> :95 releases sel->lock -> device_for_each_child
-> the partner altmode's .activate (altmodes/displayport.c:504-523;
altmodes/thunderbolt.c:247-262) -> typec_altmode_enter/exit
(bus.c:126-148/:157-177) -> the port-side ops registered by UCSI
(ucsi.c:449-463) -> ucsi_displayport_enter/exit / ucsi_thunderbolt_
enter/exit, whose FIRST statement is ucsi_con_mutex_lock()
(ucsi/displayport.c:57/:119; ucsi/thunderbolt.c:89/:122) -- the
same con->lock the unbind thread holds. The poll:
while (connected && !mutex_locked) {
mutex_trylock(...);
connected = UCSI_CONSTAT(con, CONNECTED); /* snapshot,
ucsi.h:457 */
if (connected && !mutex_locked) msleep(20);
}
never exits: the snapshot stays CONNECTED=1 (latched at connection
time), the unbind path has no ucsi_get_connector_status() call, and
msleep is not interruptible. (The DP altmode activate's cable
branch is not an escape: UCSI registers no cable_ops, so it falls
through to the same typec_altmode_enter.)
* Control runs (the race boundary, dynamically covered)
- Work merely pending (timer not fired): cancel removes it and
returns instantly -- no deadlock. So the trigger needs the work
EXECUTING its activate chain when unbind takes the lock; the work
runs on 1 s/4 s periods (cros_ec_ucsi.c:39-41) and its
activate-chain start (typec_mux_set to an i2c mux provider) takes
milliseconds -- the exposure window is real, hit probability
moderate per unbind.
- Work's first trylock succeeds before unbind takes the lock: the
work completes normally.
- The non-deadlock paths exist too: a connector-change disconnect
refreshes the status to disconnected before unregistering
(ucsi.c:1464 -> :1513), and the resume-time
ucsi_check_connection (:1387 -> :1402) likewise lets the poll
see CONNECTED=0 and return -ENOTCONN -- the unbind path is the
one that does not.
* Evidence (pristine 7.3 KASAN; module attached)
Structural replica with a handshake orchestration (hold the lock ->
start the work -> first failed trylock observed -> call the
unbind-side cancel):
TC2: work looping on failed trylocks; unbind calls cancel
(expect hang)
... no "cancel returned", no "work finished" ...
(VM killed by timeout, exit=124 -- both threads permanently
waiting)
and its mirror-image control (cancel of a pending-only work)
returned immediately and clean. Fidelity: the four deadlock
ingredients (mutex, cancel_delayed_work_sync, trylock/msleep
polling, stale CONNECTED snapshot) are replicated statement-by-
statement per ucsi.c:2003-2004 / mode_selection.c:277 /
ucsi.c:2272-2277 / ucsi.h:457-458; the workqueue primitive
semantics were verified in-kernel (workqueue.c:4552-4590).
Module usage: insmod typcrace.ko; action=tc2 (hang; expect the VM
to die by timeout) / action=tc2ctl (control; clean). On real
hardware the same shape is reached by `rmmod cros_ec_ucsi` (or
unbind of the platform driver) while a DP/TBT session is active.
* Suggested fix (any of; no patch attached)
1. ucsi_unregister_port(): refresh the connector status (or
explicitly clear the CONNECTED snapshot) BEFORE taking
con->lock, so a concurrently polled work exits its loop;
2. cros side: move typec_mode_selection_delete() out of the
con->lock critical section (detach partner->sel under the lock
against new updaters, then unlock and only then cancel+free);
3. harden ucsi_con_mutex_lock() with a bounded wait (e.g. 1 s then
-ENOTCONN) so this class of inverted lock-hold cannot wedge the
worker permanently.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread* usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port
2026-10-27 14:12 ` Greg KH
` (2 preceding siblings ...)
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 ` 徐晗翔
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) 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:00 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 5091 bytes --]
Three typec controller drivers share one teardown defect: their IRQ
thread mutates the registered partner (typec_register_partner /
typec_unregister_partner, and data-role setting) with no
synchronization against .remove(), and the IRQ itself is devm-req-
uested -- so it keeps running until AFTER .remove returns and the
port/partner objects may already be gone. stusb160x_remove() even
performs the same unlocked read-then-unregister on chip->partner
(:805-808) that the IRQ thread performs (:388-389): both sides' "see
non-NULL -> unregister" pairs race, producing a double
typec_unregister_partner on a possibly freed partner (use-after-free
writes inside the class layer), and the trailing
typec_unregister_port() frees the port while the IRQ thread may
still hold the stale port pointer. rt1719_remove() does not touch
the partner at all, so its IRQ thread can unregister the partner
after remove and even re-ATTACH one onto the removed port (orphan),
calling typec_set_pwr_opmode() on the freed port (sysfs_notify on a
dead kobject). hd3ss3220 (IRQ mode) races typec_set_data_role()
against the port teardown the same way. I verified the core
interleaving on a pristine Linux 7.3 KASAN kernel with a
statement-level replica of the stusb160x shape over the NATIVE typec
class and device core: an IRQ thread parked right after seeing a
non-NULL partner, then the remove side unregistering partner+port,
then the resumed IRQ unregistering on the freed partner -- four
typec_unregister_partner UAF reports, a freed-mutex wait, a kernfs
NULL-deref and two Oopses, with the timeline showing both sides
reading the SAME partner pointer before the free. The fix is the
ordering the sibling driver wusb3801 already uses: stop/wait the IRQ
(free_irq, which waits for a running ONESHOT thread) before touching
partner/port in remove.
The detailed description:
* The three drivers (Linux 7.3 line numbers)
- stusb160x: IRQ thread stusb160x_irq_handler (:402-429) calls
stusb160x_attach (:416 -> :364 chip->partner = typec_register_
partner(...)) / stusb160x_detach (:418 -> :388-389 unregister +
NULL) directly; stusb160x_remove (:800-815) does the same
unlocked operations at :805-808 and unregisters the port at
:815; devm IRQ at :452.
- rt1719: rt1719_irq_handler (:586-624) calls rt1719_attach
(:611; re-register at :247 after a defensive pre-unregister
:238-240) / rt1719_detach (:614 -> :288); rt1719_remove
(:928-933) only unregisters the port; devm IRQ :645.
- hd3ss3220 (IRQ mode): hd3ss3220_irq_handler (:280-286) ->
hd3ss3220_set_role (:226-241) -> typec_set_data_role (:237/:240);
remove (:522-532) cancels only the polling-mode worker (:527)
and unregisters the port (:529); devm IRQ :438/:489.
Why nothing protects this: devres_release_all runs after .remove
returns (the window covers the whole remove body); IRQF_ONESHOT
only prevents re-entry of the thread, not its concurrency with
remove; typec_unregister_partner()'s port->partner_link_lock
(class.c:1183-1194) orders the class-side list ops but neither
guards the driver's chip->partner pointer nor the port's
lifetime; and after the port is freed the IRQ thread's port
pointer is dangling (typec_port_release kfrees it).
* Evidence (pristine 7.3 KASAN; module attached)
The stusb160x-shape replica (stu_irq/stu_remove in the module)
over the native class layer: the IRQ thread is parked right after
it observed chip->partner != NULL; the remove side then reads the
same non-NULL pointer, unregisters partner and port; on release,
the IRQ thread's typec_unregister_partner() lands on the freed
partner:
irq saw partner ... parked
remove saw partner (same pointer) -> unregistered + port gone
(400 ms later) irq resumes ->
BUG: KASAN: slab-use-after-free in
typec_unregister_partner+0x51/+0xd6 (x4)
+ mutex_lock on freed lock + kernfs_find_and_get_ns NULL-deref
+ Oops x2
Fidelity: typec_register/unregister_partner, the port release and
the whole device core are native; the driver's dual
read-then-unregister pairs are replicated statement-by-statement
per stusb160x.c:388-389/:800-815.
Module usage: insmod typcrace.ko; action=tc1 (the stusb160x
choreography). On real hardware the shape is reached by unbinding
the i2c driver while a cable attach/detach event is in flight --
plain plug/unplug traffic suffices to have the IRQ thread inside
attach/detach at unbind time.
* Suggested fix
Stop the IRQ before mutating partner/port in remove:
stusb160x: free_irq() (or disable+cancel) first, then the
:805-808 pair and the port unregister; rt1719: free_irq() first,
and make remove also detach the partner (or have the IRQ thread
check a removing flag); hd3ss3220: free_irq() in both modes
before the port unregister. The correct-order precedent is
wusb3801.c:402-414, which free_irq()s -- waiting for the in-flight
ONESHOT thread -- before touching partner/port.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: typcrace.c --]
[-- Type: text/plain, Size: 18699 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* typcrace.c - dynamic verification module for drivers/usb/typec audit
* (docs/manual-review-drivers-usb-typec-2026-11-02.round3.md)
*
* Statement-faithful replicas of the audited driver state machines (anx7411,
* stusb160x), driven via the exported typec class API on a fake typec_port,
* with one-shot kprobe stalls making the audited interleavings deterministic.
* Kernel source unmodified (build-kasan-typec-7.3).
*
* tc0 - audit Case 3 interleaving 1: anx7411 work uses amode[] pointer
* (check-then-use, anx7411.c:602-613) while remove frees the device.
* Work is parked by a kprobe at its use point
* (typec_altmode_update_active) AFTER it read amode[i]; the remove
* thread unregisters (frees) the altmode during the park; on resume
* the use dereferences freed memory -> KASAN UAF.
* tc0b - audit Case 3 interleaving 2: double device_unregister. Remove is
* parked by a one-shot kprobe at typec_unregister_altmode BEFORE it
* executes; the queued work then unregisters the same altmode
* (device freed); on resume remove's unregister runs on the freed
* device -> UAF inside typec_unregister_altmode/class core.
* tc1 - audit Case 4 (stusb160x form): irq thread parked by one-shot kprobe
* at typec_unregister_partner (having seen chip->partner != NULL);
* remove thread then unregisters the same partner and unregisters the
* port; irq resumes -> unregister on freed partner -> UAF.
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/delay.h>
#include <linux/ktime.h>
#include <linux/workqueue.h>
#include <linux/kthread.h>
#include <linux/completion.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/usb/typec.h>
#include <linux/usb/typec_altmode.h>
#define MAX_ALTMODE 4
struct tyc_typec {
struct typec_port *port;
struct typec_partner *partner;
struct typec_altmode *amode[MAX_ALTMODE];
};
/* ---------------- anx7411 replica (Case 3) ---------------- */
struct tyc_anx {
struct tyc_typec typec;
struct mutex lock; /* analog of ctx->lock: work side only */
struct work_struct work;
struct workqueue_struct *wq;
struct completion work_in_use; /* work parked at its use point */
};
static void anx_unregister_altmode_replica(struct tyc_anx *ctx)
{
int i;
/* anx7411.c:535-549 -- NO ctx->lock (audited) */
for (i = 0; i < MAX_ALTMODE; i++)
if (ctx->typec.amode[i]) {
typec_unregister_altmode(ctx->typec.amode[i]);
ctx->typec.amode[i] = NULL;
}
}
static void anx_unregister_partner_replica(struct tyc_anx *ctx)
{
/* anx7411.c:585-591 -- NO ctx->lock (audited) */
if (ctx->typec.partner) {
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
static bool anx_use_mode; /* tc0: work parks at use point */
static struct tyc_anx *g_anx0; /* tc0 target for kprobe lookup */
/*
* tc0 kprobe target: typec_altmode_update_active is only reached from the
* work's use point in this module (the remove thread never calls it).
* Park here AFTER the work has read amode[i] (audited check-then-use gap).
*/
static int kp_use_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
struct tyc_anx *ctx = g_anx0;
if (!anx_use_mode || !ctx)
return 0;
anx_use_mode = false;
pr_info("TC0: work parked at use point (adev=%px), remove frees now\n",
(void *)regs->di);
complete(&ctx->work_in_use);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0: work resumes use on (possibly freed) adev\n");
return 0;
}
static struct kprobe kp_use = {
.symbol_name = "typec_altmode_update_active",
.pre_handler = kp_use_pre,
};
/* work replica of anx7411_work_func (anx7411.c:906-984) */
static void anx_work_func(struct work_struct *w)
{
struct tyc_anx *ctx = container_of(w, struct tyc_anx, work);
int i;
mutex_lock(&ctx->lock); /* :916 */
/* "update_altmode" check-then-use loop (:602-613) */
for (i = 0; i < MAX_ALTMODE; i++) {
if (!ctx->typec.amode[i])
continue;
pr_info("TC0: work read amode[%d]=%px svid=%04x, calling update_active\n",
i, ctx->typec.amode[i], ctx->typec.amode[i]->svid);
typec_altmode_update_active(ctx->typec.amode[i], true);
}
/* POW_OFF branch (:943-946) */
pr_info("TC0: work unregister altmode+partner\n");
anx_unregister_altmode_replica(ctx);
anx_unregister_partner_replica(ctx);
mutex_unlock(&ctx->lock);
}
static int anx_remove_thread(void *data)
{
struct tyc_anx *ctx = data;
pr_info("TC0*: remove enters (audited order: unregister FIRST)\n");
anx_unregister_altmode_replica(ctx); /* :1566 */
anx_unregister_partner_replica(ctx); /* :1567 */
pr_info("TC0*: remove stops work (too late, audited)\n");
cancel_work_sync(&ctx->work); /* :1571 */
pr_info("TC0*: remove done\n");
return 0;
}
/* one-shot stall at typec_unregister_altmode (tc0b): park the FIRST caller */
static bool oneshot_armed;
static struct completion tc_done_wait;
static int kp_unreg_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC0b: remove parked at typec_unregister_altmode (adev=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC0b: parked unregister resumes (device likely freed)\n");
return 0;
}
/* ---------------- stusb160x replica (Case 4) ---------------- */
struct tyc_stu {
struct tyc_typec typec;
struct task_struct *irq_thread;
struct completion irq_parked;
};
static int stu_irq_thread(void *data)
{
struct tyc_stu *ctx = data;
while (!kthread_should_stop()) {
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(msecs_to_jiffies(20));
if (kthread_should_stop())
break;
/* stusb160x_detach(:388-389) check-then-use */
if (ctx->typec.partner) {
pr_info("TC1: irq saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
}
return 0;
}
static int stu_remove_thread(void *data)
{
struct tyc_stu *ctx = data;
pr_info("TC1: remove enters (devm-irq window open)\n");
/* stusb160x.c:805-808 */
if (ctx->typec.partner) {
pr_info("TC1: remove saw partner=%px, unregistering\n",
ctx->typec.partner);
typec_unregister_partner(ctx->typec.partner);
ctx->typec.partner = NULL;
}
pr_info("TC1: remove unregisters port\n");
typec_unregister_port(ctx->typec.port);
ctx->typec.port = NULL;
pr_info("TC1: remove done\n");
return 0;
}
/* ---------------- orchestration ---------------- */
static struct tyc_anx *g_anx;
static struct tyc_stu *g_stu;
static struct kprobe kp_unreg;
static int tc_setup_common(struct tyc_typec *t, struct device *parent)
{
struct typec_capability cap = { };
struct typec_partner_desc pdesc = { };
struct typec_altmode_desc adesc = { };
int i;
cap.type = TYPEC_PORT_DRP;
cap.data = TYPEC_PORT_DRD;
cap.revision = USB_TYPEC_REV_1_2;
cap.driver_data = t;
t->port = typec_register_port(parent, &cap);
if (IS_ERR(t->port)) {
pr_err("typec_register_port failed: %pe\n", t->port);
return PTR_ERR(t->port);
}
t->partner = typec_register_partner(t->port, &pdesc);
if (IS_ERR(t->partner)) {
pr_err("typec_register_partner failed: %pe\n", t->partner);
typec_unregister_port(t->port);
t->port = NULL;
return PTR_ERR(t->partner);
}
for (i = 0; i < 2; i++) {
adesc.svid = 0x1234 + i;
adesc.mode = i + 1;
adesc.vdo = 0xdeadbeef;
adesc.roles = TYPEC_PORT_DRD;
t->amode[i] = typec_partner_register_altmode(t->partner, &adesc);
if (IS_ERR(t->amode[i])) {
pr_err("partner_register_altmode[%d] failed: %pe\n",
i, t->amode[i]);
t->amode[i] = NULL;
return -EFAULT;
}
}
return 0;
}
static int run_tc0(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&g_anx->work_in_use);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwq");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
ret = register_kprobe(&kp_use);
if (ret) {
pr_err("TC0: kprobe(use) failed %d\n", ret);
goto out;
}
g_anx0 = g_anx;
anx_use_mode = true;
/* 1. queue work; it parks inside kprobe at its first use, having
* already read amode[0] (audited check-then-use gap). */
queue_work(g_anx->wq, &g_anx->work);
/* 2. wait until parked */
if (!wait_for_completion_timeout(&g_anx->work_in_use, msecs_to_jiffies(2000))) {
pr_err("TC0: work never parked; abort\n");
goto out;
}
/* 3. remove thread frees altmodes+partner under the parked work */
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm");
if (!IS_ERR(rm))
kthread_stop(rm); /* joins: remove completes inside window */
/* 4. work auto-resumes after 400ms -> UAF */
pr_info("TC0: RESULT see KASAN reports above\n");
msleep(300);
anx_unregister_altmode_replica(g_anx);
anx_unregister_partner_replica(g_anx);
out:
unregister_kprobe(&kp_use);
g_anx0 = NULL;
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int run_tc0b(void)
{
struct device *root_dev;
struct task_struct *rm;
int ret;
root_dev = root_device_register("tycroot0b");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
init_completion(&tc_done_wait);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycanxwqb");
ret = tc_setup_common(&g_anx->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_altmode",
.pre_handler = kp_unreg_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC0b: kprobe failed %d\n", ret);
goto out;
}
/* 1. remove thread parks inside kprobe BEFORE its unregister */
oneshot_armed = true;
rm = kthread_run(anx_remove_thread, g_anx, "tyc-anxrm0b");
if (IS_ERR(rm)) {
pr_err("TC0b: kthread failed\n");
goto out;
}
/* 2. wait parked */
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC0b: remove never parked\n");
kthread_stop(rm);
goto out;
}
/* 3. now let the work run: it unregisters the same altmodes+partner
* (device freed) while remove is parked mid-unregister */
queue_work(g_anx->wq, &g_anx->work);
msleep(100);
/* 4. remove resumes -> unregister on freed device */
pr_info("TC0b: remove resumes at ~300ms\n");
msleep(700); /* let everything settle */
cancel_work_sync(&g_anx->work);
pr_info("TC0b: RESULT see KASAN reports above\n");
out:
unregister_kprobe(&kp_unreg);
if (g_anx) {
if (g_anx->wq)
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
}
root_device_unregister(root_dev);
return 0;
}
static int kp_park_pre(struct kprobe *p, struct pt_regs *regs)
{
unsigned long long t0;
if (!oneshot_armed)
return 0;
oneshot_armed = false;
pr_info("TC1: irq thread parked at typec_unregister_partner (partner=%px)\n",
(void *)regs->di);
complete(&tc_done_wait);
t0 = ktime_get_mono_fast_ns();
while (ktime_get_mono_fast_ns() - t0 < 400ULL * NSEC_PER_MSEC)
cpu_relax();
pr_info("TC1: irq resumes unregister (partner likely freed)\n");
return 0;
}
static int run_tc1(void)
{
struct device *root_dev;
int ret;
root_dev = root_device_register("tycroot1");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_stu = kzalloc(sizeof(*g_stu), GFP_KERNEL);
if (!g_stu) {
root_device_unregister(root_dev);
return -ENOMEM;
}
init_completion(&tc_done_wait);
init_completion(&g_stu->irq_parked);
ret = tc_setup_common(&g_stu->typec, root_dev);
if (ret)
goto out;
kp_unreg = (struct kprobe){
.symbol_name = "typec_unregister_partner",
.pre_handler = kp_park_pre,
};
ret = register_kprobe(&kp_unreg);
if (ret) {
pr_err("TC1: kprobe failed %d\n", ret);
goto out;
}
/* 1. irq thread parks at its unregister (partner still alive) */
oneshot_armed = true;
g_stu->irq_thread = kthread_run(stu_irq_thread, g_stu, "tyc-stuirq");
if (IS_ERR(g_stu->irq_thread)) {
g_stu->irq_thread = NULL;
goto out;
}
if (!wait_for_completion_timeout(&tc_done_wait, msecs_to_jiffies(2000))) {
pr_err("TC1: irq never parked\n");
goto out;
}
/* 2. remove sequence inline on the insmod thread (partner still
* alive -> unregister frees it, then port); parked irq thread is
* on another CPU */
stu_remove_thread(g_stu);
/* 3. irq resumes -> unregister on freed partner */
pr_info("TC1: RESULT see KASAN reports above\n");
msleep(300);
out:
unregister_kprobe(&kp_unreg);
if (g_stu) {
if (g_stu->irq_thread)
kthread_stop(g_stu->irq_thread);
kfree(g_stu);
g_stu = NULL;
}
root_device_unregister(root_dev);
return 0;
}
/* ---------------- tc2: Case 1-B deadlock structure replica ----------------
* Faithful structure replica of the audited UCSI unbind deadlock:
* - unbind thread holds "con->lock" and calls cancel_delayed_work_sync()
* (ucsi.c:2003-2004 -> mode_selection.c:277);
* - the mode-selection work, inside its activate window, must take the
* same lock via the ucsi_con_mutex_lock() trylock loop whose exit
* condition is the STALE con->status CONNECTED snapshot
* (ucsi.c:2272-2277; ucsi.h:457 reads ->status, never refreshed on the
* unbind path).
* Expected result: both sides park forever; the VM hangs until timeout.
*/
struct tyc_dead {
struct mutex lock; /* con->lock analog */
bool connected_snapshot; /* con->status CONNECTED bit (stale) */
struct delayed_work work;
struct completion in_loop; /* handshake: work inside trylock loop */
bool ctl; /* ctl=true: lock first, queue later */
};
static void dead_work_fn(struct work_struct *w)
{
struct tyc_dead *d = container_of(w, struct tyc_dead, work.work);
bool locked = false;
pr_info("TC2: work entered activate window, needs con->lock\n");
/* first trylock fails: unbind already holds the lock */
locked = mutex_trylock(&d->lock) != 0;
if (!locked)
complete(&d->in_loop);
/* ucsi_con_mutex_lock() loop, ucsi.c:2272-2277, verbatim shape */
while (d->connected_snapshot && !locked) {
locked = mutex_trylock(&d->lock) != 0;
if (d->connected_snapshot && !locked) {
msleep(20);
}
}
if (locked)
mutex_unlock(&d->lock);
pr_info("TC2: work finished\n");
}
/* audited interleaving: work enters the loop first, unbind then takes the
* lock and cancels -- expected permanent hang (VM killed by timeout). */
static int run_tc2(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true; /* cable connected snapshot */
d->ctl = false;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
/* audited order: unbind holds con->lock FIRST, work starts and
* fails its trylocks while the lock stays held */
mutex_lock(&d->lock);
pr_info("TC2: unbind holds con->lock; scheduling work\n");
schedule_delayed_work(&d->work, 0);
if (!wait_for_completion_timeout(&d->in_loop, msecs_to_jiffies(2000))) {
pr_err("TC2: work never entered loop\n");
mutex_unlock(&d->lock);
kfree(d);
return -ETIMEDOUT;
}
pr_info("TC2: work looping on failed trylocks; unbind calls cancel (expect hang)\n");
cancel_delayed_work_sync(&d->work); /* audited hang */
pr_info("TC2: cancel returned (should never print)\n");
mutex_unlock(&d->lock);
kfree(d);
return 0;
}
/* control: unbind takes the lock BEFORE the work ever runs; cancel() kills
* the pending timer -- no deadlock (audited escape-branch 1). */
static int run_tc2ctl(void)
{
struct tyc_dead *d;
d = kzalloc(sizeof(*d), GFP_KERNEL);
if (!d)
return -ENOMEM;
mutex_init(&d->lock);
init_completion(&d->in_loop);
d->connected_snapshot = true;
d->ctl = true;
INIT_DELAYED_WORK(&d->work, dead_work_fn);
mutex_lock(&d->lock);
pr_info("TC2CTL: unbind holds lock first, queueing work\n");
schedule_delayed_work(&d->work, msecs_to_jiffies(50));
cancel_delayed_work_sync(&d->work); /* pending -> killed */
pr_info("TC2CTL: cancel returned (expected, pending branch)\n");
mutex_unlock(&d->lock);
msleep(200);
kfree(d);
return 0;
}
/* ---------------- tcnat: natural hammering (no probes) ---------------- */
static int tcnat_rounds = 1000;
module_param(tcnat_rounds, int, 0444);
static int run_tcnat(void)
{
struct device *root_dev;
int round;
for (round = 0; round < tcnat_rounds; round++) {
root_dev = root_device_register("tycrootnat");
if (IS_ERR(root_dev))
return PTR_ERR(root_dev);
g_anx = kzalloc(sizeof(*g_anx), GFP_KERNEL);
if (!g_anx) {
root_device_unregister(root_dev);
return -ENOMEM;
}
mutex_init(&g_anx->lock);
INIT_WORK(&g_anx->work, anx_work_func);
g_anx->wq = create_singlethread_workqueue("tycnatwq");
if (tc_setup_common(&g_anx->typec, root_dev)) {
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
return -EFAULT;
}
/* natural race: queue work, then immediately remove */
queue_work(g_anx->wq, &g_anx->work);
anx_remove_thread(g_anx);
cancel_work_sync(&g_anx->work);
destroy_workqueue(g_anx->wq);
kfree(g_anx);
g_anx = NULL;
root_device_unregister(root_dev);
if (!(round % 100))
pr_info("TCNAT: round %d done\n", round);
}
pr_info("TCNAT: %d rounds done (check dmesg for KASAN reports)\n",
tcnat_rounds);
return 0;
}
/* ---------------- module params & entry ---------------- */
static char *action = "status";
module_param(action, charp, 0444);
MODULE_PARM_DESC(action, "tc0|tc0b|tc1|tc2|tcnat|status");
static int tyc_run_action(void)
{
if (!strcmp(action, "tc0"))
return run_tc0();
if (!strcmp(action, "tc0b"))
return run_tc0b();
if (!strcmp(action, "tc1"))
return run_tc1();
if (!strcmp(action, "tc2"))
return run_tc2();
if (!strcmp(action, "tc2ctl"))
return run_tc2ctl();
if (!strcmp(action, "tcnat"))
return run_tcnat();
pr_info("no action, done\n");
return 0;
}
static int __init tyc_init(void)
{
pr_info("action=%s\n", action);
return tyc_run_action();
}
module_init(tyc_init);
static void __exit tyc_exit(void)
{
pr_info("exit\n");
}
module_exit(tyc_exit);
MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 10+ messages in thread* 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-10-27 14:12 ` Greg KH
` (3 preceding siblings ...)
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 ` 徐晗翔
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) 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:01 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
The UCSI displayport altmode's reply triple (dp->header / vdo_data /
vdo_size, ucsi/displayport.c:28-30) is written by three writers that
all hold con->lock (enter :97-102, exit :142-147, vdm :234+) but read
by ucsi_displayport_work (:282-295) with NO lock at all -- the work
is scheduled asynchronously (:104/:149/:265) and never acquires
con->lock, so the lock only serializes the writers among themselves
and does not cover the reader. When the worker runs while a writer
updates the triple (the mode_selection activate path entering
ucsi_displayport_enter, or the next EC event arriving while the
worker is inside typec_altmode_vdm -> the partner driver), the VDM
message assembled from the mixed fields is sent to the partner
driver: a stale/mixed header with fresh data or vice versa -- a DP
state machine glitch that the partner sees as a malformed or
misdirected VDM (self-healing on the next event, but still a protocol
violation and a plain KCSAN data race). Lifetime is NOT at risk
(vdo_data only ever points at the persistent &dp->data.status or
NULL; the worker is cancel_work_sync'ed before teardown), so this is
a small locking fix: read/clear the triple under the same lock (or a
dedicated dp->lock) in the worker.
The detailed description:
* The racy pair (Linux 7.3, drivers/usb/typec/ucsi/displayport.c)
Writers (all inside ucsi_con_mutex_lock ... unlock, :57/:119/:209):
- enter path: :97-102 sets dp->header = ENTER_ACK (with
vdo_data/vdo_size) and schedules dp->work (:104);
- exit path: :142-147 + :149;
- vdm path: :234+ (with :183-184 for the ATTENTION-derived
variant) + :265.
Reader (no lock): ucsi_displayport_work (:282-295) reads
dp->header / dp->vdo_data / dp->vdo_size (:287-288) and clears
them (:292-294), then calls typec_altmode_vdm(dp->alt, ...) ->
bus.c:214-235 -> the partner driver's dp_altmode_vdm
(altmodes/displayport.c:372-443, under the driver's own dp->lock).
The worker never takes con->lock, and nothing stops a writer from
taking con->lock while the unlocked worker runs (the mode_selection
activate path enters ucsi_displayport_enter without holding
con->lock -- it takes it inside). schedule_work coalescing does
not close the window.
* Concrete interleavings
1. mode_selection activate vs an in-flight worker: the activate
path (thread A) writes ENTER_ACK and schedules; the previous
round's worker (thread B) is concurrently reading the old
header/vdo_data at :287-288 -- the partner receives a mixed
old/new VDM.
2. back-to-back vdm events: the worker is inside
typec_altmode_vdm (which enters the partner driver and takes
its dp->lock -- nonzero time), the next EC event drives
ucsi_displayport_vdm to overwrite the triple under con->lock --
same mixed-VDM outcome.
* Scope / severity
vdo_data only ever points at the persistent &dp->data.status (or
NULL, :101/:146/:183) and vdo_size is a u8 -- no dangling pointer,
no UAF; the worker is drained by ucsi_displayport_remove_partner's
cancel_work_sync (:297-313, :308) before the altmode teardown
(verified: ucsi.c:834-841 ordering). So the impact is: KCSAN
data-race reports plus transient VDM mismatches (partner-side DP
state machine warnings/retries; self-healing). Severity low; the
fix is small.
* Suggested fix
Have ucsi_displayport_work() snapshot the triple under
ucsi_con_mutex_lock(dp->con) (or a new dedicated dp->lock shared by
writers and the worker), then send the snapshot outside the lock.
Alternatively, writers coalesce under the lock with a
cancel_work_sync before overwriting -- more costly; the snapshot
is the clean minimal fix.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
^ permalink raw reply [flat|nested] 10+ messages in thread* 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-10-27 14:12 ` Greg KH
` (4 preceding siblings ...)
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 ` 徐晗翔
5 siblings, 0 replies; 10+ messages in thread
From: 徐晗翔 @ 2026-11-04 13:02 UTC (permalink / raw)
To: heikki.krogerus; +Cc: linux-usb, linux-kernel
The typec class layer leaves the lifetime of its mux/switch machinery
to caller discipline that nothing enforces or documents, and three
concrete gaps fall out of that. The reportable one is the provider
drvdata gap: a mux/switch handle holder keeps a device reference on
the provider's class device (class_find_device() takes get_device,
drivers/base/class.c:470), so after the PROVIDER driver unbinds,
typec_mux_unregister() only does device_unregister -- the mux_dev
shell (and its ->set callback pointer) survives as long as any owner
holds a handle, while the provider's devm private data is freed the
moment its .remove returns. The next orientation/mode change then
calls mux_dev->set() (mux.c:393-412) into e.g.
qmp_combo_typec_mux_set() (phy-qcom-qmp-combo.c:4804) or
rk_udphy_typec_mux_set() (phy-rockchip-usbdp.c:1346), which fetch
devm drvdata of a freed object -- use-after-free read/write with
KASAN visibility; the same shape applies to the switch/retimer-side
providers with explicit removes (ptn36502, nb7vpq904m). Two more
gaps in the same layer are contract-shaped and currently shielded
only by callers: (a) the aggregate typec_mux/typec_switch handles are
freshly allocated per get with no refcount/lock/RCU, typec_mux_set()
walks the callback list unlocked and typec_mux_put() kfrees on the
final put (the one in-tree violator of the resulting "drain before
put" discipline -- pmic_glink_altmode -- is reported separately); and
(b) mode_selection.c's exported state_update/delete pair has no
common lock or lifetime protocol at all (delete: partner->sel = NULL,
cancel_delayed_work_sync, mode_list_clean -- which does NOT take
sel->lock -- mutex_destroy, kfree; update: unlocked read of
partner->sel then mutex_lock + list access; today's only protection
is that every in-tree caller holds ucsi's con->lock, per the
enumeration below). Fixes: a dead-flag + -ENODEV check in the set
path for (the drvdata gap), a documented drain+put contract (or a
class-layer primitive) for the handle protocol, and a lock/lifetime
contract (or in-delete locking) for mode_selection.
The detailed description:
* Gap 1 (reportable UAF): provider drvdata outlived by the handle
Linux 7.3: the owner takes the handle via class_find_device()
(mux.c:295) which get_device()s the mux_dev (class.c:470); the
final typec_mux_put() (mux.c:376-391) drops it; the provider's
unregister (mux.c:478-483) only device_unregister()s. So:
provider unbind: .remove -> typec_mux_unregister (shell kept)
.remove returns -> devres releases the
provider's private data (qmp/udphy/ptn)
later mode/orientation change on the owner:
typec_mux_set(handle) -> mux_dev->set(...)
-> typec_mux_get_drvdata() -> FREED data
Instances with this devm-drvdata pattern: qcom-qmp-combo (set
:4804-4806, devm qmp, register :4924, unregister devm action
:4894-4898/:4930), rockchip-usbdp (set :1346, register :1413-1415,
unregister :1402-1406); ptn36502/nb7vpq904m have explicit removes
but devm priv/regmap, and their own put does not cover an owner
still holding the handle. Nothing else closes this: module refs
pin code; the parent-device reference pins the provider device
struct, not its devm data; the unregister's device_del has no
kernfs-drain semantics for this path (set is reached via the
owner's private pointer, not sysfs).
Fix: make the class layer mark the mux/switch dead at unregister
(WRITE_ONCE flag) and have typec_mux_set()/typec_switch_set()
return -ENODEV on it (READ_ONCE), or provide a drain primitive;
a transitional provider-side option is to allocate the drvdata
with the class device so the lifetimes coincide.
* Gap 2 (contract): aggregate handles have no concurrent protocol
mux.c allocates a fresh aggregate handle per get (:82-129/:320-368)
with no refcount/lock/RCU; typec_mux_set() (:393-412) walks the
callback list unlocked; typec_mux_put() kfrees on the final put;
and typec_unregister_altmode() even suggests the fragile order
(put at class.c:712 before device_unregister at :713). I audited
all owners: UCSI/cros/tipd/gaokun serialize set-vs-put by their
own construction (work drains, free_irq, notifier unregister --
tabulated in my notes; the one in-tree violator,
pmic_glink_altmode, has no cancel/flush at all and is reported
separately). The retimer class does it right (kref'd device,
retimer.c:65-81) -- the mux/switch aggregate handles are the
historical weak spot. Fix: document the drain+put discipline in
include/linux/usb/typec_mux.h (it currently says nothing about
concurrency) or provide a class-layer drain+put primitive.
* Gap 3 (latent): mode_selection state_update vs delete
mode_selection.c:176-197 (state_update: unlocked read of
partner->sel at :179, then mutex_lock(&sel->lock) + list access)
vs :271-283 (delete: partner->sel = NULL; cancel_delayed_work_sync
(covers only that work); mode_list_clean() at :102-110 which does
NOT take sel->lock and kfrees every node; mutex_destroy(); kfree
(sel)). Any overlap = UAF (locking a destroyed mutex; racing the
list cleanup). Today unreachable: the single update entry
(ucsi_altmode_update_active, ucsi.c:354-384) is only called under
con->lock (ucsi.c:1029/:1522/displayport.c:255 via the locked
ucsi_con_mutex_lock/thunderbolt.c:109), and the only delete
implementation (cros_ec) is likewise under con->lock
(ucsi.c:1317-1318 from :2004/:1513/:1402/:2156) -- the header
(typec_altmode.h:247-285) documents state_update as a generic
altmode-driver entry point but states no locking precondition, so
the first future caller that trusts the header reintroduces the
UAF. Fix: in-delete locking (mutex_lock/unlock(&sel->lock) before
the cleanup) or a kref/RCU anchor, and document the contract.
These three are reported together because they are the same layer
and the same root style: safety outsourced to undocumented caller
discipline. The class layer's own retimer implementation shows
the fix pattern.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
^ permalink raw reply [flat|nested] 10+ messages in thread