* 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* 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
* 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