Linux Sound subsystem development
 help / color / mirror / Atom feed
* ALSA: ctxfi: free atimer only after the timer IRQ is quiesced
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
@ 2026-11-13 11:58   ` 徐晗翔
  2026-11-13 16:30     ` Takashi Iwai
  2026-11-13 12:03   ` ALSA: ctxfi: do not free a running system timer when timer_delete_sync_try() returns -1 (UAF) 徐晗翔
                     ` (5 subsequent siblings)
  6 siblings, 1 reply; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 11:58 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

`ct_timer_free()` frees `struct ct_timer` while the ctxfi timer IRQ handler can
still be executing, or can start after `snd_card_disconnect()` has already done
its single `synchronize_irq()`.  A handler that has passed the non-atomic
`hw->irq_callback` check then calls `ct_timer_interrupt()` with the freed
`atimer`; `free_irq()` only runs later, inside `ct_atc_destroy()` ->
`destroy_hw_obj()`.  This is a use-after-free on the timer object and can crash
or wedge the device during PCI removal/unbind.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source tree under review.  This report is source-level only: no
  ctxfi hardware was available and QEMU has no ctxfi device model, so no
  driver-specific reproducer was run.  The source paths/line numbers below are
  from 7.3.

* Mechanism

  `ct_timer_new()` (`sound/pci/ctxfi/cttimer.c:388-411`) installs the native
  timer callback by default when `!use_system_timer && hw->set_timer_irq`:

    hw->irq_callback_data = atimer;
    hw->irq_callback      = ct_timer_interrupt;

  The PCI IRQ handlers `ct_20k1_interrupt()` (`cthw20k1.c:1878-1891`) and
  `ct_20k2_interrupt()` (`cthw20k2.c:2045-2059`) do:

    if (hw->irq_callback)
        hw->irq_callback(hw->irq_callback_data, status);

  `ct_timer_interrupt()` (`cttimer.c:379-386`) then reads
  `timer->ops->interrupt` and calls it with `timer`.  Neither the check nor
  the two loads is atomic.

  `ct_timer_free()` (`cttimer.c:414-421`) does:

    struct hw *hw = atimer->atc->hw;
    hw->irq_callback = NULL;
    if (atimer->ops->free_global)
        atimer->ops->free_global(atimer);
    kfree(atimer);

  `ct_xfitimer_free_global()` only calls `ct_xfitimer_irq_stop()`; that helper
  stops the hardware only if `atimer->running` is set (`cttimer.c:137-145`).
  It does not wait for an in-flight IRQ handler.

  `ct_atc_destroy()` (`ctatc.c:1250-1286`) then calls `ct_timer_free()` at
  1257-1260, calls `destroy_hw_obj(atc->hw)` at 1274-1275, and only after that
  `kfree(atc)` at 1283.  `destroy_hw_obj()` -> `hw_card_shutdown()` ->
  `free_irq()` at `cthw20k1.c:1969` / `cthw20k2.c:2140`.  So `atc` itself is
  not the freed object in this window; `atimer` is freed before `free_irq()`.

* Why the existing synchronization does not close the window

  ctxfi does set `card->sync_irq` at `cthw20k1.c:1937` / `cthw20k2.c:2104`,
  and `snd_card_disconnect()` (`sound/core/init.c:495-562`) calls
  `synchronize_irq(card->sync_irq)` once at 540-541.  But that call happens
  before `ct_timer_free()`, only waits handlers that are running at that
  instant, and does not disable the IRQ line.  A new handler can enter after
  the one-time sync and before `free_irq()`.

  The `hw->irq_callback = NULL` store does not close the race either: a
  handler that loaded the callback pointer before the store can still call it.

* Trigger model

  Open a ctxfi device (native timer path, the default), then remove/unbind the
  PCI device while an interrupt is pending or arrives during teardown.  The
  window is between `ct_timer_free()` and `free_irq()`.  Consequence: UAF read
  of `atimer->ops` and timer fields in IRQ context; crash, stall, or potential
  control-flow corruption.

* Suggested direction

  After clearing `hw->irq_callback` and calling `free_global()`, but before
  `kfree(atimer)`, do:

    if (hw->irq >= 0)
        synchronize_irq(hw->irq);

  Alternatively, move `free_irq()` earlier so it precedes the timer free.
  Keep the existing `hw->irq == -1` initialization contract.


Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

* ALSA: ctxfi: do not free a running system timer when timer_delete_sync_try() returns -1 (UAF)
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
  2026-11-13 11:58   ` ALSA: ctxfi: free atimer only after the timer IRQ is quiesced 徐晗翔
@ 2026-11-13 12:03   ` 徐晗翔
  2026-11-13 12:04   ` ALSA: asihpi: free IRQ before snd_card_free()/HPI delete on HPI6205 removal (UAF) 徐晗翔
                     ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:03 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 4108 bytes --]

`ct_systimer_prepare()` is used as both the prepare callback and the
`free_instance` callback.  It calls `timer_delete_sync_try()` and ignores the
return value; `-1` means the timer callback is running on another CPU and the
timer was NOT deleted.  `ct_timer_instance_free()` then removes the instance
and `kfree()`s it while `ct_systimer_callback()` can still dereference
`ti->substream`, `ti->substream->runtime`, `ti->apcm`, and `ti->position`.
A normal PCM close with `use_system_timer=1` can therefore produce a UAF of
the timer instance, the PCM runtime, and the driver's private PCM object.

The detailed description:

* Test environment / affected version

  Linux 7.3.0, source analysis plus a QEMU/KASAN primitive reproducer.
  The reproducer uses the real exported `timer_delete_sync_try()` and a timer
  callback; it does not call the static ctxfi functions or emulate ctxfi
  hardware.

* Mechanism

  `ct_systimer_callback()` (`sound/pci/ctxfi/cttimer.c:63-87`) reads
  `ti->substream`, `substream->runtime`, `ti->apcm`,
  `substream->ops->pointer`, `apcm->interrupt`, and `ti->position`; it takes
  `ti->lock` only near the end and may re-arm the timer.

  `ct_systimer_stop()` (`cttimer.c:105-110`) locks `ti->lock`, clears
  `ti->running`, and calls `timer_delete()`.  `ct_systimer_prepare()`
  (`cttimer.c:112-116`) then does:

    ct_systimer_stop(ti);
    timer_delete_sync_try(&ti->timer);

  `#define ct_systimer_free ct_systimer_prepare` at line 118 makes the same
  function the `free_instance` callback, and `ct_systimer_ops.free_instance`
  is set at 120-126.  `ct_timer_instance_free()` (`cttimer.c:360-373`) calls
  `atimer->ops->stop()`, then `free_instance()`, then `list_del()` under
  `list_lock`, then `kfree(ti)`.

  `timer_delete_sync_try()` (`kernel/time/timer.c:1488-1492`) returns 0 if the
  timer was not pending, 1 if it was pending and deactivated, and -1 if the
  callback is running on another CPU.  It never waits.  The return value is
  ignored here.

  The PCM close path is `snd_pcm_release_substream()`
  (`sound/core/pcm_native.c:2785-2805`) -> `snd_pcm_drop()` ->
  `snd_pcm_detach_substream()` (`sound/core/pcm.c:978-1005`).  The ctxfi
  `runtime->private_free` is `ct_atc_pcm_free_substream()`
  (`sound/pci/ctxfi/ctpcm.c:103-112`), which calls
  `ct_timer_instance_free(apcm->timer)` and then `kfree(apcm)`.  Thus a
  callback running on another CPU can use `ti`, `ti->substream->runtime`, and
  `apcm` after they are freed.

* Why the fix cannot simply sleep in ct_systimer_prepare()

  The same callback is also `ct_timer_ops.prepare`, called from
  `ct_timer_prepare()` while `snd_pcm_prepare()` holds
  `pcm_stream_lock_irq` (`pcm_native.c:2042-2050`).  Replacing the try with a
  sleeping `timer_delete_sync()` there would cause "scheduling while atomic".
  The prepare path must keep a non-sleeping delete/try; the close/free path
  needs a separate method.

* Trigger model

  `use_system_timer=1` (module parameter; default is false), open/prepare a
  PCM, let the system timer callback run, then close the PCM while the
  callback is still active on another CPU.  SMP/preemption required.
  Consequence: UAF in timer softirq; crash or memory corruption.

* Dynamic evidence

  `sndpci_race.c`, mode `v3`, runs the real
  `timer_delete_sync_try()`; when it returns -1 the test frees the object while
  the callback is in `mdelay()`. Show 16 KASAN reports:

    sndpci_race: v3 iter=0 delete_sync_try=-1
    BUG: KASAN: slab-use-after-free in v3_callback
    ...
    Allocated by task 86: v3_thread
    Freed by task 86: kfree -> v3_thread



* Suggested direction

  Split the callbacks: keep `ct_systimer_prepare()` non-sleeping for the
  prepare path, and add a separate `ct_systimer_free()` for close/free that
  first clears the callback/running state and then calls
  `timer_shutdown_sync(&ti->timer)` (or `timer_delete_sync()`), before the
  instance list removal and `kfree(ti)`.  Do not call the sleeping helper from
  the PCM prepare path.


Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: sndpci_race.c --]
[-- Type: text/plain, Size: 9729 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * sndpci_race.c - minimal ALSA/kernel lifetime-race reproducers for the
 * sound/pci manual-review cases V3/V4/V5.
 *
 * The module deliberately reproduces the *lifetime patterns* used by
 * ctxfi/asihpi, using only exported ALSA core and timer APIs.  It is not a
 * ctxfi/asihpi emulator: those drivers need hardware that this QEMU does not
 * provide.  The purpose is to test whether the core/API windows identified in
 * the report are real on an instrumented KASAN kernel.
 *
 * modes:
 *   v3  - timer_delete_sync_try() returns -1 while the callback runs on
 *         another CPU; free the object anyway (ctxfi system-timer pattern).
 *   pcm - create a fake ALSA card with two playback substreams.  The guest
 *         program pcmtest then exercises close (V4) or link+close (V5).
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/cpumask.h>
#include <linux/sched.h>
#include <linux/container_of.h>
#include <linux/string.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV "sndpci_race"
#define COOKIE 0x5a5a1234deadbeefULL

static char *mode = "pcm";
module_param(mode, charp, 0444);
static bool safe;
module_param(safe, bool, 0444);
MODULE_PARM_DESC(mode, "v3 or pcm");

/* ------------------------------------------------------------------ */
/* V3: timer_delete_sync_try() ignored -1                              */
/* ------------------------------------------------------------------ */
struct v3_obj {
	struct timer_list timer;
	u64 cookie;
	struct task_struct *thread;
};

static void v3_callback(struct timer_list *t)
{
	struct v3_obj *v = container_of(t, struct v3_obj, timer);

	/* Keep the callback alive long enough for the freeing CPU to run. */
	mdelay(20);
	pr_info(DRV ": v3 callback cookie=%#llx\n", v->cookie);
	/* Second read after the delay: the object may already be freed. */
	if (v->cookie == COOKIE)
		pr_info(DRV ": v3 callback read-after-free survived\n");
}

static int v3_thread(void *arg)
{
	struct v3_obj *v;
	int i, ret;

	for (i = 0; i < 8; i++) {
		v = kzalloc(sizeof(*v), GFP_KERNEL);
		if (!v)
			return -ENOMEM;
		v->cookie = COOKIE;
		timer_setup(&v->timer, v3_callback, 0);
		/* Pin the callback to CPU0; this thread runs on CPU1. */
		add_timer_on(&v->timer, 0);
		usleep_range(2000, 3000);
		ret = timer_delete_sync_try(&v->timer);
		pr_info(DRV ": v3 iter=%d delete_sync_try=%d\n", i, ret);
		if (ret == -1) {
			/* BUG: callback is running on CPU0; free it anyway. */
			if (safe)
				timer_delete_sync(&v->timer);
			kfree(v);
		} else {
			timer_delete_sync(&v->timer);
			kfree(v);
		}
		usleep_range(60000, 70000);
	}
	pr_info(DRV ": v3 done\n");
	return 0;
}

/* ------------------------------------------------------------------ */
/* V4/V5: fake ALSA PCM with a driver-private dpcm and a timer        */
/* ------------------------------------------------------------------ */
struct race_pcm {
	struct timer_list timer;
	struct snd_pcm_substream *substream;
	u64 cookie;
	bool respawn;
	bool active;
};

static struct snd_card *race_card;
static struct platform_device *race_pdev;
static struct snd_pcm *race_pcm_dev;

static void race_pcm_timer(struct timer_list *t)
{
	struct race_pcm *dpcm = container_of(t, struct race_pcm, timer);
	struct snd_pcm_substream *s;
	struct snd_pcm_runtime *victim_rt = NULL;
	u64 cookie;

	if (!dpcm->active)
		return;
	pr_info(DRV ": timer start dpcm=%px\n", dpcm);

	/*
	 * V5 pattern: walk the link group without the group lock and read
	 * each member's runtime/private_data.  V4 uses the same callback for
	 * the current stream; the delay widens the close/free window.
	 */
	cookie = dpcm->cookie;
	if (cookie == COOKIE && dpcm->substream && dpcm->substream->runtime) {
		snd_pcm_group_for_each_entry(s, dpcm->substream) {
			struct snd_pcm_runtime *rt = READ_ONCE(s->runtime);
			struct race_pcm *d = NULL;

			if (rt)
				d = READ_ONCE(rt->private_data);
			if (d && d->cookie == COOKIE) {
				/* Force the compiler to keep the load. */
				barrier();
			}
			if (rt) {
				(void)READ_ONCE(rt->dma_area);
				if (s != dpcm->substream && !victim_rt)
					victim_rt = rt;
			}
			/* A real driver also reports elapsed periods. */
			if (rt)
				snd_pcm_period_elapsed(s);
		}
	}

	/* Widen the window between the callback's load and the free. */
	mdelay(20);
	if (victim_rt) {
		struct race_pcm *d = READ_ONCE(victim_rt->private_data);

		pr_info(DRV ": victim use rt=%px d=%px\n", victim_rt, d);
		(void)READ_ONCE(victim_rt->dma_area);
	}
	pr_info(DRV ": timer end dpcm=%px\n", dpcm);
	if (dpcm->respawn) {
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
	}
}

static void race_pcm_free(struct snd_pcm_runtime *runtime)
{
	struct race_pcm *dpcm = runtime->private_data;

	pr_info(DRV ": pcm private_free dpcm=%px\n", dpcm);
	if (safe)
		timer_shutdown_sync(&dpcm->timer);
	kfree(dpcm);
}

static int race_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *dpcm;

	dpcm = kzalloc(sizeof(*dpcm), GFP_KERNEL);
	if (!dpcm)
		return -ENOMEM;
	dpcm->substream = substream;
	dpcm->cookie = COOKIE;
	timer_setup(&dpcm->timer, race_pcm_timer, 0);
	runtime->private_data = dpcm;
	runtime->private_free = race_pcm_free;
	runtime->hw.info = SNDRV_PCM_INFO_MMAP |
			   SNDRV_PCM_INFO_INTERLEAVED |
			   SNDRV_PCM_INFO_MMAP_VALID;
	runtime->hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
	runtime->hw.rates = SNDRV_PCM_RATE_44100;
	runtime->hw.rate_min = 44100;
	runtime->hw.rate_max = 44100;
	runtime->hw.channels_min = 2;
	runtime->hw.channels_max = 2;
	runtime->hw.buffer_bytes_max = 64 * 1024;
	runtime->hw.period_bytes_min = 1024;
	runtime->hw.period_bytes_max = 4096;
	runtime->hw.periods_min = 2;
	runtime->hw.periods_max = 8;
	pr_info(DRV ": pcm open sub=%px dpcm=%px\n", substream, dpcm);
	return 0;
}

static int race_pcm_close(struct snd_pcm_substream *substream)
{
	pr_info(DRV ": pcm .close sub=%px\n", substream);
	return 0;
}

static int race_pcm_ioctl(struct snd_pcm_substream *substream,
			  unsigned int cmd, void *arg)
{
	pr_info(DRV ": pcm ioctl cmd=%u\n", cmd);
	return snd_pcm_lib_ioctl(substream, cmd, arg);
}

static int race_pcm_hw_params(struct snd_pcm_substream *substream,
			      struct snd_pcm_hw_params *params)
{
	pr_info(DRV ": hw_params rate=%u ch=%u fmt=%u period_bytes=%u buffer_bytes=%u\n",
		params_rate(params), params_channels(params), params_format(params),
		params_period_bytes(params), params_buffer_bytes(params));
	return 0;
}

static int race_pcm_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int race_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct race_pcm *dpcm = substream->runtime->private_data;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		if (substream != race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream)
			break;
		dpcm->active = true;
		dpcm->respawn = true;
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		dpcm->active = false;
		dpcm->respawn = false;
		/* Deliberately not timer_delete_sync(): mirrors asihpi. */
		timer_delete(&dpcm->timer);
		break;
	}
	return 0;
}

static snd_pcm_uframes_t race_pcm_pointer(struct snd_pcm_substream *substream)
{
	return 0;
}

static const struct snd_pcm_ops race_pcm_ops = {
	.open = race_pcm_open,
	.close = race_pcm_close,
	.ioctl = race_pcm_ioctl,
	.hw_params = race_pcm_hw_params,
	.prepare = race_pcm_prepare,
	.trigger = race_pcm_trigger,
	.pointer = race_pcm_pointer,
};

static int __init sndpci_init(void)
{
	struct task_struct *t;
	int ret;

	pr_info(DRV ": init mode=%s\n", mode);

	if (!strcmp(mode, "v3")) {
		t = kthread_create(v3_thread, NULL, "sndpci_v3");
		if (IS_ERR(t))
			return PTR_ERR(t);
		kthread_bind(t, 1);
		wake_up_process(t);
		return 0;
	}

	race_pdev = platform_device_register_simple("sndpci_race", -1, NULL, 0);
	if (IS_ERR(race_pdev))
		return PTR_ERR(race_pdev);
	ret = snd_card_new(&race_pdev->dev, 0, "SndPciRace", THIS_MODULE, 0, &race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_new failed: %d\n", ret);
		return ret;
	}
	ret = snd_pcm_new(race_card, "SndPciRace", 0, 0, 2, &race_pcm_dev);
	if (ret < 0) {
		pr_err(DRV ": snd_pcm_new failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	snd_pcm_set_ops(race_pcm_dev, SNDRV_PCM_STREAM_CAPTURE,
			&race_pcm_ops);
	snd_pcm_lib_preallocate_pages_for_all(race_pcm_dev, SNDRV_DMA_TYPE_CONTINUOUS,
						      NULL, 64 * 1024, 64 * 1024);
	ret = 0;
	pr_info(DRV ": managed_buffer ret=%d dma_type=%d area=%px bytes=%zu\n", ret,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.dev.type,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.area,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.bytes);
	ret = snd_card_register(race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_register failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	pr_info(DRV ": card registered\n");
	return 0;
}

static void __exit sndpci_exit(void)
{
	pr_info(DRV ": exit\n");
	if (race_card)
		snd_card_free(race_card);
	if (race_pdev)
		platform_device_unregister(race_pdev);
}

module_init(sndpci_init);
module_exit(sndpci_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sound/pci lifetime-race reproducer (V3/V4/V5)");

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

* ALSA: asihpi: free IRQ before snd_card_free()/HPI delete on HPI6205 removal (UAF)
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
  2026-11-13 11:58   ` ALSA: ctxfi: free atimer only after the timer IRQ is quiesced 徐晗翔
  2026-11-13 12:03   ` ALSA: ctxfi: do not free a running system timer when timer_delete_sync_try() returns -1 (UAF) 徐晗翔
@ 2026-11-13 12:04   ` 徐晗翔
  2026-11-13 12:06   ` ALSA: asihpi: quiesce PCM timer and IRQ before freeing dpcm on close (UAF) 徐晗翔
                     ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:04 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

`snd_asihpi_remove()` frees the ALSA card and then sends HPI_ADAPTER_DELETE
(which frees the HPI6205 `phw`) before `free_irq()`.  Both the primary IRQ
handler and the threaded handler can therefore run against freed memory:
the primary handler dereferences `phw->prHSR` through
`adapter_irq_query_and_clear()`, and the threaded callback dereferences
`a->snd_card->private_data` after the card private area has been freed.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source tree.  Affected hardware path: ASI6205/HPI6205 in
  interrupt mode (`low_latency_mode && irq_supported`, `interrupt_mode=1`).
  HPI6000 does not set `irq_query_and_clear` and is polled, so it is not part
  of this report.

* Mechanism

  `snd_asihpi_remove()` (`sound/pci/asihpi/asihpi.c:2919-2933`):

    hpi->interrupt_callback = NULL;          /* if interrupt_mode */
    ... send HPI IRQ rate 0 ...
    snd_card_free(hpi->snd_card);            /* line 2930 */
    hpi->snd_card = NULL;                    /* line 2931 */
    asihpi_adapter_remove(pci_dev);          /* line 2932 */

  `asihpi_adapter_remove()` (`hpioctl.c:520-555`) then sends IRQ rate 0,
  sends `HPI_ADAPTER_DELETE` (537-540), and only afterwards calls
  `free_irq(pa->irq, pa)` (542-543).

  HPI6205 `adapter_delete()` (`hpi6205.c:491-510`) -> `delete_adapter_obj()`
  (`hpi6205.c:675-704`) does `kfree(phw)` at 703 without clearing
  `pao->priv`, then `hpi_delete_adapter()` (`hpicmn.c:96-108`) memsets the
  registry object at 106.  The primary handler `asihpi_isr()`
  (`hpioctl.c:311-335`) checks `a->adapter->irq_query_and_clear` and calls
  it; HPI6205 installs that handler at `hpi6205.c:662`, and
  `adapter_irq_query_and_clear()` (`hpi6205.c:708-722`) does
  `ioread32(phw->prHSR)` at 714.

  Window A (phw UAF): the handler loads the function pointer before
  `delete_adapter_obj()` frees `phw`, and executes after the free.

  Window B (NULL deref): the handler executes after `hpi_delete_adapter()`
  memsets the registry, so `pao->priv` is NULL and `phw->prHSR` is a NULL
  dereference.

  Window C (card private UAF): `asihpi_isr_thread()` (`hpioctl.c:337-344`)
  calls `a->interrupt_callback(a)`; `snd_card_asihpi_isr()`
  (`asihpi.c:898-907`) reads `a->snd_card->private_data` and
  `asihpi->llmode_streampriv`.  The card private area is allocated by
  `snd_card_new(..., sizeof(struct snd_card_asihpi), ...)`
  (`asihpi.c:2794-2799`) and freed by `snd_card_do_free()`
  (`sound/core/init.c:584-611`, `kfree(card)` at 610).  After
  `hpi->snd_card = NULL`, the same callback can also return NULL from the
  `snd_card` load and hit the WARN_ON/NULL path.

* Why existing synchronization does not close the window

  asihpi never sets `card->sync_irq`, so `snd_card_disconnect()` does not call
  `synchronize_irq()`.  `snd_card_free()` waits for open files and the card
  release completion, not for the IRQ handler/thread.  `free_irq()` is the
  first operation that quiesces the IRQ, and it happens after `snd_card_free()`
  and after HPI_ADAPTER_DELETE has freed `phw`.

* Trigger model

  HPI6205 device in IRQ mode; remove/unbind the PCI device.  Root is needed
  for unbind/remove.  Consequence: UAF/NULL deref in hard IRQ or threaded IRQ
  context; crash or memory corruption.  The report does not claim a
  concurrent user ioctl path: the card file drain prevents that, and only the
  single remove path sends HPI_ADAPTER_DELETE.

* Suggested direction 

  Free/quiesce the IRQ before `snd_card_free()` and before sending
  HPI_ADAPTER_DELETE:

    if (hpi->irq) {
        free_irq(hpi->irq, hpi);
        hpi->irq = 0;
    }
    snd_card_free(hpi->snd_card);

  Keep the existing `if (pa->irq)` guard in `asihpi_adapter_remove()` to avoid
  a double free.  Alternatively set `card->sync_irq` and perform an explicit
  `synchronize_irq()` before releasing the card private data, but freeing the
  IRQ first is simpler and also covers the `phw` window.



Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

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

* ALSA: asihpi: quiesce PCM timer and IRQ before freeing dpcm on close (UAF)
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
                     ` (2 preceding siblings ...)
  2026-11-13 12:04   ` ALSA: asihpi: free IRQ before snd_card_free()/HPI delete on HPI6205 removal (UAF) 徐晗翔
@ 2026-11-13 12:06   ` 徐晗翔
  2026-11-13 12:15   ` ALSA: pcm: link-group walk in timer callback can use a closed substream's freed runtime (UAF) 徐晗翔
                     ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:06 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 3582 bytes --]

`snd_card_asihpi_runtime_free()` frees the per-open `struct
snd_card_asihpi_pcm` while a polled timer callback or an IRQ/threaded handler
can still use it.  The polled stop path uses `timer_delete()` (not
`timer_delete_sync()`), and the IRQ stop path clears
`card->llmode_streampriv` without `synchronize_irq()`.  A normal PCM close can
therefore free `dpcm` and then `runtime` while the callback is still running.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source plus a QEMU/KASAN fake-PCM harness.  No ASI6205 hardware
  was available; the harness validates the ALSA core close ordering and the
  exact `timer_delete`/`llmode_streampriv` lifetime pattern.

* Mechanism

  `snd_card_asihpi_runtime_free()` (`asihpi.c:495-499`) does `kfree(dpcm)` and
  is installed as `runtime->private_free` at playback open
  (`asihpi.c:997-1000`) and capture open (`1166-1169`).

  Polled mode:

    snd_card_asihpi_pcm_timer_start()  asihpi.c:501-513
        mod_timer(&dpcm->timer, jiffies + expiry);
        dpcm->respawn_timer = 1;

    snd_card_asihpi_pcm_timer_stop()   asihpi.c:515-522
        dpcm->respawn_timer = 0;
        timer_delete(&dpcm->timer);     /* no sync */

  `snd_card_asihpi_timer_function()` (`asihpi.c:710-896`) uses `dpcm`
  throughout and can re-arm at 894-895 if it observes `respawn_timer`.

  IRQ mode:

    snd_card_asihpi_pcm_int_start()    asihpi.c:524-538
        card->llmode_streampriv = dpcm;
        ... set HPI IRQ rate ...

    snd_card_asihpi_pcm_int_stop()     asihpi.c:540-550
        ... set HPI IRQ rate 0 ...
        card->llmode_streampriv = NULL;  /* no synchronize_irq */

  `snd_card_asihpi_isr()` (`asihpi.c:898-907`) reads
  `asihpi->llmode_streampriv` twice (check, then dereference), so a threaded
  handler can hold the old `dpcm` pointer across the NULL store.

  Close path: `snd_pcm_release_substream()`
  (`sound/core/pcm_native.c:2785-2805`) -> `snd_pcm_drop()`
  (`pcm_native.c:2267-2289`) -> `snd_pcm_detach_substream()`
  (`sound/core/pcm.c:978-1005`) -> `runtime->private_free()` -> `kfree(dpcm)`
  and then `kfree(runtime)`.


* Trigger model

  Local user opens a capture or playback PCM, starts it, and closes it while
  the polled timer or HPI IRQ is active.  Polled mode is available on HPI6000
  and HPI6205; IRQ mode requires HPI6205 in interrupt mode.  SMP/preemption
  needed for the cross-CPU window.  Consequence: UAF of `dpcm`/`runtime` in
  timer softirq or threaded IRQ; crash or memory corruption.

* Dynamic evidence

  `sndpci_race.c` mode `pcm`, `verification/kmod/
  sndpci/pcmtest.c close 200`: `.trigger STOP` uses `timer_delete()` and
  `runtime->private_free` frees `dpcm`; timer pinned to CPU0, user close to
  CPU1.  Contain 35 KASAN UAF report fragments:

    BUG: KASAN: slab-use-after-free in race_pcm_timer
    ...
    Allocated by task: race_pcm_open
    Freed by task: kfree -> snd_pcm_detach_substream -> snd_pcm_release



* Suggested direction

  In `snd_card_asihpi_runtime_free()` (or immediately before it), for polled
  mode call `timer_shutdown_sync(&dpcm->timer)`.  For IRQ mode, first clear
  `card->llmode_streampriv` (with the appropriate ordering), then
  `synchronize_irq(card->hpi->irq)` before `kfree(dpcm)`.  Do not put the
  sleeping sync inside the PCM stream lock; do it in the detach/private_free
  process context.  Also consider quiescing before the HPI stream `.close`
  (the extra HPI stream handle window is discussed in the report).



Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: sndpci_race.c --]
[-- Type: text/plain, Size: 9729 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * sndpci_race.c - minimal ALSA/kernel lifetime-race reproducers for the
 * sound/pci manual-review cases V3/V4/V5.
 *
 * The module deliberately reproduces the *lifetime patterns* used by
 * ctxfi/asihpi, using only exported ALSA core and timer APIs.  It is not a
 * ctxfi/asihpi emulator: those drivers need hardware that this QEMU does not
 * provide.  The purpose is to test whether the core/API windows identified in
 * the report are real on an instrumented KASAN kernel.
 *
 * modes:
 *   v3  - timer_delete_sync_try() returns -1 while the callback runs on
 *         another CPU; free the object anyway (ctxfi system-timer pattern).
 *   pcm - create a fake ALSA card with two playback substreams.  The guest
 *         program pcmtest then exercises close (V4) or link+close (V5).
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/cpumask.h>
#include <linux/sched.h>
#include <linux/container_of.h>
#include <linux/string.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV "sndpci_race"
#define COOKIE 0x5a5a1234deadbeefULL

static char *mode = "pcm";
module_param(mode, charp, 0444);
static bool safe;
module_param(safe, bool, 0444);
MODULE_PARM_DESC(mode, "v3 or pcm");

/* ------------------------------------------------------------------ */
/* V3: timer_delete_sync_try() ignored -1                              */
/* ------------------------------------------------------------------ */
struct v3_obj {
	struct timer_list timer;
	u64 cookie;
	struct task_struct *thread;
};

static void v3_callback(struct timer_list *t)
{
	struct v3_obj *v = container_of(t, struct v3_obj, timer);

	/* Keep the callback alive long enough for the freeing CPU to run. */
	mdelay(20);
	pr_info(DRV ": v3 callback cookie=%#llx\n", v->cookie);
	/* Second read after the delay: the object may already be freed. */
	if (v->cookie == COOKIE)
		pr_info(DRV ": v3 callback read-after-free survived\n");
}

static int v3_thread(void *arg)
{
	struct v3_obj *v;
	int i, ret;

	for (i = 0; i < 8; i++) {
		v = kzalloc(sizeof(*v), GFP_KERNEL);
		if (!v)
			return -ENOMEM;
		v->cookie = COOKIE;
		timer_setup(&v->timer, v3_callback, 0);
		/* Pin the callback to CPU0; this thread runs on CPU1. */
		add_timer_on(&v->timer, 0);
		usleep_range(2000, 3000);
		ret = timer_delete_sync_try(&v->timer);
		pr_info(DRV ": v3 iter=%d delete_sync_try=%d\n", i, ret);
		if (ret == -1) {
			/* BUG: callback is running on CPU0; free it anyway. */
			if (safe)
				timer_delete_sync(&v->timer);
			kfree(v);
		} else {
			timer_delete_sync(&v->timer);
			kfree(v);
		}
		usleep_range(60000, 70000);
	}
	pr_info(DRV ": v3 done\n");
	return 0;
}

/* ------------------------------------------------------------------ */
/* V4/V5: fake ALSA PCM with a driver-private dpcm and a timer        */
/* ------------------------------------------------------------------ */
struct race_pcm {
	struct timer_list timer;
	struct snd_pcm_substream *substream;
	u64 cookie;
	bool respawn;
	bool active;
};

static struct snd_card *race_card;
static struct platform_device *race_pdev;
static struct snd_pcm *race_pcm_dev;

static void race_pcm_timer(struct timer_list *t)
{
	struct race_pcm *dpcm = container_of(t, struct race_pcm, timer);
	struct snd_pcm_substream *s;
	struct snd_pcm_runtime *victim_rt = NULL;
	u64 cookie;

	if (!dpcm->active)
		return;
	pr_info(DRV ": timer start dpcm=%px\n", dpcm);

	/*
	 * V5 pattern: walk the link group without the group lock and read
	 * each member's runtime/private_data.  V4 uses the same callback for
	 * the current stream; the delay widens the close/free window.
	 */
	cookie = dpcm->cookie;
	if (cookie == COOKIE && dpcm->substream && dpcm->substream->runtime) {
		snd_pcm_group_for_each_entry(s, dpcm->substream) {
			struct snd_pcm_runtime *rt = READ_ONCE(s->runtime);
			struct race_pcm *d = NULL;

			if (rt)
				d = READ_ONCE(rt->private_data);
			if (d && d->cookie == COOKIE) {
				/* Force the compiler to keep the load. */
				barrier();
			}
			if (rt) {
				(void)READ_ONCE(rt->dma_area);
				if (s != dpcm->substream && !victim_rt)
					victim_rt = rt;
			}
			/* A real driver also reports elapsed periods. */
			if (rt)
				snd_pcm_period_elapsed(s);
		}
	}

	/* Widen the window between the callback's load and the free. */
	mdelay(20);
	if (victim_rt) {
		struct race_pcm *d = READ_ONCE(victim_rt->private_data);

		pr_info(DRV ": victim use rt=%px d=%px\n", victim_rt, d);
		(void)READ_ONCE(victim_rt->dma_area);
	}
	pr_info(DRV ": timer end dpcm=%px\n", dpcm);
	if (dpcm->respawn) {
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
	}
}

static void race_pcm_free(struct snd_pcm_runtime *runtime)
{
	struct race_pcm *dpcm = runtime->private_data;

	pr_info(DRV ": pcm private_free dpcm=%px\n", dpcm);
	if (safe)
		timer_shutdown_sync(&dpcm->timer);
	kfree(dpcm);
}

static int race_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *dpcm;

	dpcm = kzalloc(sizeof(*dpcm), GFP_KERNEL);
	if (!dpcm)
		return -ENOMEM;
	dpcm->substream = substream;
	dpcm->cookie = COOKIE;
	timer_setup(&dpcm->timer, race_pcm_timer, 0);
	runtime->private_data = dpcm;
	runtime->private_free = race_pcm_free;
	runtime->hw.info = SNDRV_PCM_INFO_MMAP |
			   SNDRV_PCM_INFO_INTERLEAVED |
			   SNDRV_PCM_INFO_MMAP_VALID;
	runtime->hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
	runtime->hw.rates = SNDRV_PCM_RATE_44100;
	runtime->hw.rate_min = 44100;
	runtime->hw.rate_max = 44100;
	runtime->hw.channels_min = 2;
	runtime->hw.channels_max = 2;
	runtime->hw.buffer_bytes_max = 64 * 1024;
	runtime->hw.period_bytes_min = 1024;
	runtime->hw.period_bytes_max = 4096;
	runtime->hw.periods_min = 2;
	runtime->hw.periods_max = 8;
	pr_info(DRV ": pcm open sub=%px dpcm=%px\n", substream, dpcm);
	return 0;
}

static int race_pcm_close(struct snd_pcm_substream *substream)
{
	pr_info(DRV ": pcm .close sub=%px\n", substream);
	return 0;
}

static int race_pcm_ioctl(struct snd_pcm_substream *substream,
			  unsigned int cmd, void *arg)
{
	pr_info(DRV ": pcm ioctl cmd=%u\n", cmd);
	return snd_pcm_lib_ioctl(substream, cmd, arg);
}

static int race_pcm_hw_params(struct snd_pcm_substream *substream,
			      struct snd_pcm_hw_params *params)
{
	pr_info(DRV ": hw_params rate=%u ch=%u fmt=%u period_bytes=%u buffer_bytes=%u\n",
		params_rate(params), params_channels(params), params_format(params),
		params_period_bytes(params), params_buffer_bytes(params));
	return 0;
}

static int race_pcm_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int race_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct race_pcm *dpcm = substream->runtime->private_data;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		if (substream != race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream)
			break;
		dpcm->active = true;
		dpcm->respawn = true;
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		dpcm->active = false;
		dpcm->respawn = false;
		/* Deliberately not timer_delete_sync(): mirrors asihpi. */
		timer_delete(&dpcm->timer);
		break;
	}
	return 0;
}

static snd_pcm_uframes_t race_pcm_pointer(struct snd_pcm_substream *substream)
{
	return 0;
}

static const struct snd_pcm_ops race_pcm_ops = {
	.open = race_pcm_open,
	.close = race_pcm_close,
	.ioctl = race_pcm_ioctl,
	.hw_params = race_pcm_hw_params,
	.prepare = race_pcm_prepare,
	.trigger = race_pcm_trigger,
	.pointer = race_pcm_pointer,
};

static int __init sndpci_init(void)
{
	struct task_struct *t;
	int ret;

	pr_info(DRV ": init mode=%s\n", mode);

	if (!strcmp(mode, "v3")) {
		t = kthread_create(v3_thread, NULL, "sndpci_v3");
		if (IS_ERR(t))
			return PTR_ERR(t);
		kthread_bind(t, 1);
		wake_up_process(t);
		return 0;
	}

	race_pdev = platform_device_register_simple("sndpci_race", -1, NULL, 0);
	if (IS_ERR(race_pdev))
		return PTR_ERR(race_pdev);
	ret = snd_card_new(&race_pdev->dev, 0, "SndPciRace", THIS_MODULE, 0, &race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_new failed: %d\n", ret);
		return ret;
	}
	ret = snd_pcm_new(race_card, "SndPciRace", 0, 0, 2, &race_pcm_dev);
	if (ret < 0) {
		pr_err(DRV ": snd_pcm_new failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	snd_pcm_set_ops(race_pcm_dev, SNDRV_PCM_STREAM_CAPTURE,
			&race_pcm_ops);
	snd_pcm_lib_preallocate_pages_for_all(race_pcm_dev, SNDRV_DMA_TYPE_CONTINUOUS,
						      NULL, 64 * 1024, 64 * 1024);
	ret = 0;
	pr_info(DRV ": managed_buffer ret=%d dma_type=%d area=%px bytes=%zu\n", ret,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.dev.type,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.area,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.bytes);
	ret = snd_card_register(race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_register failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	pr_info(DRV ": card registered\n");
	return 0;
}

static void __exit sndpci_exit(void)
{
	pr_info(DRV ": exit\n");
	if (race_card)
		snd_card_free(race_card);
	if (race_pdev)
		platform_device_unregister(race_pdev);
}

module_init(sndpci_init);
module_exit(sndpci_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sound/pci lifetime-race reproducer (V3/V4/V5)");

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

* ALSA: pcm: link-group walk in timer callback can use a closed substream's freed runtime (UAF)
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
                     ` (3 preceding siblings ...)
  2026-11-13 12:06   ` ALSA: asihpi: quiesce PCM timer and IRQ before freeing dpcm on close (UAF) 徐晗翔
@ 2026-11-13 12:15   ` 徐晗翔
  2026-11-13 12:28   ` ALSA: usb-audio: us144mkii: PCM close frees runtime while playback/feedback URB completion and capture work still use it (UAF) 徐晗翔
  2026-11-13 12:29   ` ALSA: usb-audio: us144mkii: queued stop_pcm_work calls snd_pcm_stop() after PCM close and dereferences NULL runtime 徐晗翔
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:15 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 4714 bytes --]

`snd_card_asihpi_timer_function()` walks the PCM link group with
`snd_pcm_group_for_each_entry()` and dereferences each member's
`s->runtime->private_data`.  That macro is a plain `list_for_each_entry()`;
the callback holds no group reference and no group lock.  Close of another
linked substream can unlink/free the group and detach/free that substream's
runtime and `dpcm`, leaving the timer callback with a stale `s`/runtime
pointer.  This is reachable from user space through
`SNDRV_PCM_IOCTL_LINK` plus close, on hardware whose driver supports PCM
linking (asihpi does).

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source plus a QEMU/KASAN fake-PCM harness.  No ASI6205 hardware
  was available; the harness reproduces the core group traversal and close
  ordering, with the asihpi callback's exact
  `snd_pcm_group_for_each_entry()` pattern.

* Mechanism

  The group loops are at `sound/pci/asihpi/asihpi.c:729-735` and `821-827`.
  They read `s->runtime->private_data` (and in one path `dma_area`) and call
  `snd_pcm_period_elapsed(s)`.  The macro is
  `include/sound/pcm.h:694`:

    #define snd_pcm_group_for_each_entry(s, substream) \
        list_for_each_entry(s, &substream->group->substreams, link_list)

  `snd_pcm_stream_lock_irq()` (`sound/core/pcm_native.c:140-144`) locks
  `substream->self_group`, not the group that the timer callback walks.
  `snd_pcm_unlink()` (`pcm_native.c:2370-2402`) takes
  `snd_pcm_link_rwsem` and the group lock, removes the closing substream,
  relinks the remaining streams, and may `kfree(group)`.
  `snd_pcm_release_substream()` (`pcm_native.c:2785-2805`) calls
  `pcm_release_private()` -> `snd_pcm_unlink()` at 2779-2783 before
  `snd_pcm_detach_substream()` frees the runtime and `dpcm`.

  A timer callback that already loaded member `s` or `s->runtime` can run
  after B's unlink/detach.  It may then read freed `runtime`, freed
  `private_data`, or a freed group list node; `snd_pcm_period_elapsed(s)`
  additionally takes the stale substream.

* Why existing synchronization does not close the window

  `snd_pcm_stop()` does transiently take a group reference through
  `snd_pcm_action()` -> `snd_pcm_stream_group_ref()` (`pcm_native.c:1378-1427`)
  and group lock, but it drops the reference after the STOP action.  The
  asihpi timer callback is not part of that action and holds no group
  reference.  Taking `snd_pcm_stream_lock_irq(A)` in the callback would only
  lock A's `self_group`; it would not pin B or the group.  The group can be
  freed while the callback holds a raw member pointer.

* Trigger model

  Two capture substreams A and B, same PCM and runtime state; user does
  `SNDRV_PCM_IOCTL_LINK` then starts A.  While A's timer callback is walking
  the group, user closes B.  `SNDRV_PCM_IOCTL_LINK` is available to a local
  user with the PCM device open; no special privilege is needed beyond access
  to the PCM.  Consequence: UAF in timer context, crash or memory corruption.


* How to run the PoC

  The harness is self-contained and does not need ASI6205 hardware: it creates
  a fake ALSA card with two capture substreams.  The tested configuration was
  7.3.0 with KASAN and 4 vCPU; a uniprocessor run may not overlap.

  Build against the target kernel build tree:

    printf 'obj-m := sndpci_race.o\n' > Kbuild
    make -C <kernel-build-dir> M=$PWD modules
    gcc -static -O2 -I<kernel-source>/include/uapi -o pcmtest pcmtest.c

  Put `sndpci_race.ko` and `pcmtest` into the guest initramfs/rootfs, boot the
  target kernel (QEMU/KVM recommended), then run as root:

    insmod sndpci_race.ko mode=pcm
    ./pcmtest link 200
    dmesg | grep -E 'BUG: KASAN|race_pcm_timer|victim use'

  In the minimal guest the fake card enumerates as card 0 and `pcmtest` opens
  `/dev/snd/pcmC0D0c`.  If the test machine already has another sound card,
  adjust `DEV` in `pcmtest.c` or use the QEMU initramfs.  Expected result:
  32 KASAN reports with `slab-use-after-free in race_pcm_timer` and about 114
  `victim use` lines.  


* Suggested direction 

  This needs a core/lifetime fix, not just an asihpi timer fix.  Possible
  directions: make group traversal safe against concurrent unlink (group
  reference or RCU-protected/atomic-safe iterator that pins each member's
  runtime), or provide a driver API that snapshots/refcounts members before
  the callback walks them.  On the asihpi side, quiesce the timer/IRQ before
  allowing the linked substream to detach.  Adding only
  `snd_pcm_stream_lock_irq(A)` is not sufficient because it locks A's
  `self_group`, not the group being walked.



Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: sndpci_race.c --]
[-- Type: text/plain, Size: 9729 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * sndpci_race.c - minimal ALSA/kernel lifetime-race reproducers for the
 * sound/pci manual-review cases V3/V4/V5.
 *
 * The module deliberately reproduces the *lifetime patterns* used by
 * ctxfi/asihpi, using only exported ALSA core and timer APIs.  It is not a
 * ctxfi/asihpi emulator: those drivers need hardware that this QEMU does not
 * provide.  The purpose is to test whether the core/API windows identified in
 * the report are real on an instrumented KASAN kernel.
 *
 * modes:
 *   v3  - timer_delete_sync_try() returns -1 while the callback runs on
 *         another CPU; free the object anyway (ctxfi system-timer pattern).
 *   pcm - create a fake ALSA card with two playback substreams.  The guest
 *         program pcmtest then exercises close (V4) or link+close (V5).
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/cpumask.h>
#include <linux/sched.h>
#include <linux/container_of.h>
#include <linux/string.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV "sndpci_race"
#define COOKIE 0x5a5a1234deadbeefULL

static char *mode = "pcm";
module_param(mode, charp, 0444);
static bool safe;
module_param(safe, bool, 0444);
MODULE_PARM_DESC(mode, "v3 or pcm");

/* ------------------------------------------------------------------ */
/* V3: timer_delete_sync_try() ignored -1                              */
/* ------------------------------------------------------------------ */
struct v3_obj {
	struct timer_list timer;
	u64 cookie;
	struct task_struct *thread;
};

static void v3_callback(struct timer_list *t)
{
	struct v3_obj *v = container_of(t, struct v3_obj, timer);

	/* Keep the callback alive long enough for the freeing CPU to run. */
	mdelay(20);
	pr_info(DRV ": v3 callback cookie=%#llx\n", v->cookie);
	/* Second read after the delay: the object may already be freed. */
	if (v->cookie == COOKIE)
		pr_info(DRV ": v3 callback read-after-free survived\n");
}

static int v3_thread(void *arg)
{
	struct v3_obj *v;
	int i, ret;

	for (i = 0; i < 8; i++) {
		v = kzalloc(sizeof(*v), GFP_KERNEL);
		if (!v)
			return -ENOMEM;
		v->cookie = COOKIE;
		timer_setup(&v->timer, v3_callback, 0);
		/* Pin the callback to CPU0; this thread runs on CPU1. */
		add_timer_on(&v->timer, 0);
		usleep_range(2000, 3000);
		ret = timer_delete_sync_try(&v->timer);
		pr_info(DRV ": v3 iter=%d delete_sync_try=%d\n", i, ret);
		if (ret == -1) {
			/* BUG: callback is running on CPU0; free it anyway. */
			if (safe)
				timer_delete_sync(&v->timer);
			kfree(v);
		} else {
			timer_delete_sync(&v->timer);
			kfree(v);
		}
		usleep_range(60000, 70000);
	}
	pr_info(DRV ": v3 done\n");
	return 0;
}

/* ------------------------------------------------------------------ */
/* V4/V5: fake ALSA PCM with a driver-private dpcm and a timer        */
/* ------------------------------------------------------------------ */
struct race_pcm {
	struct timer_list timer;
	struct snd_pcm_substream *substream;
	u64 cookie;
	bool respawn;
	bool active;
};

static struct snd_card *race_card;
static struct platform_device *race_pdev;
static struct snd_pcm *race_pcm_dev;

static void race_pcm_timer(struct timer_list *t)
{
	struct race_pcm *dpcm = container_of(t, struct race_pcm, timer);
	struct snd_pcm_substream *s;
	struct snd_pcm_runtime *victim_rt = NULL;
	u64 cookie;

	if (!dpcm->active)
		return;
	pr_info(DRV ": timer start dpcm=%px\n", dpcm);

	/*
	 * V5 pattern: walk the link group without the group lock and read
	 * each member's runtime/private_data.  V4 uses the same callback for
	 * the current stream; the delay widens the close/free window.
	 */
	cookie = dpcm->cookie;
	if (cookie == COOKIE && dpcm->substream && dpcm->substream->runtime) {
		snd_pcm_group_for_each_entry(s, dpcm->substream) {
			struct snd_pcm_runtime *rt = READ_ONCE(s->runtime);
			struct race_pcm *d = NULL;

			if (rt)
				d = READ_ONCE(rt->private_data);
			if (d && d->cookie == COOKIE) {
				/* Force the compiler to keep the load. */
				barrier();
			}
			if (rt) {
				(void)READ_ONCE(rt->dma_area);
				if (s != dpcm->substream && !victim_rt)
					victim_rt = rt;
			}
			/* A real driver also reports elapsed periods. */
			if (rt)
				snd_pcm_period_elapsed(s);
		}
	}

	/* Widen the window between the callback's load and the free. */
	mdelay(20);
	if (victim_rt) {
		struct race_pcm *d = READ_ONCE(victim_rt->private_data);

		pr_info(DRV ": victim use rt=%px d=%px\n", victim_rt, d);
		(void)READ_ONCE(victim_rt->dma_area);
	}
	pr_info(DRV ": timer end dpcm=%px\n", dpcm);
	if (dpcm->respawn) {
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
	}
}

static void race_pcm_free(struct snd_pcm_runtime *runtime)
{
	struct race_pcm *dpcm = runtime->private_data;

	pr_info(DRV ": pcm private_free dpcm=%px\n", dpcm);
	if (safe)
		timer_shutdown_sync(&dpcm->timer);
	kfree(dpcm);
}

static int race_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *dpcm;

	dpcm = kzalloc(sizeof(*dpcm), GFP_KERNEL);
	if (!dpcm)
		return -ENOMEM;
	dpcm->substream = substream;
	dpcm->cookie = COOKIE;
	timer_setup(&dpcm->timer, race_pcm_timer, 0);
	runtime->private_data = dpcm;
	runtime->private_free = race_pcm_free;
	runtime->hw.info = SNDRV_PCM_INFO_MMAP |
			   SNDRV_PCM_INFO_INTERLEAVED |
			   SNDRV_PCM_INFO_MMAP_VALID;
	runtime->hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
	runtime->hw.rates = SNDRV_PCM_RATE_44100;
	runtime->hw.rate_min = 44100;
	runtime->hw.rate_max = 44100;
	runtime->hw.channels_min = 2;
	runtime->hw.channels_max = 2;
	runtime->hw.buffer_bytes_max = 64 * 1024;
	runtime->hw.period_bytes_min = 1024;
	runtime->hw.period_bytes_max = 4096;
	runtime->hw.periods_min = 2;
	runtime->hw.periods_max = 8;
	pr_info(DRV ": pcm open sub=%px dpcm=%px\n", substream, dpcm);
	return 0;
}

static int race_pcm_close(struct snd_pcm_substream *substream)
{
	pr_info(DRV ": pcm .close sub=%px\n", substream);
	return 0;
}

static int race_pcm_ioctl(struct snd_pcm_substream *substream,
			  unsigned int cmd, void *arg)
{
	pr_info(DRV ": pcm ioctl cmd=%u\n", cmd);
	return snd_pcm_lib_ioctl(substream, cmd, arg);
}

static int race_pcm_hw_params(struct snd_pcm_substream *substream,
			      struct snd_pcm_hw_params *params)
{
	pr_info(DRV ": hw_params rate=%u ch=%u fmt=%u period_bytes=%u buffer_bytes=%u\n",
		params_rate(params), params_channels(params), params_format(params),
		params_period_bytes(params), params_buffer_bytes(params));
	return 0;
}

static int race_pcm_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int race_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct race_pcm *dpcm = substream->runtime->private_data;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		if (substream != race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream)
			break;
		dpcm->active = true;
		dpcm->respawn = true;
		if (!timer_pending(&dpcm->timer))
			add_timer_on(&dpcm->timer, 0);
		else
			mod_timer(&dpcm->timer, jiffies + 1);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		dpcm->active = false;
		dpcm->respawn = false;
		/* Deliberately not timer_delete_sync(): mirrors asihpi. */
		timer_delete(&dpcm->timer);
		break;
	}
	return 0;
}

static snd_pcm_uframes_t race_pcm_pointer(struct snd_pcm_substream *substream)
{
	return 0;
}

static const struct snd_pcm_ops race_pcm_ops = {
	.open = race_pcm_open,
	.close = race_pcm_close,
	.ioctl = race_pcm_ioctl,
	.hw_params = race_pcm_hw_params,
	.prepare = race_pcm_prepare,
	.trigger = race_pcm_trigger,
	.pointer = race_pcm_pointer,
};

static int __init sndpci_init(void)
{
	struct task_struct *t;
	int ret;

	pr_info(DRV ": init mode=%s\n", mode);

	if (!strcmp(mode, "v3")) {
		t = kthread_create(v3_thread, NULL, "sndpci_v3");
		if (IS_ERR(t))
			return PTR_ERR(t);
		kthread_bind(t, 1);
		wake_up_process(t);
		return 0;
	}

	race_pdev = platform_device_register_simple("sndpci_race", -1, NULL, 0);
	if (IS_ERR(race_pdev))
		return PTR_ERR(race_pdev);
	ret = snd_card_new(&race_pdev->dev, 0, "SndPciRace", THIS_MODULE, 0, &race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_new failed: %d\n", ret);
		return ret;
	}
	ret = snd_pcm_new(race_card, "SndPciRace", 0, 0, 2, &race_pcm_dev);
	if (ret < 0) {
		pr_err(DRV ": snd_pcm_new failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	snd_pcm_set_ops(race_pcm_dev, SNDRV_PCM_STREAM_CAPTURE,
			&race_pcm_ops);
	snd_pcm_lib_preallocate_pages_for_all(race_pcm_dev, SNDRV_DMA_TYPE_CONTINUOUS,
						      NULL, 64 * 1024, 64 * 1024);
	ret = 0;
	pr_info(DRV ": managed_buffer ret=%d dma_type=%d area=%px bytes=%zu\n", ret,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.dev.type,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.area,
		race_pcm_dev->streams[SNDRV_PCM_STREAM_CAPTURE].substream->dma_buffer.bytes);
	ret = snd_card_register(race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_register failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	pr_info(DRV ": card registered\n");
	return 0;
}

static void __exit sndpci_exit(void)
{
	pr_info(DRV ": exit\n");
	if (race_card)
		snd_card_free(race_card);
	if (race_pdev)
		platform_device_unregister(race_pdev);
}

module_init(sndpci_init);
module_exit(sndpci_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("sound/pci lifetime-race reproducer (V3/V4/V5)");

[-- Attachment #3: pcmtest.c --]
[-- Type: text/plain, Size: 3737 bytes --]

#define _GNU_SOURCE
// SPDX-License-Identifier: GPL-2.0
/*
 * pcmtest.c - guest-side exerciser for sndpci_race.ko.
 *
 * Usage:
 *   pcmtest close [iters]  - open/start/close one playback substream (V4)
 *   pcmtest link  [iters]  - link two playback substreams, close B while
 *                            the A timer callback walks the group (V5)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <limits.h>
#include <sched.h>
#include <sound/asound.h>

#define DEV "/dev/snd/pcmC0D0c"

static int set_hw_params(int fd)
{
	struct snd_pcm_hw_params p;
	int i, ret;

	memset(&p, 0, sizeof(p));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_MASK -
			SNDRV_PCM_HW_PARAM_FIRST_MASK; i++)
		memset(&p.masks[i], 0xff, sizeof(p.masks[i]));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
			SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; i++) {
		p.intervals[i].min = 0;
		p.intervals[i].max = UINT_MAX;
		p.intervals[i].integer = 0;
	}
	p.rmask = ~0U;
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_REFINE, &p);
	printf("HW_REFINE ret=%d errno=%d\n", ret, errno);
	if (ret < 0)
		return -1;
	printf("refined: rate=%u ch=%u fmt=%u ps=%u periods=%u bs=%u\n",
	       p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       __builtin_ctz(p.masks[SNDRV_PCM_HW_PARAM_FORMAT].bits[0]),
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIOD_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIODS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_BUFFER_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min);
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
	if (ret < 0) {
		fprintf(stderr, "HW_PARAMS: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int open_stream(void)
{
	int fd = open(DEV, O_RDWR | O_NONBLOCK);

	if (fd < 0)
		fprintf(stderr, "open %s: %s\n", DEV, strerror(errno));
	return fd;
}

static int prepare_stream(int fd)
{
	if (set_hw_params(fd) < 0)
		return -1;
	if (ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0) < 0) {
		fprintf(stderr, "PREPARE: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int start_stream(int fd)
{
	if (ioctl(fd, SNDRV_PCM_IOCTL_START, 0) < 0) {
		fprintf(stderr, "START: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int one_close(void)
{
	int fd = open_stream();

	if (fd < 0)
		return -1;
	if (prepare_stream(fd) < 0 || start_stream(fd) < 0) {
		close(fd);
		return -1;
	}
	usleep(10000);
	close(fd);
	return 0;
}

static int one_link(void)
{
	int a = open_stream();
	int b = open_stream();

	if (a < 0 || b < 0)
		return -1;
	if (prepare_stream(a) < 0 || prepare_stream(b) < 0)
		goto out;
	if (ioctl(a, SNDRV_PCM_IOCTL_LINK, b) < 0) {
		fprintf(stderr, "LINK: %s\n", strerror(errno));
		goto out;
	}
	if (start_stream(a) < 0)
		goto out;
	/* Let the A timer callback start walking the linked group. */
	usleep(1500);
	/* Close B while A's callback may be in the group loop. */
	close(b);
	b = -1;
	usleep(1500);
out:
	if (b >= 0)
		close(b);
	if (a >= 0)
		close(a);
	return 0;
}

int main(int argc, char **argv)
{
	cpu_set_t set;

	CPU_ZERO(&set);
	CPU_SET(1, &set);
	sched_setaffinity(0, sizeof(set), &set);
	int i, n = 200;

	if (argc < 2) {
		fprintf(stderr, "usage: %s close|link [iters]\n", argv[0]);
		return 2;
	}
	if (argc > 2)
		n = atoi(argv[2]);
	printf("pcmtest: mode=%s iters=%d\n", argv[1], n);
	for (i = 0; i < n; i++) {
		if (!strcmp(argv[1], "link")) {
			if (one_link() < 0)
				break;
		} else {
			if (one_close() < 0)
				break;
		}
	}
	printf("pcmtest: done i=%d\n", i);
	return 0;
}

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

* ALSA: usb-audio: us144mkii: PCM close frees runtime while playback/feedback URB completion and capture work still use it (UAF)
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
                     ` (4 preceding siblings ...)
  2026-11-13 12:15   ` ALSA: pcm: link-group walk in timer callback can use a closed substream's freed runtime (UAF) 徐晗翔
@ 2026-11-13 12:28   ` 徐晗翔
  2026-11-13 12:29   ` ALSA: usb-audio: us144mkii: queued stop_pcm_work calls snd_pcm_stop() after PCM close and dereferences NULL runtime 徐晗翔
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:28 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 8212 bytes --]

tascam_playback_close() and tascam_capture_close() only clear the
substream pointer; they do not stop URBs or cancel work.  The ALSA
release path then calls snd_pcm_detach_substream(), which sets
substream->runtime to NULL and frees the snd_pcm_runtime.  A
playback/feedback URB completion, or the capture work, can have loaded
the substream/runtime pointer before close; after detach it dereferences
runtime->rate, runtime->buffer_size, runtime->dma_area or
runtime->status, and may resubmit the next URB.  This is a
use-after-free / oops on an ordinary PCM close while the device remains
connected.  It is not the disconnect path.  Capture close is also
affected because feedback_urb_complete() uses the capture runtime.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source tree (VERSION=7, PATCHLEVEL=3, SUBLEVEL=0),
  sound/usb/usx2y/us144mkii.c, us144mkii_playback.c,
  us144mkii_capture.c and us144mkii_pcm.c.  The driver binds the
  TASCAM US-144MKII (0644:8020) and US-144 (0644:800f).  The relevant
  configuration is CONFIG_SND_USB_US144MKII=m.

  This report is based on source inspection, plus a synthetic ALSA PCM
  model used to validate the close/detach lifetime pattern (see
  "Reproducer (attached POC)" below).  I have not run the real driver on
  TASCAM hardware or under KASAN; no real-hardware result is claimed.

* Mechanism

  1. The close callbacks do not stop I/O:

     static int tascam_playback_close(struct snd_pcm_substream *substream)
     {
             struct tascam_card *tascam = snd_pcm_substream_chip(substream);

             tascam->playback_substream = NULL;
             return 0;
     }

     (us144mkii_playback.c:35-42; tascam_capture_close() is the same at
     us144mkii_capture.c:35-42).  No usb_kill_urb(), no
     cancel_work_sync().

  2. tascam_pcm_trigger() on STOP only clears the active flags and
     schedules stop_work asynchronously:

             atomic_set(&tascam->playback_active, 0);
             atomic_set(&tascam->capture_active, 0);
             ...
             schedule_work(&tascam->stop_work);

     (us144mkii_pcm.c:280-287 and 352-353).  tascam_pcm_hw_free() is a
     no-op (us144mkii_pcm.c:257-260), so ALSA's do_hw_free() cannot stop
     anything either.

  3. ALSA then frees the runtime:

             snd_pcm_drop(substream);                  /* trigger STOP */
             ...
             substream->ops->close(substream);         /* only NULLs ptr */
             ...
             snd_pcm_detach_substream(substream);      /* frees runtime */

     (sound/core/pcm_native.c:2785-2805; snd_pcm_detach_substream() at
     sound/core/pcm.c:978-1005, substream->runtime = NULL at 997 and
     kfree(runtime) at 1001).

  4. Three consumers can already hold the old runtime pointer:

     - playback_urb_complete() reads tascam->playback_substream and
       substream->runtime at us144mkii_playback.c:176-182, then uses
       runtime->rate / runtime->buffer_size under tascam->lock and
       runtime->dma_area after unlocking (184-234), before resubmitting
       at 237-247.

     - feedback_urb_complete() reads the playback and capture
       substreams at us144mkii_playback.c:282-288.  It uses
       capture_rt->period_size at 413-417 and calls
       snd_pcm_period_elapsed(capture_ss) at 430 after unlocking.
       Therefore capture close must also stop the feedback URB; it
       currently stops neither capture nor feedback URBs.

     - tascam_capture_work_handler() reads
       tascam->capture_substream and substream->runtime at
       us144mkii_capture.c:157-169 and writes runtime->dma_area at 230
       under tascam->lock.  It is queued by capture_urb_complete() at
       line 301.

  5. The asynchronous stop_work is not a close-time barrier.
     usb_kill_anchored_urbs() in tascam_stop_work_handler() does wait
     for a completion that is already inside the HCD giveback path
     (__usb_hcd_giveback_urb() suspends the anchor wakeups before
     usb_unanchor_urb(), and resumes them only after urb->complete()),
     but stop_work itself is scheduled asynchronously by trigger(STOP).
     close does not wait for stop_work, so detach can free the runtime
     before stop_work runs.  A per-URB usb_kill_urb() in close() is the
     explicit synchronous barrier; it waits for urb->use_count == 0
     (drivers/usb/core/urb.c:703-720).

  6. A completion that has already passed the active/pointer check can
     resubmit a URB after close's kill unless the final check and
     usb_submit_urb() are serialized with close.  The close path must
     clear the active flag and substream pointer under tascam->lock,
     kill the URBs, cancel the work, and make the completion's final
     submit path take the same lock.

  7. There is a second direction: feedback completion consumes the
     capture runtime, but tascam_capture_close() stops neither capture
     URBs nor feedback URBs.  Closing capture while playback (and
     therefore feedback) remains open can free capture_runtime while
     feedback completion still uses capture_rt.

  8. close() must also be serialized with tascam_disconnect():
     tascam_disconnect() calls tascam_free_urbs() before
     snd_card_free() waits for open PCM files to close
     (us144mkii.c:580-611; free at 38-110).  Without a
     disconnect_lock/disconnected flag, a close that runs after the
     slots were freed can call usb_kill_urb(NULL), or race the free.



* Reproducer

  Attached files:
    tascam_poc.c  - synthetic ALSA PCM kernel module (C1 mode pcm1)
    pcmtest.c     - userland exerciser
    Makefile      - builds tascam_poc.ko against the running kernel

  This POC does not drive the TASCAM device.  It creates a synthetic ALSA
  PCM card whose .close() only drops the substream pointer, starts an
  asynchronous work item that caches the runtime, and lets ALSA detach and
  free the runtime before the work runs.  It therefore reproduces the
  lifetime ordering in point 3/4 above, not the real USB URB completion.
  A KASAN kernel is required to observe the UAF.

  Build and run:
    make
    gcc -static -O2 -o pcmtest pcmtest.c
    sudo modprobe snd-pcm
    sudo insmod ./tascam_poc.ko mode=pcm1
    sudo ./pcmtest 1
    dmesg | tail -50
    sudo rmmod tascam_poc

  Expected result with KASAN:
    BUG: KASAN: slab-use-after-free in race_pcm_work+0xce/0x140
    Allocated by task ... (snd_pcm_open)
    Freed by task ... (snd_pcm_release_substream/snd_pcm_detach_substream)

  Negative control (close cancels the work):
    sudo insmod ./tascam_poc.ko mode=pcm1 safe=1
    sudo ./pcmtest 1
  Expected: no KASAN report.

  The module creates a capture PCM at card index 0; if that index is
  occupied, check /proc/asound/cards and adjust DEV in pcmtest.c.

  For a real-hardware KASAN run, the proposed procedure is:

    arecord -D hw:CARD=US144MKII,DEV=0 -f S32_LE -r 48000 -c 4 /dev/null &
    aplay    -D hw:CARD=US144MKII,DEV=0 -f S32_LE -r 48000 -c 4 /dev/zero
    # start playback, then close it while feedback is active;
    # repeat open/start/close in a loop and watch dmesg for KASAN

  For the capture-close direction, keep the playback process running
  and repeatedly open/start/close the capture stream.  The expected
  KASAN top frames are playback_urb_complete(), feedback_urb_complete()
  or tascam_capture_work_handler().


* Suggested direction 

  In .close(), under tascam->lock, clear the active flag and substream
  pointer, then call usb_kill_urb() for every feedback/playback URB
  (capture close: feedback/capture), and finally cancel_work_sync() for
  stop_pcm_work, stop_work and capture_work as appropriate, before
  returning to snd_pcm_detach_substream().

  Use a disconnect_lock plus a disconnected flag to serialize this
  cleanup with tascam_disconnect()'s tascam_free_urbs(); release the
  lock before snd_card_free().  The completion's final
  active/pointer check and usb_anchor_urb()/usb_submit_urb() should run
  under the same tascam->lock, so a completion that raced with close
  cannot submit a new URB after the kill.

  I am happy to test a proposed fix.


Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: tascam_poc.c --]
[-- Type: text/plain, Size: 6616 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * tascam_poc.c - synthetic ALSA PCM lifetime reproducer for the TASCAM
 * US-144MKII manual-review cases C1 and C2.
 *
 * This module deliberately reproduces the *lifetime patterns* of the driver,
 * not the USB device itself:
 *   pcm1 : ALSA PCM .close() drops the runtime while an asynchronous
 *          "completion" still holds a runtime pointer (C1).
 *   pcm2 : a queued work item calls snd_pcm_stop() after .close()/detach
 *          (C2).
 *
 * The ALSA card is a synthetic platform device; no TASCAM hardware or USB
 * traffic is involved.  Build and run instructions are in README.txt.
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/wait.h>
#include <linux/completion.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV "sndusb_race"
#define COOKIE 0x5a5a1234deadbeefULL

static char *mode = "pcm1";
module_param(mode, charp, 0444);
static bool safe;
module_param(safe, bool, 0444);
MODULE_PARM_DESC(mode, "pcm1 or pcm2");
MODULE_PARM_DESC(safe, "take the safe cleanup path (negative control)");

/* ------------------------------------------------------------------ */
/* V1/V2 synthetic ALSA PCM                                            */
/* ------------------------------------------------------------------ */
struct race_pcm {
	struct snd_pcm_substream *substream;
	struct snd_pcm_runtime *runtime;
	struct work_struct work;
	atomic_t active;
	bool c2;
	u64 cookie;
};

static struct snd_card *race_card;
static struct platform_device *race_pdev;
static struct snd_pcm *race_pcm_dev;

static void race_pcm_work(struct work_struct *work)
{
	struct race_pcm *m = container_of(work, struct race_pcm, work);
	struct snd_pcm_runtime *rt = m->runtime;
	struct snd_pcm_substream *ss = m->substream;

	pr_info(DRV ": work start mode=%s rt=%px ss=%px cookie=%#llx\n",
		m->c2 ? "c2" : "c1", rt, ss, m->cookie);
	if (!m->c2 && !rt)
		return;
	if (m->c2 && !ss)
		return;

	/* Give the closing thread time to run .close() and detach/free. */
	msleep(120);

	if (!m->c2) {
		/*
		 * C1: the completion cached runtime before close; after
		 * snd_pcm_detach_substream() this is freed memory.
		 */
		pr_info(DRV ": c1 use rt=%px dma_area=%px\n", rt,
			READ_ONCE(rt->dma_area));
		barrier();
		(void)READ_ONCE(rt->dma_area);
	} else {
		/*
		 * C2: the work cached the substream, but detach made
		 * ss->runtime NULL; snd_pcm_stop() dereferences it.
		 */
		pr_info(DRV ": c2 call snd_pcm_stop ss=%px runtime=%px\n",
			ss, READ_ONCE(ss->runtime));
		snd_pcm_stop(ss, SNDRV_PCM_STATE_XRUN);
	}
	pr_info(DRV ": work end\n");
}

static int race_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m;

	m = kzalloc(sizeof(*m), GFP_KERNEL);
	if (!m)
		return -ENOMEM;
	m->substream = substream;
	m->runtime = runtime;
	m->cookie = COOKIE;
	m->c2 = !strcmp(mode, "pcm2");
	INIT_WORK(&m->work, race_pcm_work);
	atomic_set(&m->active, 0);
	runtime->private_data = m;
	runtime->hw.info = SNDRV_PCM_INFO_MMAP |
			   SNDRV_PCM_INFO_INTERLEAVED |
			   SNDRV_PCM_INFO_MMAP_VALID;
	runtime->hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
	runtime->hw.rates = SNDRV_PCM_RATE_44100;
	runtime->hw.rate_min = 44100;
	runtime->hw.rate_max = 44100;
	runtime->hw.channels_min = 2;
	runtime->hw.channels_max = 2;
	runtime->hw.buffer_bytes_max = 64 * 1024;
	runtime->hw.period_bytes_min = 1024;
	runtime->hw.period_bytes_max = 4096;
	runtime->hw.periods_min = 2;
	runtime->hw.periods_max = 8;
	pr_info(DRV ": open sub=%px rt=%px mode=%s\n", substream, runtime, mode);
	return 0;
}

static int race_pcm_close(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m = runtime ? runtime->private_data : NULL;

	pr_info(DRV ": close sub=%px rt=%px m=%px safe=%d\n",
		substream, runtime, m, safe);
	if (m && safe)
		cancel_work_sync(&m->work);
	if (m) {
		m->substream = NULL;
		m->runtime = NULL;
	}
	return 0;
}

static int race_pcm_hw_params(struct snd_pcm_substream *substream,
			      struct snd_pcm_hw_params *params)
{
	return 0;
}

static int race_pcm_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int race_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m = runtime ? runtime->private_data : NULL;

	if (!m)
		return 0;
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		atomic_set(&m->active, 1);
		pr_info(DRV ": trigger start, queue work\n");
		schedule_work(&m->work);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		atomic_set(&m->active, 0);
		break;
	}
	return 0;
}

static snd_pcm_uframes_t race_pcm_pointer(struct snd_pcm_substream *substream)
{
	return 0;
}

static const struct snd_pcm_ops race_pcm_ops = {
	.open = race_pcm_open,
	.close = race_pcm_close,
	.hw_params = race_pcm_hw_params,
	.prepare = race_pcm_prepare,
	.trigger = race_pcm_trigger,
	.pointer = race_pcm_pointer,
};

static int sndusb_pcm_init(void)
{
	int ret;

	race_pdev = platform_device_register_simple("sndusb_race", -1, NULL, 0);
	if (IS_ERR(race_pdev))
		return PTR_ERR(race_pdev);
	ret = snd_card_new(&race_pdev->dev, 0, "SndUsbRace", THIS_MODULE, 0,
			   &race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_new failed: %d\n", ret);
		return ret;
	}
	ret = snd_pcm_new(race_card, "SndUsbRace", 0, 0, 1, &race_pcm_dev);
	if (ret < 0) {
		pr_err(DRV ": snd_pcm_new failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	snd_pcm_set_ops(race_pcm_dev, SNDRV_PCM_STREAM_CAPTURE, &race_pcm_ops);
	snd_pcm_lib_preallocate_pages_for_all(race_pcm_dev,
					      SNDRV_DMA_TYPE_CONTINUOUS, NULL,
					      64 * 1024, 64 * 1024);
	ret = snd_card_register(race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_register failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	pr_info(DRV ": card registered mode=%s safe=%d\n", mode, safe);
	return 0;
}

static int __init sndusb_init(void)
{
	pr_info(DRV ": init mode=%s safe=%d\n", mode, safe);
	return sndusb_pcm_init();
}

static void __exit sndusb_exit(void)
{
	pr_info(DRV ": exit\n");
	if (race_card)
		snd_card_free(race_card);
	if (race_pdev)
		platform_device_unregister(race_pdev);
}

module_init(sndusb_init);
module_exit(sndusb_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TASCAM US-144MKII C1/C2 synthetic lifetime reproducer");

[-- Attachment #3: pcmtest.c --]
[-- Type: text/plain, Size: 3737 bytes --]

#define _GNU_SOURCE
// SPDX-License-Identifier: GPL-2.0
/*
 * pcmtest.c - guest-side exerciser for sndpci_race.ko.
 *
 * Usage:
 *   pcmtest close [iters]  - open/start/close one playback substream (V4)
 *   pcmtest link  [iters]  - link two playback substreams, close B while
 *                            the A timer callback walks the group (V5)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <limits.h>
#include <sched.h>
#include <sound/asound.h>

#define DEV "/dev/snd/pcmC0D0c"

static int set_hw_params(int fd)
{
	struct snd_pcm_hw_params p;
	int i, ret;

	memset(&p, 0, sizeof(p));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_MASK -
			SNDRV_PCM_HW_PARAM_FIRST_MASK; i++)
		memset(&p.masks[i], 0xff, sizeof(p.masks[i]));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
			SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; i++) {
		p.intervals[i].min = 0;
		p.intervals[i].max = UINT_MAX;
		p.intervals[i].integer = 0;
	}
	p.rmask = ~0U;
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_REFINE, &p);
	printf("HW_REFINE ret=%d errno=%d\n", ret, errno);
	if (ret < 0)
		return -1;
	printf("refined: rate=%u ch=%u fmt=%u ps=%u periods=%u bs=%u\n",
	       p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       __builtin_ctz(p.masks[SNDRV_PCM_HW_PARAM_FORMAT].bits[0]),
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIOD_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIODS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_BUFFER_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min);
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
	if (ret < 0) {
		fprintf(stderr, "HW_PARAMS: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int open_stream(void)
{
	int fd = open(DEV, O_RDWR | O_NONBLOCK);

	if (fd < 0)
		fprintf(stderr, "open %s: %s\n", DEV, strerror(errno));
	return fd;
}

static int prepare_stream(int fd)
{
	if (set_hw_params(fd) < 0)
		return -1;
	if (ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0) < 0) {
		fprintf(stderr, "PREPARE: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int start_stream(int fd)
{
	if (ioctl(fd, SNDRV_PCM_IOCTL_START, 0) < 0) {
		fprintf(stderr, "START: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int one_close(void)
{
	int fd = open_stream();

	if (fd < 0)
		return -1;
	if (prepare_stream(fd) < 0 || start_stream(fd) < 0) {
		close(fd);
		return -1;
	}
	usleep(10000);
	close(fd);
	return 0;
}

static int one_link(void)
{
	int a = open_stream();
	int b = open_stream();

	if (a < 0 || b < 0)
		return -1;
	if (prepare_stream(a) < 0 || prepare_stream(b) < 0)
		goto out;
	if (ioctl(a, SNDRV_PCM_IOCTL_LINK, b) < 0) {
		fprintf(stderr, "LINK: %s\n", strerror(errno));
		goto out;
	}
	if (start_stream(a) < 0)
		goto out;
	/* Let the A timer callback start walking the linked group. */
	usleep(1500);
	/* Close B while A's callback may be in the group loop. */
	close(b);
	b = -1;
	usleep(1500);
out:
	if (b >= 0)
		close(b);
	if (a >= 0)
		close(a);
	return 0;
}

int main(int argc, char **argv)
{
	cpu_set_t set;

	CPU_ZERO(&set);
	CPU_SET(1, &set);
	sched_setaffinity(0, sizeof(set), &set);
	int i, n = 200;

	if (argc < 2) {
		fprintf(stderr, "usage: %s close|link [iters]\n", argv[0]);
		return 2;
	}
	if (argc > 2)
		n = atoi(argv[2]);
	printf("pcmtest: mode=%s iters=%d\n", argv[1], n);
	for (i = 0; i < n; i++) {
		if (!strcmp(argv[1], "link")) {
			if (one_link() < 0)
				break;
		} else {
			if (one_close() < 0)
				break;
		}
	}
	printf("pcmtest: done i=%d\n", i);
	return 0;
}

[-- Attachment #4: Makefile --]
[-- Type: application/octet-stream, Size: 320 bytes --]

# Build against the running kernel's headers.
# For a KASAN test kernel, set KDIR to that build tree, e.g.:
#   make KDIR=/path/to/linux KASAN=1
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

obj-m := tascam_poc.o

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

* ALSA: usb-audio: us144mkii: queued stop_pcm_work calls snd_pcm_stop() after PCM close and dereferences NULL runtime
       [not found] ` <2026102727-data-sequel-ce41@gregkh>
                     ` (5 preceding siblings ...)
  2026-11-13 12:28   ` ALSA: usb-audio: us144mkii: PCM close frees runtime while playback/feedback URB completion and capture work still use it (UAF) 徐晗翔
@ 2026-11-13 12:29   ` 徐晗翔
  6 siblings, 0 replies; 8+ messages in thread
From: 徐晗翔 @ 2026-11-13 12:29 UTC (permalink / raw)
  To: perex, tiwai; +Cc: linux-sound, linux-kernel

[-- Attachment #1: Type: text/plain, Size: 8406 bytes --]

tascam_stop_pcm_work_handler() is queued by feedback_urb_complete()
after a feedback sync loss, then reads tascam->playback_substream and
tascam->capture_substream without any lock and calls snd_pcm_stop() on
them.  PCM close does not cancel this work.  If close completes first,
snd_pcm_detach_substream() sets substream->runtime to NULL and frees the
snd_pcm_runtime; when the handler reaches snd_pcm_stop(),
snd_pcm_pre_stop() re-reads substream->runtime and dereferences the NULL
pointer.  This is a conditional NULL-pointer dereference / oops on an
ordinary PCM close; a narrower runtime-UAF window is theoretically
possible but was not reproduced, so this report does not claim UAF.
The handler also violates the ALSA convention that snd_pcm_action()
must be called with the PCM stream lock held.

The detailed description:

* Test environment / affected version

  Linux 7.3.0 source tree (VERSION=7, PATCHLEVEL=3, SUBLEVEL=0),
  sound/usb/usx2y/us144mkii_playback.c, us144mkii_pcm.c and
  us144mkii.h.  The driver binds the TASCAM US-144MKII (0644:8020) and
  US-144 (0644:800f).  The relevant configuration is
  CONFIG_SND_USB_US144MKII=m.

  This report is based on source inspection, plus a synthetic ALSA PCM
  model used to validate the close/detach ordering.  I have not run the
  real driver on TASCAM hardware; no real-hardware result is claimed.
  The attached POC is that synthetic-ordering model, not a TASCAM
  hardware reproducer.

* Mechanism

  1. The work is queued on feedback sync loss:

             if (tascam->feedback_synced) {
                     tascam->feedback_consecutive_errors++;
                     if (tascam->feedback_consecutive_errors >
                         FEEDBACK_SYNC_LOSS_THRESHOLD) {
                             dev_err(tascam->card->dev,
                                     "Fatal: Feedback sync lost. Stopping stream.\n");
                             schedule_work(&tascam->stop_pcm_work);
                             tascam->feedback_synced = false;
                             break;
                     }
             }

     (us144mkii_playback.c:347-356).  FEEDBACK_SYNC_LOSS_THRESHOLD is
     41 (us144mkii.h:88), so the condition is reached after 42
     consecutive feedback errors.

  2. The handler reads the pointers without a lock:

             void tascam_stop_pcm_work_handler(struct work_struct *work)
             {
                     struct tascam_card *tascam =
                             container_of(work, struct tascam_card,
                                          stop_pcm_work);

                     if (tascam->playback_substream)
                             snd_pcm_stop(tascam->playback_substream,
                                          SNDRV_PCM_STATE_XRUN);
                     if (tascam->capture_substream)
                             snd_pcm_stop(tascam->capture_substream,
                                          SNDRV_PCM_STATE_XRUN);
             }

     (us144mkii_playback.c:447-456).

  3. PCM close only NULLs the pointers and does not cancel the work:

             tascam->playback_substream = NULL;   /* or capture */
             return 0;

     (us144mkii_playback.c:35-42, us144mkii_capture.c:35-42).  There
     is no cancel_work_sync(&tascam->stop_pcm_work) in either close
     callback.

  4. ALSA then detaches and frees the runtime:

             snd_pcm_drop(substream);
             ...
             substream->ops->close(substream);
             ...
             snd_pcm_detach_substream(substream);

     (sound/core/pcm_native.c:2785-2805; sound/core/pcm.c:978-1005:
     substream->runtime = NULL at 997, kfree(runtime) at 1001).

  5. snd_pcm_stop() is not safe without the stream lock.
     snd_pcm_action() is documented "Note: call with stream lock"
     (sound/core/pcm_native.c:1411-1412); snd_pcm_pre_stop() does:

             struct snd_pcm_runtime *runtime = substream->runtime;
             if (runtime->state == SNDRV_PCM_STATE_OPEN)
                     return -EBADFD;
             runtime->trigger_master = substream;

     (sound/core/pcm_native.c:1549-1558).  After detach runtime is
     NULL, so the first dereference is a NULL-pointer oops.
     snd_pcm_do_stop()/snd_pcm_post_stop() would also touch
     runtime->trigger_master and runtime->sleep/tsleep (1570-1577).

  6. Interleaving: the handler loads tascam->playback_substream (or
     capture_substream) before close clears it, is preempted, close then
     detaches and frees the runtime, and the handler proceeds into
     snd_pcm_stop() and touches the NULL/freed runtime.  The workqueue
     item does not need to overlap with an URB completion at close time;
     it only needs to be queued before close and executed after detach.
     If the handler already passed a runtime non-NULL check before
     detach, a narrower UAF is possible, but that form was not
     reproduced and is not the primary claim.

* Trigger model

  Feedback sync loss is needed: 42 consecutive feedback errors while
  playback or capture is running.  This can be produced with a USB
  traffic fault injector, a short-packet/loss injection on the feedback
  endpoint, or a programmable USB gadget emulating the device.  After
  the driver logs the feedback sync loss and queues the work, close the
  PCM stream before the work runs.  No special privilege beyond access
  to the PCM device is required; TASCAM US-144MKII/US-144 hardware (or
  an emulated equivalent) is required.

  Consequence: NULL-pointer dereference / oops in
  tascam_stop_pcm_work_handler() -> snd_pcm_stop() ->
  snd_pcm_pre_stop(); because the work is already queued, this is
  independent of the URB completion timing in Case 1.

* Reproducer (attached POC)

  Attached files:
    tascam_poc.c  - synthetic ALSA PCM kernel module (C2 mode pcm2)
    pcmtest.c     - userland exerciser
    Makefile      - builds tascam_poc.ko against the running kernel

  This POC does not drive the TASCAM device or inject feedback errors.
  It creates a synthetic ALSA PCM card whose .close() only drops the
  substream pointer, queues work that caches the substream, starts the
  stream, and lets ALSA detach the runtime before the work calls
  snd_pcm_stop().  It therefore reproduces the close/detach ordering in
  point 4 above, not the real feedback-error path.

  Build and run:
    make
    gcc -static -O2 -o pcmtest pcmtest.c
    sudo modprobe snd-pcm
    sudo insmod ./tascam_poc.ko mode=pcm2
    sudo ./pcmtest 1
    dmesg | tail -50
    sudo rmmod tascam_poc

  Expected result:
    sndusb_race: c2 call snd_pcm_stop ss=... runtime=0000000000000000
    BUG: KASAN: null-ptr-deref in snd_pcm_pre_stop+0x24/0x50
    BUG: kernel NULL pointer dereference, address: 0000000000000000
    Oops: 0000 [#1] SMP KASAN NOPTI
    RIP: snd_pcm_pre_stop+0x24/0x50

  Negative control (close cancels the work):
    sudo insmod ./tascam_poc.ko mode=pcm2 safe=1
    sudo ./pcmtest 1
  Expected: no oops/KASAN report.

  The module creates a capture PCM at card index 0; if that index is
  occupied, check /proc/asound/cards and adjust DEV in pcmtest.c.

  A minimal real-hardware KASAN test would:
    - start playback and/or capture;
    - inject 42 consecutive feedback errors (or use a gadget that
      returns short/error packets on the feedback endpoint) until the
      driver logs "Fatal: Feedback sync lost. Stopping stream.";
    - immediately close the PCM stream in a loop;
    - watch dmesg for a KASAN report with
      tascam_stop_pcm_work_handler in the stack.


* Suggested direction

  Make .close() cancel the work before returning to ALSA: clear the
  active flag and pointer under tascam->lock, kill the relevant URBs
  per-URB, then cancel_work_sync(&tascam->stop_pcm_work) (and
  stop_work/capture_work as appropriate).  cancel_work_sync() is the
  lifecycle guarantee; it waits for a running handler.

  In the handler, use snd_pcm_stop_xrun()
  (sound/core/pcm_native.c:1628-1635), which takes the PCM stream lock
  and rechecks substream->runtime.  That fixes the API-convention
  violation, but it is not a substitute for cancelling the work in
  close: snd_pcm_detach_substream() does not take the stream lock, so
  a handler that already passed the runtime check can still race with
  detach.

  I am happy to test a proposed fix.

Please keep me in Cc; I am not subscribed to the lists.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn

[-- Attachment #2: tascam_poc.c --]
[-- Type: text/plain, Size: 6616 bytes --]

// SPDX-License-Identifier: GPL-2.0
/*
 * tascam_poc.c - synthetic ALSA PCM lifetime reproducer for the TASCAM
 * US-144MKII manual-review cases C1 and C2.
 *
 * This module deliberately reproduces the *lifetime patterns* of the driver,
 * not the USB device itself:
 *   pcm1 : ALSA PCM .close() drops the runtime while an asynchronous
 *          "completion" still holds a runtime pointer (C1).
 *   pcm2 : a queued work item calls snd_pcm_stop() after .close()/detach
 *          (C2).
 *
 * The ALSA card is a synthetic platform device; no TASCAM hardware or USB
 * traffic is involved.  Build and run instructions are in README.txt.
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/wait.h>
#include <linux/completion.h>
#include <linux/platform_device.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

#define DRV "sndusb_race"
#define COOKIE 0x5a5a1234deadbeefULL

static char *mode = "pcm1";
module_param(mode, charp, 0444);
static bool safe;
module_param(safe, bool, 0444);
MODULE_PARM_DESC(mode, "pcm1 or pcm2");
MODULE_PARM_DESC(safe, "take the safe cleanup path (negative control)");

/* ------------------------------------------------------------------ */
/* V1/V2 synthetic ALSA PCM                                            */
/* ------------------------------------------------------------------ */
struct race_pcm {
	struct snd_pcm_substream *substream;
	struct snd_pcm_runtime *runtime;
	struct work_struct work;
	atomic_t active;
	bool c2;
	u64 cookie;
};

static struct snd_card *race_card;
static struct platform_device *race_pdev;
static struct snd_pcm *race_pcm_dev;

static void race_pcm_work(struct work_struct *work)
{
	struct race_pcm *m = container_of(work, struct race_pcm, work);
	struct snd_pcm_runtime *rt = m->runtime;
	struct snd_pcm_substream *ss = m->substream;

	pr_info(DRV ": work start mode=%s rt=%px ss=%px cookie=%#llx\n",
		m->c2 ? "c2" : "c1", rt, ss, m->cookie);
	if (!m->c2 && !rt)
		return;
	if (m->c2 && !ss)
		return;

	/* Give the closing thread time to run .close() and detach/free. */
	msleep(120);

	if (!m->c2) {
		/*
		 * C1: the completion cached runtime before close; after
		 * snd_pcm_detach_substream() this is freed memory.
		 */
		pr_info(DRV ": c1 use rt=%px dma_area=%px\n", rt,
			READ_ONCE(rt->dma_area));
		barrier();
		(void)READ_ONCE(rt->dma_area);
	} else {
		/*
		 * C2: the work cached the substream, but detach made
		 * ss->runtime NULL; snd_pcm_stop() dereferences it.
		 */
		pr_info(DRV ": c2 call snd_pcm_stop ss=%px runtime=%px\n",
			ss, READ_ONCE(ss->runtime));
		snd_pcm_stop(ss, SNDRV_PCM_STATE_XRUN);
	}
	pr_info(DRV ": work end\n");
}

static int race_pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m;

	m = kzalloc(sizeof(*m), GFP_KERNEL);
	if (!m)
		return -ENOMEM;
	m->substream = substream;
	m->runtime = runtime;
	m->cookie = COOKIE;
	m->c2 = !strcmp(mode, "pcm2");
	INIT_WORK(&m->work, race_pcm_work);
	atomic_set(&m->active, 0);
	runtime->private_data = m;
	runtime->hw.info = SNDRV_PCM_INFO_MMAP |
			   SNDRV_PCM_INFO_INTERLEAVED |
			   SNDRV_PCM_INFO_MMAP_VALID;
	runtime->hw.formats = SNDRV_PCM_FMTBIT_S16_LE;
	runtime->hw.rates = SNDRV_PCM_RATE_44100;
	runtime->hw.rate_min = 44100;
	runtime->hw.rate_max = 44100;
	runtime->hw.channels_min = 2;
	runtime->hw.channels_max = 2;
	runtime->hw.buffer_bytes_max = 64 * 1024;
	runtime->hw.period_bytes_min = 1024;
	runtime->hw.period_bytes_max = 4096;
	runtime->hw.periods_min = 2;
	runtime->hw.periods_max = 8;
	pr_info(DRV ": open sub=%px rt=%px mode=%s\n", substream, runtime, mode);
	return 0;
}

static int race_pcm_close(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m = runtime ? runtime->private_data : NULL;

	pr_info(DRV ": close sub=%px rt=%px m=%px safe=%d\n",
		substream, runtime, m, safe);
	if (m && safe)
		cancel_work_sync(&m->work);
	if (m) {
		m->substream = NULL;
		m->runtime = NULL;
	}
	return 0;
}

static int race_pcm_hw_params(struct snd_pcm_substream *substream,
			      struct snd_pcm_hw_params *params)
{
	return 0;
}

static int race_pcm_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int race_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct race_pcm *m = runtime ? runtime->private_data : NULL;

	if (!m)
		return 0;
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		atomic_set(&m->active, 1);
		pr_info(DRV ": trigger start, queue work\n");
		schedule_work(&m->work);
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		atomic_set(&m->active, 0);
		break;
	}
	return 0;
}

static snd_pcm_uframes_t race_pcm_pointer(struct snd_pcm_substream *substream)
{
	return 0;
}

static const struct snd_pcm_ops race_pcm_ops = {
	.open = race_pcm_open,
	.close = race_pcm_close,
	.hw_params = race_pcm_hw_params,
	.prepare = race_pcm_prepare,
	.trigger = race_pcm_trigger,
	.pointer = race_pcm_pointer,
};

static int sndusb_pcm_init(void)
{
	int ret;

	race_pdev = platform_device_register_simple("sndusb_race", -1, NULL, 0);
	if (IS_ERR(race_pdev))
		return PTR_ERR(race_pdev);
	ret = snd_card_new(&race_pdev->dev, 0, "SndUsbRace", THIS_MODULE, 0,
			   &race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_new failed: %d\n", ret);
		return ret;
	}
	ret = snd_pcm_new(race_card, "SndUsbRace", 0, 0, 1, &race_pcm_dev);
	if (ret < 0) {
		pr_err(DRV ": snd_pcm_new failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	snd_pcm_set_ops(race_pcm_dev, SNDRV_PCM_STREAM_CAPTURE, &race_pcm_ops);
	snd_pcm_lib_preallocate_pages_for_all(race_pcm_dev,
					      SNDRV_DMA_TYPE_CONTINUOUS, NULL,
					      64 * 1024, 64 * 1024);
	ret = snd_card_register(race_card);
	if (ret < 0) {
		pr_err(DRV ": snd_card_register failed: %d\n", ret);
		snd_card_free(race_card);
		return ret;
	}
	pr_info(DRV ": card registered mode=%s safe=%d\n", mode, safe);
	return 0;
}

static int __init sndusb_init(void)
{
	pr_info(DRV ": init mode=%s safe=%d\n", mode, safe);
	return sndusb_pcm_init();
}

static void __exit sndusb_exit(void)
{
	pr_info(DRV ": exit\n");
	if (race_card)
		snd_card_free(race_card);
	if (race_pdev)
		platform_device_unregister(race_pdev);
}

module_init(sndusb_init);
module_exit(sndusb_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("TASCAM US-144MKII C1/C2 synthetic lifetime reproducer");

[-- Attachment #3: pcmtest.c --]
[-- Type: text/plain, Size: 3737 bytes --]

#define _GNU_SOURCE
// SPDX-License-Identifier: GPL-2.0
/*
 * pcmtest.c - guest-side exerciser for sndpci_race.ko.
 *
 * Usage:
 *   pcmtest close [iters]  - open/start/close one playback substream (V4)
 *   pcmtest link  [iters]  - link two playback substreams, close B while
 *                            the A timer callback walks the group (V5)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <limits.h>
#include <sched.h>
#include <sound/asound.h>

#define DEV "/dev/snd/pcmC0D0c"

static int set_hw_params(int fd)
{
	struct snd_pcm_hw_params p;
	int i, ret;

	memset(&p, 0, sizeof(p));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_MASK -
			SNDRV_PCM_HW_PARAM_FIRST_MASK; i++)
		memset(&p.masks[i], 0xff, sizeof(p.masks[i]));
	for (i = 0; i <= SNDRV_PCM_HW_PARAM_LAST_INTERVAL -
			SNDRV_PCM_HW_PARAM_FIRST_INTERVAL; i++) {
		p.intervals[i].min = 0;
		p.intervals[i].max = UINT_MAX;
		p.intervals[i].integer = 0;
	}
	p.rmask = ~0U;
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_REFINE, &p);
	printf("HW_REFINE ret=%d errno=%d\n", ret, errno);
	if (ret < 0)
		return -1;
	printf("refined: rate=%u ch=%u fmt=%u ps=%u periods=%u bs=%u\n",
	       p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       __builtin_ctz(p.masks[SNDRV_PCM_HW_PARAM_FORMAT].bits[0]),
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIOD_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_PERIODS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,
	       p.intervals[SNDRV_PCM_HW_PARAM_BUFFER_SIZE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min);
	ret = ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p);
	if (ret < 0) {
		fprintf(stderr, "HW_PARAMS: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int open_stream(void)
{
	int fd = open(DEV, O_RDWR | O_NONBLOCK);

	if (fd < 0)
		fprintf(stderr, "open %s: %s\n", DEV, strerror(errno));
	return fd;
}

static int prepare_stream(int fd)
{
	if (set_hw_params(fd) < 0)
		return -1;
	if (ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0) < 0) {
		fprintf(stderr, "PREPARE: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int start_stream(int fd)
{
	if (ioctl(fd, SNDRV_PCM_IOCTL_START, 0) < 0) {
		fprintf(stderr, "START: %s\n", strerror(errno));
		return -1;
	}
	return 0;
}

static int one_close(void)
{
	int fd = open_stream();

	if (fd < 0)
		return -1;
	if (prepare_stream(fd) < 0 || start_stream(fd) < 0) {
		close(fd);
		return -1;
	}
	usleep(10000);
	close(fd);
	return 0;
}

static int one_link(void)
{
	int a = open_stream();
	int b = open_stream();

	if (a < 0 || b < 0)
		return -1;
	if (prepare_stream(a) < 0 || prepare_stream(b) < 0)
		goto out;
	if (ioctl(a, SNDRV_PCM_IOCTL_LINK, b) < 0) {
		fprintf(stderr, "LINK: %s\n", strerror(errno));
		goto out;
	}
	if (start_stream(a) < 0)
		goto out;
	/* Let the A timer callback start walking the linked group. */
	usleep(1500);
	/* Close B while A's callback may be in the group loop. */
	close(b);
	b = -1;
	usleep(1500);
out:
	if (b >= 0)
		close(b);
	if (a >= 0)
		close(a);
	return 0;
}

int main(int argc, char **argv)
{
	cpu_set_t set;

	CPU_ZERO(&set);
	CPU_SET(1, &set);
	sched_setaffinity(0, sizeof(set), &set);
	int i, n = 200;

	if (argc < 2) {
		fprintf(stderr, "usage: %s close|link [iters]\n", argv[0]);
		return 2;
	}
	if (argc > 2)
		n = atoi(argv[2]);
	printf("pcmtest: mode=%s iters=%d\n", argv[1], n);
	for (i = 0; i < n; i++) {
		if (!strcmp(argv[1], "link")) {
			if (one_link() < 0)
				break;
		} else {
			if (one_close() < 0)
				break;
		}
	}
	printf("pcmtest: done i=%d\n", i);
	return 0;
}

[-- Attachment #4: Makefile --]
[-- Type: application/octet-stream, Size: 320 bytes --]

# Build against the running kernel's headers.
# For a KASAN test kernel, set KDIR to that build tree, e.g.:
#   make KDIR=/path/to/linux KASAN=1
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

obj-m := tascam_poc.o

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

* Re: ALSA: ctxfi: free atimer only after the timer IRQ is quiesced
  2026-11-13 11:58   ` ALSA: ctxfi: free atimer only after the timer IRQ is quiesced 徐晗翔
@ 2026-11-13 16:30     ` Takashi Iwai
  0 siblings, 0 replies; 8+ messages in thread
From: Takashi Iwai @ 2026-11-13 16:30 UTC (permalink / raw)
  To: 徐晗翔; +Cc: perex, tiwai, linux-sound, linux-kernel

On Fri, 13 Nov 2026 12:58:51 +0100,
徐晗翔 wrote:
> 
> `ct_timer_free()` frees `struct ct_timer` while the ctxfi timer IRQ handler can
> still be executing, or can start after `snd_card_disconnect()` has already done
> its single `synchronize_irq()`.  A handler that has passed the non-atomic
> `hw->irq_callback` check then calls `ct_timer_interrupt()` with the freed
> `atimer`; `free_irq()` only runs later, inside `ct_atc_destroy()` ->
> `destroy_hw_obj()`.  This is a use-after-free on the timer object and can crash
> or wedge the device during PCI removal/unbind.
> 
> The detailed description:
> 
> * Test environment / affected version
> 
>   Linux 7.3.0 source tree under review.  This report is source-level only: no
>   ctxfi hardware was available and QEMU has no ctxfi device model, so no
>   driver-specific reproducer was run.  The source paths/line numbers below are
>   from 7.3.
> 
> * Mechanism
> 
>   `ct_timer_new()` (`sound/pci/ctxfi/cttimer.c:388-411`) installs the native
>   timer callback by default when `!use_system_timer && hw->set_timer_irq`:
> 
>     hw->irq_callback_data = atimer;
>     hw->irq_callback      = ct_timer_interrupt;
> 
>   The PCI IRQ handlers `ct_20k1_interrupt()` (`cthw20k1.c:1878-1891`) and
>   `ct_20k2_interrupt()` (`cthw20k2.c:2045-2059`) do:
> 
>     if (hw->irq_callback)
>         hw->irq_callback(hw->irq_callback_data, status);
> 
>   `ct_timer_interrupt()` (`cttimer.c:379-386`) then reads
>   `timer->ops->interrupt` and calls it with `timer`.  Neither the check nor
>   the two loads is atomic.
> 
>   `ct_timer_free()` (`cttimer.c:414-421`) does:
> 
>     struct hw *hw = atimer->atc->hw;
>     hw->irq_callback = NULL;
>     if (atimer->ops->free_global)
>         atimer->ops->free_global(atimer);
>     kfree(atimer);
> 
>   `ct_xfitimer_free_global()` only calls `ct_xfitimer_irq_stop()`; that helper
>   stops the hardware only if `atimer->running` is set (`cttimer.c:137-145`).
>   It does not wait for an in-flight IRQ handler.
> 
>   `ct_atc_destroy()` (`ctatc.c:1250-1286`) then calls `ct_timer_free()` at
>   1257-1260, calls `destroy_hw_obj(atc->hw)` at 1274-1275, and only after that
>   `kfree(atc)` at 1283.  `destroy_hw_obj()` -> `hw_card_shutdown()` ->
>   `free_irq()` at `cthw20k1.c:1969` / `cthw20k2.c:2140`.  So `atc` itself is
>   not the freed object in this window; `atimer` is freed before `free_irq()`.
> 
> * Why the existing synchronization does not close the window
> 
>   ctxfi does set `card->sync_irq` at `cthw20k1.c:1937` / `cthw20k2.c:2104`,
>   and `snd_card_disconnect()` (`sound/core/init.c:495-562`) calls
>   `synchronize_irq(card->sync_irq)` once at 540-541.  But that call happens
>   before `ct_timer_free()`, only waits handlers that are running at that
>   instant, and does not disable the IRQ line.  A new handler can enter after
>   the one-time sync and before `free_irq()`.
> 
>   The `hw->irq_callback = NULL` store does not close the race either: a
>   handler that loaded the callback pointer before the store can still call it.
> 
> * Trigger model
> 
>   Open a ctxfi device (native timer path, the default), then remove/unbind the
>   PCI device while an interrupt is pending or arrives during teardown.  The
>   window is between `ct_timer_free()` and `free_irq()`.  Consequence: UAF read
>   of `atimer->ops` and timer fields in IRQ context; crash, stall, or potential
>   control-flow corruption.
> 
> * Suggested direction
> 
>   After clearing `hw->irq_callback` and calling `free_global()`, but before
>   `kfree(atimer)`, do:
> 
>     if (hw->irq >= 0)
>         synchronize_irq(hw->irq);
> 
>   Alternatively, move `free_irq()` earlier so it precedes the timer free.
>   Keep the existing `hw->irq == -1` initialization contract.
> 
> 
> Please keep me in Cc; I am not subscribed to the lists.

Could you rather submit a fix patch (with a proper patch description)
for each your reported issue?  You can put Link: tag to pointing to
the earlier reports.


thanks,

Takashi

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

end of thread, other threads:[~2026-11-13 16:30 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <4c7f9772.192e1.1a17d5c2ed5.Coremail.xuhx@hust.edu.cn>
     [not found] ` <2026102727-data-sequel-ce41@gregkh>
2026-11-13 11:58   ` ALSA: ctxfi: free atimer only after the timer IRQ is quiesced 徐晗翔
2026-11-13 16:30     ` Takashi Iwai
2026-11-13 12:03   ` ALSA: ctxfi: do not free a running system timer when timer_delete_sync_try() returns -1 (UAF) 徐晗翔
2026-11-13 12:04   ` ALSA: asihpi: free IRQ before snd_card_free()/HPI delete on HPI6205 removal (UAF) 徐晗翔
2026-11-13 12:06   ` ALSA: asihpi: quiesce PCM timer and IRQ before freeing dpcm on close (UAF) 徐晗翔
2026-11-13 12:15   ` ALSA: pcm: link-group walk in timer callback can use a closed substream's freed runtime (UAF) 徐晗翔
2026-11-13 12:28   ` ALSA: usb-audio: us144mkii: PCM close frees runtime while playback/feedback URB completion and capture work still use it (UAF) 徐晗翔
2026-11-13 12:29   ` ALSA: usb-audio: us144mkii: queued stop_pcm_work calls snd_pcm_stop() after PCM close and dereferences NULL runtime 徐晗翔

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