From: sashiko-bot@kernel.org
To: "Joonho Lee" <mynameisjune111@gmail.com>, linux-sound@vger.kernel.org
Cc: virtualization@lists.linux.dev,
"Michael S. Tsirkin" <mst@redhat.com>,
"Eugenio Perez" <eperezma@redhat.com>
Subject: Re: [PATCH v9 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery
Date: Thu, 29 Oct 2026 09:29:59 +0000 [thread overview]
Message-ID: <sashiko-outbox-189039@kernel.org> (raw)
In-Reply-To: <20261029085032.70689-10-mynameisjune111@gmail.com>
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] ALSA: virtio: race clearing vqueue in virtsnd_reset_vqs() before del_vqs()
- [High] ALSA: virtio: uncanceled elapsed_period work on sync_stop and close paths
- [Medium] ALSA: virtio: incomplete pause failure recovery in virtsnd_pcm_trigger()
- [Medium] ALSA: virtio: race in virtsnd_pcm_sync_stop() across prepare generations
- [Medium] ALSA: virtio: race between virtsnd_pcm_prepare_locked() and staged ack()
--
commit a385aa4275f0e96bd41c0f455c953ca4c07a25b9
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: add staged capture accounting and lifecycle recovery
This commit introduces staged capture with request-owned DMA buffers and
separate produced, observed, and consumed accounting positions for virtio-snd.
It also adds lifecycle serialization and state tracking for control requests,
pause-restart recovery, and virtqueue resets.
> diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
> index df7091b74a521..07d66225ce04a 100644
> --- a/sound/virtio/virtio_card.c
> +++ b/sound/virtio/virtio_card.c
> @@ -348,6 +348,22 @@ static void virtsnd_reset_vqs(struct virtio_snd *snd)
> struct virtio_device *vdev = snd->vdev;
> unsigned int i;
>
> + for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
> + struct virtio_pcm_substream *vss = &snd->substreams[i];
> + struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
> +
> + guard(spinlock_irqsave)(&queue->lock);
> + guard(spinlock)(&vss->lock);
> + vss->reset_epoch++;
> + vss->resetting = true;
> + vss->prepared = false;
> + vss->device_running = false;
> + vss->release_required = false;
> + vss->control_uncertain = false;
> + vss->capture_reset_observing = false;
> + vss->xfer_enabled = false;
> + vss->suspended = true;
> + }
> virtio_reset_device(vdev);
>
> /* Deleting a virtqueue does not unmap its outstanding descriptors. */
> @@ -364,9 +380,20 @@ static void virtsnd_reset_vqs(struct virtio_snd *snd)
> spin_unlock_irqrestore(&queue->lock, flags);
> }
>
> - vdev->config->del_vqs(vdev);
> - for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i)
> + for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) {
> + guard(spinlock_irqsave)(&snd->queues[i].lock);
> snd->queues[i].vqueue = NULL;
> + }
> + vdev->config->del_vqs(vdev);
[Severity: High]
Could clearing snd->queues[i].vqueue before calling vdev->config->del_vqs(vdev)
cause a NULL pointer dereference in virtsnd_pcm_notify_cb()?
On transports whose reset callback does not call synchronize_irq() (such as
virtio_mmio's vm_reset() or virtio_vdpa's virtio_vdpa_reset()), virtqueue IRQ
handlers are not synchronized or freed until vdev->config->del_vqs(vdev) runs.
Unlike virtsnd_ctl_notify_cb() and virtsnd_event_notify_cb(), which use their
vqueue parameter directly, virtsnd_pcm_notify_cb() in
sound/virtio/virtio_pcm_msg.c dereferences queue->vqueue after acquiring
queue->lock:
guard(spinlock_irqsave)(&queue->lock);
do {
virtqueue_disable_cb(queue->vqueue);
while ((msg = virtqueue_get_buf(queue->vqueue, &written_bytes)))
virtsnd_pcm_msg_complete(msg, written_bytes);
} while (!virtqueue_enable_cb(queue->vqueue));
If an in-flight TX or RX interrupt on another CPU has entered
virtsnd_pcm_tx_notify_cb() or virtsnd_pcm_rx_notify_cb() and is spinning on
queue->lock while virtsnd_reset_vqs() sets snd->queues[i].vqueue = NULL, won't
virtsnd_pcm_notify_cb() dereference a NULL queue->vqueue as soon as
queue->lock is released?
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index de11bf162006e..ee07e5bb3c62f 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
[ ... ]
> @@ -320,48 +706,372 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
[ ... ]
> + scoped_guard(spinlock_irqsave, &vss->lock) {
> + if (epoch != vss->reset_epoch || vss->resetting)
> + return -ECANCELED;
> + vss->prepared = false;
> + vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
> + vss->hw_ptr = 0;
> + if (!pause_restart)
> + vss->xfer_xrun = false;
> + vss->suspended = false;
> + vss->msg_count = 0;
> + virtsnd_pcm_msg_reset(vss);
> + memset(&vss->pcm_indirect, 0, sizeof(vss->pcm_indirect));
> + vss->pcm_indirect.sw_buffer_size = vss->buffer_bytes;
> + vss->pcm_indirect.hw_buffer_size = vss->buffer_bytes;
> + if (vss->capture_staging) {
> + vss->capture_boundary = substream->runtime->boundary;
> + rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
> + &free_running);
> + if (rc)
> + return rc;
> + }
> + }
>
> - memset(&vss->pcm_indirect, 0, sizeof(vss->pcm_indirect));
> - vss->pcm_indirect.sw_buffer_size =
> - vss->pcm_indirect.hw_buffer_size =
> - snd_pcm_lib_buffer_bytes(substream);
> + rc = virtsnd_pcm_dev_prepare(vss, epoch);
> + scoped_guard(spinlock_irqsave, &vss->lock) {
> + if (epoch != vss->reset_epoch || vss->resetting)
> + return -ECANCELED;
> + vss->prepared = !rc;
> + if (!rc)
> + vss->release_required = true;
> + }
[Severity: Medium]
Can virtsnd_pcm_prepare_locked() race with a concurrent
virtsnd_pcm_capture_staged_ack() call and latch a false xrun on the newly
prepared substream?
In the ALSA PCM core, snd_pcm_do_prepare() calls substream->ops->prepare()
under runtime->buffer_mutex without holding pcm_stream_lock, and only resets
runtime->control->appl_ptr to 0 afterward in snd_pcm_do_reset().
Here in virtsnd_pcm_prepare_locked(), virtsnd_pcm_msg_reset(vss) zeroes
capture_copied_total, capture_acked_total, capture_pointer_seen_total, and
capture_ready, and memset() zeroes vss->pcm_indirect.appl_ptr while
vss->runtime_active remains true. It then sleeps in virtsnd_pcm_dev_prepare()
and sets vss->prepared = true before returning to snd_pcm_do_prepare().
Meanwhile, a concurrent snd_pcm_sync_ptr() ioctl (without
SNDRV_PCM_SYNC_PTR_HWSYNC or SNDRV_PCM_SYNC_PTR_APPL) only acquires
pcm_stream_lock and invokes:
pcm_lib_apply_appl_ptr()
-> virtsnd_pcm_cp_ack()
-> virtsnd_pcm_capture_staged_ack()
In virtsnd_pcm_capture_staged_ack():
if (!vss->prepared && runtime->state != SNDRV_PCM_STATE_DRAINING &&
!(runtime->state == SNDRV_PCM_STATE_PAUSED &&
virtsnd_pcm_pause_restart_enabled(vss) &&
vss->pause_restart_state == VIRTSND_PCM_PAUSE_RESTART_RELEASED))
return -EBADFD;
Once vss->prepared is set to true (or while virtsnd_pcm_dev_prepare() is
sleeping if runtime->state is SNDRV_PCM_STATE_DRAINING or PAUSED),
virtsnd_pcm_capture_staged_ack() can compare the unreset pre-prepare
runtime->control->appl_ptr against the zeroed vss->pcm_indirect.appl_ptr and
vss->capture_copied_total, setting vss->xfer_xrun = true. Even with the
follow-up commit "ALSA: virtio: synchronize staged capture accounting during
RESET", virtsnd_pcm_reset() returns early when vss->xfer_xrun is true, so
wouldn't the next start fail with -EPIPE?
[ ... ]
> @@ -377,68 +1087,218 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
[ ... ]
> switch (command) {
> case SNDRV_PCM_TRIGGER_START:
> case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
> - queue = virtsnd_pcm_queue(vss);
> -
> - scoped_guard(spinlock_irqsave, &queue->lock) {
> - guard(spinlock)(&vss->lock);
> - if (vss->stopped) {
> - substream->runtime->stop_operating = true;
> - return -EBADFD;
> - }
> - if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
> - rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
> - if (rc) {
> - vss->stopped = true;
> - substream->runtime->stop_operating = true;
> - return rc;
> + scoped_guard(spinlock_irqsave, &vss->lock) {
> + if (vss->device_running && !vss->control_uncertain &&
> + !vss->resetting && !vss->stopped) {
> + vss->xfer_enabled = true;
> + return 0;
> }
> - vss->xfer_enabled = true;
> + }
> + rc = virtsnd_pcm_start_queue(vss, epoch);
> + if (rc) {
> + virtsnd_pcm_start_failed(vss, epoch);
> + return rc;
> }
>
> msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_START,
> - GFP_KERNEL);
> + GFP_KERNEL, epoch);
> if (!msg) {
> - virtsnd_pcm_start_failed(vss);
> + virtsnd_pcm_start_failed(vss, epoch);
> return -ENOMEM;
> }
>
> - rc = virtsnd_ctl_msg_send_sync(snd, msg);
> - if (rc)
> - virtsnd_pcm_start_failed(vss);
> + rc = virtsnd_pcm_send_sync(vss, msg);
> + if (rc && rc != -ECANCELED)
> + virtsnd_pcm_start_failed(vss, epoch);
>
> + if (!rc) {
> + guard(spinlock_irqsave)(&vss->lock);
> + if (vss->resetting || vss->reset_epoch != epoch)
> + return -ECANCELED;
> + vss->device_running = true;
> + }
> + if (!rc && vss->capture_staging)
> + virtsnd_pcm_schedule_period_elapsed(vss);
> return rc;
> case SNDRV_PCM_TRIGGER_SUSPEND:
> - vss->suspended = true;
> + scoped_guard(spinlock_irqsave, &vss->lock)
> + vss->suspended = true;
> fallthrough;
> case SNDRV_PCM_TRIGGER_STOP:
> case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
> scoped_guard(spinlock_irqsave, &vss->lock) {
> + if (!vss->prepared)
> + return 0;
> if (command != SNDRV_PCM_TRIGGER_PAUSE_PUSH)
> vss->stopped = true;
> vss->xfer_enabled = false;
> + if (!vss->device_running)
> + return 0;
> }
>
> + rc = virtsnd_pcm_stop_device(vss, epoch);
> + if (rc && command == SNDRV_PCM_TRIGGER_PAUSE_PUSH) {
> + guard(spinlock_irqsave)(&vss->lock);
> + if (vss->reset_epoch == epoch && !vss->resetting &&
> + !vss->control_uncertain && vss->device_running &&
> + !vss->xfer_xrun)
> + vss->xfer_enabled = true;
> + }
> + return rc;
[Severity: Medium]
What happens to staged capture RX descriptors if virtsnd_pcm_stop_device()
fails during SNDRV_PCM_TRIGGER_PAUSE_PUSH when pause-restart mode is disabled?
Before calling virtsnd_pcm_stop_device(), SNDRV_PCM_TRIGGER_PAUSE_PUSH clears
vss->xfer_enabled = false and drops vss->lock while the device is still
running. Any staged capture RX buffers that complete during this window see
vss->xfer_enabled == false in virtsnd_pcm_capture_complete(), so their audio
is discarded, msg->length is cleared to 0, vss->msg_count is decremented, and
virtsnd_pcm_capture_enqueue() is skipped.
If virtsnd_pcm_stop_device() fails with a definite error (so
vss->control_uncertain is false and vss->device_running remains true), both
the recovery path above and the SNDRV_PCM_TRIGGER_PAUSE_RELEASE fast path
invoked by snd_pcm_undo_pause() set vss->xfer_enabled = true without calling
virtsnd_pcm_capture_send() to refill the drained RX buffers. If all queued RX
descriptors completed while vss->xfer_enabled was false, won't vss->msg_count
remain 0 and stall capture indefinitely?
Also, the SNDRV_PCM_TRIGGER_PAUSE_RELEASE fast path when vss->device_running
is true does not check !vss->xfer_xrun before setting vss->xfer_enabled = true,
and if virtsnd_pcm_stop_device() fails with vss->control_uncertain == true on
a linked substream where snd_pcm_action_group() does not call
snd_pcm_undo_pause() on the failing stream, the substream is left in
SNDRV_PCM_STATE_RUNNING with vss->xfer_enabled == false, vss->stopped == false,
and runtime->stop_operating == false.
[ ... ]
> @@ -455,31 +1315,49 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
> unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
> bool need_release;
> int rc;
> + u64 epoch;
> +
> + if (virtsnd_pcm_pause_restart_enabled(vss))
> + return virtsnd_pcm_pause_restart_sync_stop(substream);
>
> scoped_guard(spinlock_irqsave, &vss->lock) {
> + if (vss->resetting)
> + return -ECANCELED;
> + epoch = vss->reset_epoch;
> vss->xfer_enabled = false;
> need_release = vss->release_required;
> if (need_release)
> vss->stopped = true;
> }
> cancel_work_sync(&vss->elapsed_period);
[Severity: High]
Can vss->elapsed_period remain pending across sync_stop, hw_free, and close,
racing with snd_pcm_detach_substream() freeing substream->runtime?
There appear to be two ways vss->elapsed_period is left uncanceled when a
substream is closed:
1. In virtsnd_reset_vqs(), vss->resetting = true is set in the first loop
before virtio_reset_device() and vdev->config->del_vqs() run, while
cancel_work_sync(&vss->elapsed_period) only runs in the final loop (and if
virtsnd_restore() fails in virtsnd_find_vqs(), vss->resetting stays true).
When vss->resetting is true, virtsnd_pcm_sync_stop_locked() and
virtsnd_pcm_pause_restart_sync_stop() return -ECANCELED before reaching
cancel_work_sync(&vss->elapsed_period) (and virtsnd_pcm_sync_stop() also
returns 0 early when !vss->stopped without canceling work). For non-staged
substreams (!vss->capture_staging), neither virtsnd_pcm_hw_free() nor
virtsnd_pcm_close() calls cancel_work_sync(&vss->elapsed_period):
sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_close() {
if (vss->capture_staging) {
scoped_guard(spinlock_irqsave, &vss->lock) {
vss->runtime_active = false;
vss->xfer_enabled = false;
}
cancel_work_sync(&vss->elapsed_period);
}
}
2. In virtsnd_pcm_pause_restart_sync_stop(), cancel_work_sync() runs near the
beginning of the function, but the subsequent error paths call
virtsnd_pcm_pause_restart_fail(), which re-schedules vss->elapsed_period when
vss->runtime_active is true.
When snd_pcm_release_substream() then calls snd_pcm_detach_substream(), which
clears substream->runtime = NULL and calls kfree(runtime) without holding
pcm_stream_lock, can virtsnd_pcm_period_elapsed() concurrently access the
freed runtime?
[ ... ]
> @@ -637,6 +1675,19 @@ static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
> return 0;
> }
>
> +static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
> +{
> + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
> +
> + guard(mutex)(&vss->lifecycle_lock);
> + scoped_guard(spinlock_irqsave, &vss->lock) {
> + /* A newer prepare may have consumed this stop generation. */
> + if (!vss->stopped)
> + return 0;
> + }
> + return virtsnd_pcm_sync_stop_locked(substream);
> +}
[Severity: Medium]
Is checking !vss->stopped under vss->lifecycle_lock sufficient to detect that
a newer prepare already consumed an earlier stop generation?
In sound/core/pcm_native.c, snd_pcm_drop() drops pcm_stream_lock before
calling snd_pcm_sync_stop() without holding runtime->buffer_mutex. If thread A
in snd_pcm_sync_stop() clears runtime->stop_operating = false and is preempted
before acquiring vss->lifecycle_lock in virtsnd_pcm_sync_stop(), thread B can:
1. Call snd_pcm_prepare(), which acquires vss->lifecycle_lock, consumes thread
A's stop via virtsnd_pcm_sync_stop_locked(), and prepares a new stream
generation.
2. Start the stream via virtsnd_pcm_trigger(SNDRV_PCM_TRIGGER_START).
3. Issue SNDRV_PCM_TRIGGER_PAUSE_PUSH or SNDRV_PCM_TRIGGER_STOP, which run
under pcm_stream_lock without vss->lifecycle_lock and set
vss->stopped = true.
When thread A then acquires vss->lifecycle_lock, vss->stopped is true again,
so thread A proceeds into virtsnd_pcm_sync_stop_locked() against the new
generation.
In pause-restart mode, if thread B is mid-transition inside
virtsnd_pcm_pause_restart_pause() or virtsnd_pcm_pause_restart_stop(), thread
A will see virtsnd_pcm_pause_restart_transitioning(state) and force
VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED; or if thread B has completed
PAUSE_PUSH (reaching VIRTSND_PCM_PAUSE_RESTART_RELEASED with
vss->stopped == true), thread A will clear vss->stopped = false in
virtsnd_pcm_pause_restart_sync_stop(), causing a subsequent PAUSE_RELEASE in
virtsnd_pcm_pause_restart_start() to fail with -EBADFD. In normal mode, if
thread B is inside virtsnd_pcm_trigger(SNDRV_PCM_TRIGGER_STOP), thread A can
issue overlapping STOP and RELEASE control requests concurrently.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029085032.70689-1-mynameisjune111@gmail.com?part=9
next prev parent reply other threads:[~2026-10-29 9:30 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 8:50 [PATCH v9 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
2026-10-29 8:50 ` [PATCH v9 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
2026-10-29 9:14 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 02/14] ALSA: virtio: Recover PCM state after a failed start Joonho Lee
2026-10-29 9:25 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 03/14] ALSA: virtio: Retain queued messages through device reset Joonho Lee
2026-10-29 9:19 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership Joonho Lee
2026-10-29 9:18 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 05/14] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee
2026-10-29 9:02 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 06/14] ALSA: virtio: wait through signals for PCM teardown Joonho Lee
2026-10-29 9:13 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee
2026-10-29 8:55 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 08/14] ALSA: pcm: report partial progress under the stream lock Joonho Lee
2026-10-29 8:56 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery Joonho Lee
2026-10-29 9:29 ` sashiko-bot [this message]
2026-10-29 8:50 ` [PATCH v9 10/14] ALSA: virtio: prioritize staged capture period work Joonho Lee
2026-10-29 9:00 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 11/14] docs: describe virtio-sound capture options Joonho Lee
2026-10-29 8:58 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 12/14] ALSA: pcm: add an optional reset operation under the stream lock Joonho Lee
2026-10-29 9:02 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 13/14] ALSA: virtio: synchronize staged capture accounting during RESET Joonho Lee
2026-10-29 9:09 ` sashiko-bot
2026-10-29 8:50 ` [PATCH v9 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET Joonho Lee
2026-10-29 9:17 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-189039@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=eperezma@redhat.com \
--cc=linux-sound@vger.kernel.org \
--cc=mst@redhat.com \
--cc=mynameisjune111@gmail.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=virtualization@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox