Files
linux-stable-mirror/sound/core/seq/seq_fifo.c
T
Cen ZhangandTakashi Iwai e546128291 ALSA: seq: avoid stale FIFO cells during resize
snd_seq_fifo_resize() still needs to publish the replacement pool
before it waits for FIFO users. A blocking snd_seq_read() holds
f->use_lock while it sleeps, so concurrent senders must be able to
queue to the new pool and wake that reader instead of failing against a
closing old pool.

However, snd_seq_fifo_event_in() duplicates an event before it takes
f->lock, and snd_seq_read() can dequeue a cell and later call
snd_seq_fifo_cell_putback() if copy_to_user() or
snd_seq_expand_var_event() fails. If resize swaps f->pool and detaches
oldhead in between, either path can relink an old-pool cell after the
snapshot. That stale cell sits outside the drained oldhead list, keeps
oldpool->counter elevated, and can leave snd_seq_pool_delete() waiting
for the retired pool to drain.

Keep the existing swap-before-wait ordering in snd_seq_fifo_resize(),
but reject stale cells before any FIFO relink. Revalidate event-in cells
under f->lock and retry them against the published replacement pool, and
free stale putback cells instead of linking them back into the FIFO.

The buggy scenario involves two paths, with each column showing the
order within that path:

resize path:                    relink path:
1. Allocate newpool.             1. Take f->use_lock.
2. Swap f->pool to newpool and   2. Duplicate or dequeue an old-pool
   detach oldhead.                  cell before oldpool closes.
3. Mark oldpool closing and      3. Reach a later relink point after
   wait for FIFO users.             resize published newpool.
4. Free oldhead and delete       4. Relink the old-pool cell after
   oldpool.                         resize detached oldhead.
                                 5. Drop f->use_lock.

The reproducer reports a resize ioctl blocked in the expected pool
teardown path:

signal: resize iteration=98 target_pool=4 exceeded 250ms
        (elapsed=251ms)
diagnostic: resize_tid=651 wchan=snd_seq_pool_done
diagnostic: resize_tid=651 stack=
  snd_seq_pool_done+0x5b/0x140
  snd_seq_pool_delete+0x7a/0x90
  snd_seq_fifo_resize+0x193/0x1e0
  snd_seq_ioctl_set_client_pool+0x214/0x260
  snd_seq_ioctl+0x119/0x540
  __x64_sys_ioctl+0xd1/0x120
  do_syscall_64+0xbb/0x2f0
  entry_SYSCALL_64_after_hwframe+0x77/0x7f

A second run with larger pools hit the same target path:

signal: resize iteration=32 target_pool=64 exceeded 250ms
        (elapsed=251ms)
diagnostic: resize_tid=663 wchan=snd_seq_pool_done
diagnostic: resize_tid=663 stack=
  snd_seq_pool_done+0x5b/0x140
  snd_seq_pool_delete+0x7a/0x90
  snd_seq_fifo_resize+0x193/0x1e0
  snd_seq_ioctl_set_client_pool+0x214/0x260
  snd_seq_ioctl+0x119/0x540
  __x64_sys_ioctl+0xd1/0x120
  do_syscall_64+0xbb/0x2f0
  entry_SYSCALL_64_after_hwframe+0x77/0x7f

Fixes: 2d7d54002e ("ALSA: seq: Fix race during FIFO resize")

Signed-off-by: Cen Zhang <zzzccc427@gmail.com>
Link: https://patch.msgid.link/20260614004801.3507773-2-zzzccc427@gmail.com
Signed-off-by: Takashi Iwai <tiwai@suse.de>
2026-06-14 10:57:10 +02:00

293 lines
5.8 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* ALSA sequencer FIFO
* Copyright (c) 1998 by Frank van de Pol <fvdpol@coil.demon.nl>
*/
#include <sound/core.h>
#include <linux/slab.h>
#include <linux/sched/signal.h>
#include "seq_fifo.h"
#include "seq_lock.h"
/* FIFO */
/* create new fifo */
struct snd_seq_fifo *snd_seq_fifo_new(int poolsize)
{
struct snd_seq_fifo *f;
f = kzalloc_obj(*f);
if (!f)
return NULL;
f->pool = snd_seq_pool_new(poolsize);
if (f->pool == NULL) {
kfree(f);
return NULL;
}
if (snd_seq_pool_init(f->pool) < 0) {
snd_seq_pool_delete(&f->pool);
kfree(f);
return NULL;
}
spin_lock_init(&f->lock);
snd_use_lock_init(&f->use_lock);
init_waitqueue_head(&f->input_sleep);
atomic_set(&f->overflow, 0);
f->head = NULL;
f->tail = NULL;
f->cells = 0;
return f;
}
void snd_seq_fifo_delete(struct snd_seq_fifo **fifo)
{
struct snd_seq_fifo *f;
if (snd_BUG_ON(!fifo))
return;
f = *fifo;
if (snd_BUG_ON(!f))
return;
*fifo = NULL;
if (f->pool)
snd_seq_pool_mark_closing(f->pool);
snd_seq_fifo_clear(f);
/* wake up clients if any */
if (waitqueue_active(&f->input_sleep))
wake_up(&f->input_sleep);
/* release resources...*/
/*....................*/
if (f->pool) {
snd_seq_pool_done(f->pool);
snd_seq_pool_delete(&f->pool);
}
kfree(f);
}
static struct snd_seq_event_cell *fifo_cell_out(struct snd_seq_fifo *f);
/* clear queue */
void snd_seq_fifo_clear(struct snd_seq_fifo *f)
{
struct snd_seq_event_cell *cell;
/* clear overflow flag */
atomic_set(&f->overflow, 0);
snd_use_lock_sync(&f->use_lock);
guard(spinlock_irq)(&f->lock);
/* drain the fifo */
while ((cell = fifo_cell_out(f)) != NULL) {
snd_seq_cell_free(cell);
}
}
/* enqueue event to fifo */
int snd_seq_fifo_event_in(struct snd_seq_fifo *f,
struct snd_seq_event *event)
{
struct snd_seq_event_cell *cell;
struct snd_seq_pool *pool;
bool linked;
int err;
if (snd_BUG_ON(!f))
return -EINVAL;
guard(snd_seq_fifo)(f);
retry:
pool = READ_ONCE(f->pool);
err = snd_seq_event_dup(pool, event, &cell, 1, NULL, NULL); /* always non-blocking */
if (err < 0) {
if ((err == -ENOMEM) || (err == -EAGAIN))
atomic_inc(&f->overflow);
return err;
}
/* append new cells to fifo */
linked = false;
scoped_guard(spinlock_irqsave, &f->lock) {
if (cell->pool == f->pool) {
if (f->tail)
f->tail->next = cell;
f->tail = cell;
if (!f->head)
f->head = cell;
cell->next = NULL;
f->cells++;
linked = true;
}
}
if (!linked) {
/* Retry against the replacement pool after resize publishes it. */
snd_seq_cell_free(cell);
goto retry;
}
/* wakeup client */
if (waitqueue_active(&f->input_sleep))
wake_up(&f->input_sleep);
return 0; /* success */
}
/* dequeue cell from fifo */
static struct snd_seq_event_cell *fifo_cell_out(struct snd_seq_fifo *f)
{
struct snd_seq_event_cell *cell;
cell = f->head;
if (cell) {
f->head = cell->next;
/* reset tail if this was the last element */
if (f->tail == cell)
f->tail = NULL;
cell->next = NULL;
f->cells--;
}
return cell;
}
/* dequeue cell from fifo and copy on user space */
int snd_seq_fifo_cell_out(struct snd_seq_fifo *f,
struct snd_seq_event_cell **cellp, int nonblock)
{
struct snd_seq_event_cell *cell;
unsigned long flags;
wait_queue_entry_t wait;
if (snd_BUG_ON(!f))
return -EINVAL;
*cellp = NULL;
init_waitqueue_entry(&wait, current);
spin_lock_irqsave(&f->lock, flags);
while ((cell = fifo_cell_out(f)) == NULL) {
if (nonblock) {
/* non-blocking - return immediately */
spin_unlock_irqrestore(&f->lock, flags);
return -EAGAIN;
}
set_current_state(TASK_INTERRUPTIBLE);
add_wait_queue(&f->input_sleep, &wait);
spin_unlock_irqrestore(&f->lock, flags);
schedule();
spin_lock_irqsave(&f->lock, flags);
remove_wait_queue(&f->input_sleep, &wait);
if (signal_pending(current)) {
spin_unlock_irqrestore(&f->lock, flags);
return -ERESTARTSYS;
}
}
spin_unlock_irqrestore(&f->lock, flags);
*cellp = cell;
return 0;
}
void snd_seq_fifo_cell_putback(struct snd_seq_fifo *f,
struct snd_seq_event_cell *cell)
{
bool linked = false;
if (cell) {
scoped_guard(spinlock_irqsave, &f->lock) {
if (cell->pool == f->pool) {
cell->next = f->head;
f->head = cell;
if (!f->tail)
f->tail = cell;
f->cells++;
linked = true;
}
}
if (!linked)
snd_seq_cell_free(cell);
}
}
/* polling; return non-zero if queue is available */
int snd_seq_fifo_poll_wait(struct snd_seq_fifo *f, struct file *file,
poll_table *wait)
{
poll_wait(file, &f->input_sleep, wait);
guard(spinlock_irq)(&f->lock);
return (f->cells > 0);
}
/* change the size of pool; all old events are removed */
int snd_seq_fifo_resize(struct snd_seq_fifo *f, int poolsize)
{
struct snd_seq_pool *newpool, *oldpool;
struct snd_seq_event_cell *cell, *next, *oldhead;
if (snd_BUG_ON(!f || !f->pool))
return -EINVAL;
/* allocate new pool */
newpool = snd_seq_pool_new(poolsize);
if (newpool == NULL)
return -ENOMEM;
if (snd_seq_pool_init(newpool) < 0) {
snd_seq_pool_delete(&newpool);
return -ENOMEM;
}
scoped_guard(spinlock_irq, &f->lock) {
/* remember old pool */
oldpool = f->pool;
oldhead = f->head;
/* exchange pools */
WRITE_ONCE(f->pool, newpool);
f->head = NULL;
f->tail = NULL;
f->cells = 0;
/* NOTE: overflow flag is not cleared */
}
/* close the old pool and wait until all users are gone */
snd_seq_pool_mark_closing(oldpool);
snd_use_lock_sync(&f->use_lock);
/* release cells in old pool */
for (cell = oldhead; cell; cell = next) {
next = cell->next;
snd_seq_cell_free(cell);
}
snd_seq_pool_delete(&oldpool);
return 0;
}
/* get the number of unused cells safely */
int snd_seq_fifo_unused_cells(struct snd_seq_fifo *f)
{
if (!f)
return 0;
guard(snd_seq_fifo)(f);
guard(spinlock_irqsave)(&f->lock);
return snd_seq_unused_cells(f->pool);
}