mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: andersson@kernel.org, konradybcio@kernel.org
Cc: linux-arm-msm@vger.kernel.org, linux-usb@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: soc: qcom: pmic_glink_altmode: fix use-after-free on unbind  (no cancel_work_sync for port work and enable_work)
Date: Wed, 4 Nov 2026 20:54:38 +0800 (GMT+08:00)	[thread overview]
Message-ID: <5eae64b0.1e583.1a1a6a030a4.Coremail.xuhx@hust.edu.cn> (raw)
In-Reply-To: <2026102727-data-sequel-ce41@gregkh>

[-- 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");

  parent reply	other threads:[~2026-11-04 12:54 UTC|newest]

Thread overview: 21+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
2026-10-27 14:12 ` Greg KH
2026-11-03 11:21   ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
2026-11-04  9:41     ` Qing Wang
2026-11-04 10:07       ` Qing Wang
2026-11-03 11:30   ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
2026-11-03 11:48   ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
2026-11-03 14:51     ` Sasha Levin
2026-11-03 11:57   ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
2026-11-03 15:49     ` Eric Dumazet
2026-11-04 15:03     ` Sasha Levin
2026-11-03 12:07   ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
2026-11-03 12:10   ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
2026-11-04 12:54   ` 徐晗翔 [this message]
2026-11-04 13:04     ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 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 徐晗翔
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 徐晗翔
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   ` 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) 徐晗翔

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=5eae64b0.1e583.1a1a6a030a4.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=andersson@kernel.org \
    --cc=konradybcio@kernel.org \
    --cc=linux-arm-msm@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®