* [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization
@ 2026-10-30 5:26 Joonho Lee
2026-10-30 5:26 ` [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
` (13 more replies)
0 siblings, 14 replies; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc
Hi Takashi,
This revision supersedes v9 and includes its unmerged prerequisites.
Apply patches 1-14 directly to sound.git for-next at
a4311e9447bbb219044db38897f95be112f46a8a. Targeting 7.5 or later still
works for me.
Previous revision:
https://lore.kernel.org/all/20261029085032.70689-1-mynameisjune111@gmail.com/T/
The series adds staged capture accounting for shared users such as
dsnoop, lifecycle recovery, and synchronized PCM RESET accounting. The
capture compatibility options remain opt-in and disabled by default.
Changes since v9
Sashiko raised five findings in patch 9 and four in patch 14. I folded
their fixes into those patches. The other patches retain their code;
patch 13 has refreshed hunk positions. The final postimage matches the
candidate used for the new regression tests.
Patch 9 now checks for a detached virtqueue in the PCM IRQ callback.
Period work is disabled until open succeeds and disabled synchronously at
close, including ordinary capture. hw_free cancels work after teardown
even when cleanup returns an error. These changes cover late producers
after reset, failed restore, and pause-restart error handling.
Playback and capture ACK take each runtime's buffer_mutex with a trylock
while holding the PCM lock. This excludes prepare without waiting in the
reverse lock order. Linked prepare marks buffer_accessing negative only
on the member that initiated the action, so checking that counter alone
left later members exposed to the old appl_ptr during counter
initialization.
sync_stop checks the PCM state under its stream lock after acquiring the
lifecycle mutex. This waits for trigger to finish and preserves a newer
active stream. A definitely rejected native pause also refills RX buffers
that returned while transfer was disabled. Failed recovery latches XRUN
and keeps the teardown obligation.
Patch 14 suppresses ACK refill during the common RESET pointer
observation. Paused RESET observes the latest device pointer before
rebasing indirect positions. A late refill failure retains XRUN across
capture completions, and RESET avoids scheduling new work while the
device is resetting.
A further playback check found the same prepare/ACK window: a forced ACK
consumed the old appl_ptr after the driver counters were reset, and the
first write failed with -EPIPE. The buffer_mutex trylock now protects
playback as well. Limiting the XRUN latch to capture also preserves the
earlier TX completion behavior and playback pause/resume after an event.
Both cases passed with recovery and reopen on the corrected candidate.
Patch layout
1-6: ACK errors, START/RELEASE recovery, and request/DMA lifetime.
7: Core stop-threshold accesses used by capture policy sampling.
8: Partial-progress notification under the PCM stream lock.
9: Staged capture, dsnoop policy, and lifecycle recovery; uses 1-8.
10: Staged capture work priority; uses 9.
11: Capture compatibility option documentation.
12: Optional PCM reset operation under the stream lock.
13: Staged capture RESET accounting; uses 9 and 12.
14: Ordinary capture RESET accounting; follows 12-13.
The IRQ and work-lifetime fixes are present from patch 9, where their new
lifecycle paths appear. The ordinary RESET observation flag and error
latch are introduced with patch 14. The two ALSA core API patches remain
separate from the driver changes that use them.
RESET behavior
The callback retains the common pointer and time handling through
snd_pcm_lib_reset(). Zero permits the core to complete the member's
position and appl_ptr rebase while holding its stream lock. Drivers
without the callback keep their existing ioctl path.
A late refill failure permits that rebase and reports XRUN afterwards.
RESET can return zero before the notification worker publishes the error.
A nonblocking read can initially return -EAGAIN and later -EPIPE; paused
resume is rejected. Prepare or reopen is required for recovery. Linked
RESET and prepare stop at the first error without rollback, so earlier
members may already have completed. These policies are unchanged from v9.
The internal snd_pcm_ops addition requires module rebuilds.
Validation
Seven reported conditions were reproduced against v9 and passed with the
fixes: the NULL-queue crash, prepare/ACK interference, RX starvation
after pause rejection, stale sync_stop, nested RESET refill, stale paused
positions, and loss of the latched XRUN. Prepare/ACK tests also covered a
real linked playback/capture group with capture as a later member.
The two work-lifetime findings share the close barrier. The original UAF
itself was not reproduced. Tests exercised active freeze, RESET, failed
restore, close, late work submission after runtime release, and recovery
for ordinary and staged capture. The uncertain control-result subcase of
the pause finding was checked statically; the forced pause rejection
tests used a definite error.
Thirteen fresh ARM64 QEMU runs passed with KASAN and lockdep after these
playback corrections. For each capture mode, an uninstrumented build
recorded 70 seconds through RW and another 70 seconds through MMAP after
paused RESET, followed by reopen checks. All 14 captured segments,
14,208,000 S16 mono frames at 48 kHz, matched SHA-256 values generated
independently from QEMU's sequence metadata. The comparison accounts for
the intended RESET discard boundary and the backend's epoch change at
restart.
RW and MMAP use separate VM boots to stay within the test backend's
4,096-response limit. Test hooks are separate from the submitted code.
ARM64 W=1 Image/modules and ARM32 LLVM object builds passed. Series
application checks compare every intermediate postimage and the final
source with the validated candidate.
Three fresh intermediate stage tests passed: native and pause-restart
capture at patch 9, and paused MMAP RESET/reopen at patch 13. The three
patch-13 capture segments also matched the backend sequence. Stages 9-14
built without compiler warnings. Separate failed-START tests checked
prepare, direct reconfiguration with the same geometry, and close/reopen
recovery.
Earlier prerequisite and RESET tests remain relevant where their source
is unchanged. The new 70-second results do not measure high-load
behavior. Earlier TCG tests had reader gaps of 165-283 ms against 160 ms
buffers; those comparisons cannot establish performance equivalence or
exclude a regression. Work priority provides no real-time guarantee.
This revision still needs physical VZ/MOTU acceptance, a separate KCSAN
run, and broader format, multichannel, playback-error, and ARM32 runtime
testing. Playback event tests cover completion-policy preservation and
pause/resume, leaving broader playback error validation pending. The
partial-progress helper and linked RESET contract also need maintainer
review.
Coding assistance was used to analyze the reports, prepare the fixes and
test fixtures, and draft these explanations. The patches carry
Assisted-by: LLM.
Thanks, Joonho
base-commit: a4311e9447bbb219044db38897f95be112f46a8a
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:48 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start Joonho Lee
` (12 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
virtsnd_pcm_msg_send() can fail while queueing a transfer. Restore the
failed message's length and notify the device about earlier messages
queued by the same call, so they do not remain stranded.
Propagate that failure through the indirect PCM transfer callback instead
of discarding it. Commit the updated indirect state only after successful
submission and stop further transfers when a send fails. Return an XRUN
error to ALSA so the stream can recover.
Keep the polling feature path unchanged.
Fixes: fe981e67568c ("ALSA: virtio: use ack callback")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -260,6 +260,18 @@
dev_err(&vdev->dev,
"SID %u: failed to send I/O message\n",
vss->sid);
+ msg->length -= n;
+
+ /*
+ * Do not leave earlier messages queued without notifying
+ * the device when a later message could not be added.
+ */
+ if (msg_count != vss->msg_count &&
+ !(vss->features &
+ (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) &&
+ virtqueue_kick_prepare(vqueue))
+ virtqueue_notify(vqueue);
+
return rc;
}
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -466,36 +466,100 @@
vss->hw_ptr);
}
+struct virtsnd_pcm_transfer {
+ struct snd_pcm_indirect indirect;
+ int error;
+};
+
static void virtsnd_pcm_trans_copy(struct snd_pcm_substream *substream,
struct snd_pcm_indirect *rec, size_t bytes)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
-
- virtsnd_pcm_msg_send(vss, rec->sw_data, bytes);
+ struct virtsnd_pcm_transfer *transfer =
+ container_of(rec, struct virtsnd_pcm_transfer, indirect);
+
+ if (transfer->error)
+ return;
+
+ transfer->error = virtsnd_pcm_msg_send(vss, rec->sw_data, bytes);
+}
+
+/* Caller holds vss->lock. */
+static void virtsnd_pcm_transfer_failed(struct virtio_pcm_substream *vss)
+{
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ vss->substream->runtime->stop_operating = true;
}
static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
-
- guard(spinlock_irqsave)(&queue->lock);
- guard(spinlock)(&vss->lock);
-
- return snd_pcm_indirect_playback_transfer(substream, &vss->pcm_indirect,
- virtsnd_pcm_trans_copy);
+ struct virtsnd_pcm_transfer transfer = {};
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ transfer.indirect = vss->pcm_indirect;
+ if (vss->stopped) {
+ rc = -EPIPE;
+ } else {
+ rc = snd_pcm_indirect_playback_transfer(substream,
+ &transfer.indirect,
+ virtsnd_pcm_trans_copy);
+ }
+ if (!rc)
+ rc = transfer.error;
+ if (rc)
+ virtsnd_pcm_transfer_failed(vss);
+ else
+ vss->pcm_indirect = transfer.indirect;
+ }
+ if (rc) {
+ dev_err(&vss->snd->vdev->dev,
+ "SID %u: playback ACK transfer failed: %d\n",
+ vss->sid, rc);
+ /* ALSA treats -EPIPE as an XRUN in ACK and pointer paths. */
+ return -EPIPE;
+ }
+
+ return 0;
}
static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
-
- guard(spinlock_irqsave)(&queue->lock);
- guard(spinlock)(&vss->lock);
-
- return snd_pcm_indirect_capture_transfer(substream, &vss->pcm_indirect,
- virtsnd_pcm_trans_copy);
+ struct virtsnd_pcm_transfer transfer = {};
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ transfer.indirect = vss->pcm_indirect;
+ if (vss->stopped) {
+ rc = -EPIPE;
+ } else {
+ rc = snd_pcm_indirect_capture_transfer(substream,
+ &transfer.indirect,
+ virtsnd_pcm_trans_copy);
+ }
+ if (!rc)
+ rc = transfer.error;
+ if (rc)
+ virtsnd_pcm_transfer_failed(vss);
+ else
+ vss->pcm_indirect = transfer.indirect;
+ }
+ if (rc) {
+ dev_err(&vss->snd->vdev->dev,
+ "SID %u: capture ACK transfer failed: %d\n",
+ vss->sid, rc);
+ /* ALSA treats -EPIPE as an XRUN in ACK and pointer paths. */
+ return -EPIPE;
+ }
+
+ return 0;
}
/* PCM substream operators map. */
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
2026-10-30 5:26 ` [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:50 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset Joonho Lee
` (11 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Capture trigger queues I/O messages before PCM_START. If the START
message cannot be allocated or sent, transfers may remain queued, and
ALSA does not invoke the action undo callback for the failed trigger.
Mark the stream stopped and disable transfers for every failed START
path. Arm runtime->stop_operating so sync_stop releases pending I/O and
prevents prepare, hw_params, or START retries until release succeeds.
The stopped flag is shared with ACK callbacks that can run without the
ALSA stream lock. Protect every stopped access with vss->lock. Read the
pending message count under the same lock in open and hw_params. In
sync_stop, decide whether RELEASE is needed and mark the stream stopped
in one locked transition; leave it stopped when release or draining fails.
Always cancel elapsed-period work in sync_stop, even when no RELEASE
is needed.
Record the RELEASE obligation after successful PREPARE and clear it as
soon as RELEASE succeeds. If the subsequent RX wait fails, retry that
wait without submitting RELEASE again.
Fixes: da76e9f3e43a ("ALSA: virtio: PCM substream operators")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -26,13 +26,13 @@
* @hw: Kernel ALSA substream hardware descriptor.
* @elapsed_period: Kernel work to handle the elapsed period state.
* @lock: Spinlock that protects fields shared by interrupt handlers and
- * substream operators.
+ * substream operators, including @stopped.
* @buffer_bytes: Current buffer size in bytes.
* @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes).
* @xfer_enabled: Data transfer state (0 - off, 1 - on).
* @xfer_xrun: Data underflow/overflow state (0 - no xrun, 1 - xrun).
* @stopped: True if the substream is stopped and must be released on the device
- * side.
+ * side; protected by @lock.
* @suspended: True if the substream is suspended and must be reconfigured on
* the device side at resume.
* @msgs: Allocated I/O messages.
@@ -56,6 +56,8 @@
bool xfer_enabled;
bool xfer_xrun;
bool stopped;
+ /* Successful PREPARE still needs RELEASE; protected by lock. */
+ bool release_required;
bool suspended;
struct virtio_pcm_msg **msgs;
unsigned int nmsgs;
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -96,6 +96,15 @@
static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream);
+static void virtsnd_pcm_start_failed(struct virtio_pcm_substream *vss)
+{
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ }
+ vss->substream->runtime->stop_operating = true;
+}
+
/**
* virtsnd_pcm_open() - Open the PCM substream.
* @substream: Kernel ALSA substream.
@@ -115,7 +124,8 @@
snd_pcm_hw_constraint_integer(substream->runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
- vss->stopped = !!virtsnd_pcm_msg_pending_num(vss);
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->stopped |= !!vss->msg_count;
vss->suspended = false;
/*
@@ -217,11 +227,23 @@
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_device *vdev = vss->snd->vdev;
+ unsigned int msg_count;
+ bool stopped;
int rc;
- if (virtsnd_pcm_msg_pending_num(vss)) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ msg_count = vss->msg_count;
+ stopped = vss->stopped;
+ }
+ if (msg_count) {
dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n",
vss->sid);
+ return -EBADFD;
+ }
+ if (stopped) {
+ dev_err(&vdev->dev, "SID %u: stream release failed\n",
+ vss->sid);
+ substream->runtime->stop_operating = true;
return -EBADFD;
}
@@ -273,6 +295,17 @@
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_device *vdev = vss->snd->vdev;
struct virtio_snd_msg *msg;
+ bool stopped;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ stopped = vss->stopped;
+ if (stopped) {
+ dev_err(&vdev->dev, "SID %u: stream release failed\n",
+ vss->sid);
+ substream->runtime->stop_operating = true;
+ return -EBADFD;
+ }
if (!vss->suspended) {
if (virtsnd_pcm_msg_pending_num(vss)) {
@@ -311,7 +344,12 @@
if (!msg)
return -ENOMEM;
- return virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ if (!rc) {
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->release_required = true;
+ }
+ return rc;
}
/**
@@ -338,31 +376,40 @@
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)
+ if (rc) {
+ vss->stopped = true;
+ substream->runtime->stop_operating = true;
return rc;
+ }
vss->xfer_enabled = true;
}
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_START,
GFP_KERNEL);
if (!msg) {
- guard(spinlock_irqsave)(&vss->lock);
- vss->xfer_enabled = false;
-
+ virtsnd_pcm_start_failed(vss);
return -ENOMEM;
}
- return virtsnd_ctl_msg_send_sync(snd, msg);
+ rc = virtsnd_ctl_msg_send_sync(snd, msg);
+ if (rc)
+ virtsnd_pcm_start_failed(vss);
+
+ return rc;
case SNDRV_PCM_TRIGGER_SUSPEND:
vss->suspended = true;
fallthrough;
case SNDRV_PCM_TRIGGER_STOP:
- vss->stopped = true;
- fallthrough;
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (command != SNDRV_PCM_TRIGGER_PAUSE_PUSH)
+ vss->stopped = true;
vss->xfer_enabled = false;
}
@@ -393,21 +440,34 @@
struct virtio_snd *snd = vss->snd;
struct virtio_snd_msg *msg;
unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ bool need_release;
int rc;
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->xfer_enabled = false;
+ need_release = vss->release_required;
+ if (need_release)
+ vss->stopped = true;
+ }
cancel_work_sync(&vss->elapsed_period);
- if (!vss->stopped)
- return 0;
-
- msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
- GFP_KERNEL);
- if (!msg)
- return -ENOMEM;
-
- rc = virtsnd_ctl_msg_send_sync(snd, msg);
- if (rc)
- return rc;
+ if (need_release) {
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
+ GFP_KERNEL);
+ if (!msg) {
+ substream->runtime->stop_operating = true;
+ return -ENOMEM;
+ }
+
+ rc = virtsnd_ctl_msg_send_sync(snd, msg);
+ if (rc) {
+ substream->runtime->stop_operating = true;
+ return rc;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->release_required = false;
+ }
/*
* The spec states that upon receipt of the RELEASE command "the device
@@ -421,11 +481,13 @@
if (rc <= 0) {
dev_warn(&snd->vdev->dev, "SID %u: failed to flush I/O queue\n",
vss->sid);
+ substream->runtime->stop_operating = true;
return !rc ? -ETIMEDOUT : rc;
}
- vss->stopped = false;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->stopped = false;
return 0;
}
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
2026-10-30 5:26 ` [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
2026-10-30 5:26 ` [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:57 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership Joonho Lee
` (10 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Keep the control queue available through snd_card_free() so close-time
STOP and RELEASE commands can still be submitted. Block new control
requests and mark pending messages canceled before reset, then release
their queue references only after reset and queue deletion.
For PCM queues, detach each remaining virtqueue buffer after the device
has stopped DMA and retire its message without reporting a normal
completion. Detach all queues before deleting them so outstanding DMA
mappings are unmapped and no token remains for a late device completion.
Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -338,6 +338,38 @@
}
/**
+ * virtsnd_reset_vqs() - Stop device access and unmap outstanding buffers.
+ * @snd: VirtIO sound device.
+ *
+ * Context: Any context that permits to sleep.
+ */
+static void virtsnd_reset_vqs(struct virtio_snd *snd)
+{
+ struct virtio_device *vdev = snd->vdev;
+ unsigned int i;
+
+ virtio_reset_device(vdev);
+
+ /* Deleting a virtqueue does not unmap its outstanding descriptors. */
+ for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) {
+ struct virtio_snd_queue *queue = &snd->queues[i];
+ unsigned long flags;
+ void *msg;
+
+ spin_lock_irqsave(&queue->lock, flags);
+ if (queue->vqueue)
+ while ((msg = virtqueue_detach_unused_buf(queue->vqueue)))
+ if (i == VIRTIO_SND_VQ_TX || i == VIRTIO_SND_VQ_RX)
+ virtsnd_pcm_msg_cancel(msg);
+ spin_unlock_irqrestore(&queue->lock, flags);
+ }
+
+ vdev->config->del_vqs(vdev);
+ for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i)
+ snd->queues[i].vqueue = NULL;
+}
+
+/**
* virtsnd_remove() - Remove VirtIO and ALSA devices.
* @vdev: VirtIO parent device.
*
@@ -349,13 +381,14 @@
unsigned int i;
virtsnd_disable_event_vq(snd);
- virtsnd_ctl_msg_cancel_all(snd);
if (snd->card)
snd_card_free(snd->card);
- virtio_reset_device(vdev);
- vdev->config->del_vqs(vdev);
+ virtsnd_ctl_msg_cancel_all(snd);
+
+ virtsnd_reset_vqs(snd);
+ virtsnd_ctl_msg_cleanup(snd);
for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
struct virtio_pcm_substream *vss = &snd->substreams[i];
@@ -383,8 +416,8 @@
virtsnd_disable_event_vq(snd);
virtsnd_ctl_msg_cancel_all(snd);
- virtio_reset_device(vdev);
- vdev->config->del_vqs(vdev);
+ virtsnd_reset_vqs(snd);
+ virtsnd_ctl_msg_cleanup(snd);
for (i = 0; i < snd->nsubstreams; ++i)
cancel_work_sync(&snd->substreams[i].elapsed_period);
@@ -412,6 +445,7 @@
return rc;
virtio_device_ready(vdev);
+ virtsnd_ctl_msg_enable(snd);
virtsnd_enable_event_vq(snd);
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h
--- a/sound/virtio/virtio_card.h
+++ b/sound/virtio/virtio_card.h
@@ -47,6 +47,7 @@
* @queues: Virtqueue wrappers.
* @card: ALSA sound card.
* @ctl_msgs: Pending control request list.
+ * @ctl_msgs_blocked: Control requests are blocked during teardown or suspend.
* @event_msgs: Device events.
* @pcm_list: VirtIO PCM device list.
* @jacks: VirtIO jacks.
@@ -64,6 +65,7 @@
struct virtio_snd_queue queues[VIRTIO_SND_VQ_MAX];
struct snd_card *card;
struct list_head ctl_msgs;
+ bool ctl_msgs_blocked;
struct virtio_snd_event *event_msgs;
struct list_head pcm_list;
struct virtio_jack *jacks;
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -15,6 +15,7 @@
* @list: Pending message list entry.
* @notify: Request completed notification.
* @ref_count: Reference count used to manage a message lifetime.
+ * @cancelled: Request canceled during device teardown.
*/
struct virtio_snd_msg {
struct scatterlist sg_request;
@@ -22,6 +23,7 @@
struct list_head list;
struct completion notify;
refcount_t ref_count;
+ bool cancelled;
};
/**
@@ -100,7 +102,7 @@
INIT_LIST_HEAD(&msg->list);
init_completion(&msg->notify);
- /* This reference is dropped in virtsnd_ctl_msg_complete(). */
+ /* The caller owns this reference; send adds a queue reference. */
refcount_set(&msg->ref_count, 1);
return msg;
@@ -134,6 +136,7 @@
int rc;
virtsnd_ctl_msg_ref(msg);
+ /* This reference stays with the queue until completion or teardown. */
/* Set the default status in case the message was canceled. */
response->code = cpu_to_le32(VIRTIO_SND_S_IO_ERR);
@@ -147,12 +150,17 @@
psgs[nouts + nins++] = in_sgs;
scoped_guard(spinlock_irqsave, &queue->lock) {
- rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg,
- GFP_ATOMIC);
+ if (snd->ctl_msgs_blocked)
+ rc = -ENODEV;
+ else
+ rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins,
+ msg, GFP_ATOMIC);
if (!rc) {
notify = virtqueue_kick_prepare(queue->vqueue);
list_add_tail(&msg->list, &snd->ctl_msgs);
+ if (notify)
+ virtqueue_notify(queue->vqueue);
}
}
@@ -168,9 +176,6 @@
goto on_exit;
}
-
- if (notify)
- virtqueue_notify(queue->vqueue);
if (nowait)
goto on_exit;
@@ -184,6 +189,11 @@
rc = -ETIMEDOUT;
}
+ goto on_exit;
+ }
+
+ if (READ_ONCE(msg->cancelled)) {
+ rc = -ENODEV;
goto on_exit;
}
@@ -227,9 +237,33 @@
* virtsnd_ctl_msg_cancel_all() - Cancel all pending control messages.
* @snd: VirtIO sound device.
*
- * Context: Any context.
+ * Context: Any context. Blocks new senders and marks pending requests canceled.
+ * Existing senders are woken when their request completes or after
+ * the device has been reset.
*/
void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd)
+{
+ struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
+ struct virtio_snd_msg *msg;
+
+ guard(spinlock_irqsave)(&queue->lock);
+ snd->ctl_msgs_blocked = true;
+ list_for_each_entry(msg, &snd->ctl_msgs, list) {
+ WRITE_ONCE(msg->cancelled, true);
+ }
+}
+
+/**
+ * virtsnd_ctl_msg_cleanup() - Release messages left after queue teardown.
+ * @snd: VirtIO sound device.
+ *
+ * This must be called after device reset, buffer detachment, and del_vqs(),
+ * when no queue callback or DMA access can still reach a message. The queue
+ * object and its lock must remain alive until this function returns.
+ *
+ * Context: Any context.
+ */
+void virtsnd_ctl_msg_cleanup(struct virtio_snd *snd)
{
struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
@@ -241,6 +275,20 @@
virtsnd_ctl_msg_complete(msg);
}
+}
+
+/**
+ * virtsnd_ctl_msg_enable() - Allow control requests after queue restoration.
+ * @snd: VirtIO sound device.
+ *
+ * Context: Any context.
+ */
+void virtsnd_ctl_msg_enable(struct virtio_snd *snd)
+{
+ struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
+
+ guard(spinlock_irqsave)(&queue->lock);
+ snd->ctl_msgs_blocked = false;
}
/**
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -68,6 +68,10 @@
void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd);
+void virtsnd_ctl_msg_cleanup(struct virtio_snd *snd);
+
+void virtsnd_ctl_msg_enable(struct virtio_snd *snd);
+
void virtsnd_ctl_msg_complete(struct virtio_snd_msg *msg);
int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id,
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -122,6 +122,8 @@
void virtsnd_pcm_msg_reset(struct virtio_pcm_substream *vss);
+void virtsnd_pcm_msg_cancel(struct virtio_pcm_msg *msg);
+
int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
unsigned long bytes);
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -305,6 +305,35 @@
{
guard(spinlock_irqsave)(&vss->lock);
return vss->msg_count;
+}
+
+/**
+ * virtsnd_pcm_msg_cancel() - Cancel a message detached during device reset.
+ * @msg: Message returned by virtqueue_detach_unused_buf().
+ *
+ * Reset cancellation retires the queue token without treating it as a device
+ * completion. In particular, it does not advance the PCM hardware pointer or
+ * schedule period work.
+ *
+ * Context: Process context with the PCM queue lock held.
+ */
+void virtsnd_pcm_msg_cancel(struct virtio_pcm_msg *msg)
+{
+ struct virtio_pcm_substream *vss = msg->substream;
+ bool wake;
+
+ {
+ guard(spinlock)(&vss->lock);
+ msg->length = 0;
+ vss->xfer_enabled = false;
+ if (WARN_ON_ONCE(!vss->msg_count))
+ return;
+ vss->msg_count--;
+ wake = !vss->msg_count;
+ }
+
+ if (wake)
+ wake_up_all(&vss->msg_empty);
}
/**
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (2 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:56 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee
` (9 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Retain references to the pages backing ALSA's vmalloc buffer until its
queued I/O messages are freed. On NOMMU systems, use driver-owned memory
for DMA and copy playback data before submission and capture data after
completion, so ALSA buffer release cannot invalidate an active request.
Validate the PCM buffer and each period before building scatterlists.
Clamp device-reported capture lengths to the message capacity before
advancing the hardware pointer, preventing an oversized completion from
accounting bytes beyond the buffer actually submitted.
Require whole-byte periods whose total matches the negotiated buffer.
Check offset, length and the final message index before indexing the
message array, including empty transfers and arithmetic wraparound.
Fixes: f40a28679e0b ("ALSA: virtio: handling control and I/O messages for the PCM device")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -13,6 +13,7 @@
struct virtio_pcm;
struct virtio_pcm_msg;
+struct page;
/**
* struct virtio_pcm_substream - VirtIO PCM substream.
@@ -38,6 +39,10 @@
* @msgs: Allocated I/O messages.
* @nmsgs: Number of allocated I/O messages.
* @msg_count: Number of pending I/O messages in the virtqueue.
+ * @dma_pages: Pages backing DMA messages, retained until messages are freed.
+ * @ndma_pages: Number of retained pages.
+ * @owned_dma_area: Driver-owned PCM buffer backing asynchronous messages.
+ * @owned_dma_bytes: Size of @owned_dma_area.
* @msg_empty: Notify when msg_count is zero.
*/
struct virtio_pcm_substream {
@@ -62,6 +67,12 @@
struct virtio_pcm_msg **msgs;
unsigned int nmsgs;
unsigned int msg_count;
+ struct page **dma_pages;
+ unsigned int ndma_pages;
+#ifndef CONFIG_MMU
+ u8 *owned_dma_area;
+ size_t owned_dma_bytes;
+#endif
wait_queue_head_t msg_empty;
};
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -3,6 +3,9 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/limits.h>
+#include <linux/mm.h>
+#include <linux/overflow.h>
#include <sound/pcm_params.h>
#include "virtio_card.h"
@@ -13,6 +16,7 @@
* @xfer: Request header payload.
* @status: Response header payload.
* @length: Data length in bytes.
+ * @data_offset: Offset in the driver-owned PCM buffer on NOMMU systems.
* @sgs: Payload scatter-gather table.
*/
struct virtio_pcm_msg {
@@ -20,6 +24,9 @@
struct virtio_snd_pcm_xfer xfer;
struct virtio_snd_pcm_status status;
size_t length;
+#ifndef CONFIG_MMU
+ size_t data_offset;
+#endif
struct scatterlist sgs[];
};
@@ -37,9 +44,8 @@
};
/**
- * virtsnd_pcm_sg_num() - Count the number of sg-elements required to represent
- * vmalloc'ed buffer.
- * @data: Pointer to vmalloc'ed buffer.
+ * virtsnd_pcm_sg_num() - Count the sg-elements for a page-backed buffer.
+ * @data: Pointer to the page-backed buffer.
* @length: Buffer size.
*
* Context: Any context.
@@ -47,14 +53,18 @@
*/
static int virtsnd_pcm_sg_num(u8 *data, unsigned int length)
{
- phys_addr_t sg_address;
- unsigned int sg_length;
+ phys_addr_t sg_address = 0;
+ unsigned int sg_length = 0;
int num = 0;
while (length) {
struct page *pg = vmalloc_to_page(data);
- phys_addr_t pg_address = page_to_phys(pg);
+ phys_addr_t pg_address;
size_t pg_length;
+
+ if (!pg)
+ return -EINVAL;
+ pg_address = page_to_phys(pg);
pg_length = PAGE_SIZE - offset_in_page(data);
if (pg_length > length)
@@ -75,11 +85,80 @@
return num;
}
-/**
- * virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
+#ifdef CONFIG_MMU
+/**
+ * virtsnd_pcm_dma_pages_put() - Release retained PCM buffer pages.
+ * @vss: VirtIO PCM substream.
+ *
+ * Context: Process context.
+ */
+static void virtsnd_pcm_dma_pages_put(struct virtio_pcm_substream *vss)
+{
+ while (vss->ndma_pages)
+ put_page(vss->dma_pages[--vss->ndma_pages]);
+ kfree(vss->dma_pages);
+ vss->dma_pages = NULL;
+}
+
+/**
+ * virtsnd_pcm_dma_pages_get() - Retain the pages backing the PCM buffer.
+ * @vss: VirtIO PCM substream.
+ * @data: PCM buffer address.
+ * @length: PCM buffer size.
+ *
+ * Keep the backing pages alive while virtqueue descriptors can still refer to
+ * them, even if ALSA releases the vmalloc mapping after a failed drain.
+ *
+ * Context: Process context.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_pcm_dma_pages_get(struct virtio_pcm_substream *vss,
+ u8 *data, size_t length)
+{
+ size_t i;
+ size_t npages;
+ size_t span;
+
+ if (!data || !length || !is_vmalloc_addr(data))
+ return -EINVAL;
+ if (vss->dma_pages || vss->ndma_pages)
+ return -EBUSY;
+
+ if (check_add_overflow(length, (size_t)offset_in_page(data), &span))
+ return -EOVERFLOW;
+
+ npages = span / PAGE_SIZE + !!(span % PAGE_SIZE);
+ if (npages > UINT_MAX)
+ return -EOVERFLOW;
+
+ vss->dma_pages = kcalloc(npages, sizeof(*vss->dma_pages), GFP_KERNEL);
+ if (!vss->dma_pages)
+ return -ENOMEM;
+
+ for (i = 0; i < npages; i++) {
+ struct page *page = vmalloc_to_page(data + i * PAGE_SIZE);
+
+ if (!page)
+ goto err_put_pages;
+
+ get_page(page);
+ vss->dma_pages[vss->ndma_pages++] = page;
+ }
+
+ return 0;
+
+err_put_pages:
+ virtsnd_pcm_dma_pages_put(vss);
+
+ return -EINVAL;
+}
+#endif
+
+/**
+ * virtsnd_pcm_sg_from() - Build an sg-list from a page-backed buffer.
* @sgs: Preallocated sg-list to populate.
* @nsgs: The maximum number of elements in the @sgs.
- * @data: Pointer to vmalloc'ed buffer.
+ * @data: Pointer to the page-backed buffer.
* @length: Buffer size.
*
* Splits the buffer into physically contiguous parts and makes an sg-list of
@@ -132,36 +211,99 @@
int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
unsigned int periods, unsigned int period_bytes)
{
- struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ struct snd_pcm_runtime *runtime;
+ size_t buffer_bytes;
unsigned int i;
+ int rc;
+
+ if (vss->msgs || vss->nmsgs || vss->dma_pages || vss->ndma_pages)
+ return -EBUSY;
+#ifndef CONFIG_MMU
+ if (vss->owned_dma_area || vss->owned_dma_bytes)
+ return -EBUSY;
+#endif
+ if (!periods || !period_bytes)
+ return -EINVAL;
+
+ if (check_mul_overflow((size_t)periods, (size_t)period_bytes,
+ &buffer_bytes))
+ return -EOVERFLOW;
+
+ if (!vss->substream || !vss->substream->runtime)
+ return -EINVAL;
+ runtime = vss->substream->runtime;
+ if (!runtime->dma_area || buffer_bytes > runtime->dma_bytes)
+ return -EINVAL;
+
+#ifdef CONFIG_MMU
+ rc = virtsnd_pcm_dma_pages_get(vss, runtime->dma_area, buffer_bytes);
+ if (rc)
+ return rc;
+#else
+ vss->owned_dma_area = kzalloc(buffer_bytes, GFP_KERNEL);
+ if (!vss->owned_dma_area)
+ return -ENOMEM;
+ vss->owned_dma_bytes = buffer_bytes;
+#endif
vss->msgs = kzalloc_objs(*vss->msgs, periods);
- if (!vss->msgs)
- return -ENOMEM;
+ if (!vss->msgs) {
+ rc = -ENOMEM;
+ goto err_free_pages;
+ }
vss->nmsgs = periods;
for (i = 0; i < periods; ++i) {
- u8 *data = runtime->dma_area + period_bytes * i;
- int sg_num = virtsnd_pcm_sg_num(data, period_bytes);
+ int sg_num;
struct virtio_pcm_msg *msg;
+ u8 *data;
+#ifdef CONFIG_MMU
+ data = runtime->dma_area + (size_t)period_bytes * i;
+#else
+ data = vss->owned_dma_area + (size_t)period_bytes * i;
+#endif
+ sg_num = virtsnd_pcm_sg_num(data, period_bytes);
+ if (sg_num < 0) {
+ rc = sg_num;
+ goto err_free_msgs;
+ }
msg = kzalloc_flex(*msg, sgs, sg_num + 2);
- if (!msg)
- return -ENOMEM;
+ if (!msg) {
+ rc = -ENOMEM;
+ goto err_free_msgs;
+ }
msg->substream = vss;
+ vss->msgs[i] = msg;
sg_init_one(&msg->sgs[PCM_MSG_SG_XFER], &msg->xfer,
sizeof(msg->xfer));
sg_init_one(&msg->sgs[PCM_MSG_SG_STATUS], &msg->status,
sizeof(msg->status));
+#ifndef CONFIG_MMU
+ msg->data_offset = (size_t)period_bytes * i;
+#endif
virtsnd_pcm_sg_from(&msg->sgs[PCM_MSG_SG_DATA], sg_num, data,
period_bytes);
-
- vss->msgs[i] = msg;
}
return 0;
+
+err_free_msgs:
+ virtsnd_pcm_msg_free(vss);
+ return rc;
+
+err_free_pages:
+#ifdef CONFIG_MMU
+ virtsnd_pcm_dma_pages_put(vss);
+#else
+ kfree(vss->owned_dma_area);
+ vss->owned_dma_area = NULL;
+ vss->owned_dma_bytes = 0;
+#endif
+
+ return rc;
}
/**
@@ -177,6 +319,13 @@
for (i = 0; vss->msgs && i < vss->nmsgs; ++i)
kfree(vss->msgs[i]);
kfree(vss->msgs);
+#ifdef CONFIG_MMU
+ virtsnd_pcm_dma_pages_put(vss);
+#else
+ kfree(vss->owned_dma_area);
+ vss->owned_dma_area = NULL;
+ vss->owned_dma_bytes = 0;
+#endif
vss->msgs = NULL;
vss->nmsgs = 0;
@@ -228,8 +377,15 @@
bool notify = false;
int rc;
+ if (!bytes)
+ return 0;
+ if (!period_bytes || !vss->msgs || offset >= vss->buffer_bytes ||
+ bytes > vss->buffer_bytes - offset)
+ return -EINVAL;
start = offset / period_bytes;
end = (offset + bytes - 1) / period_bytes;
+ if (end >= vss->nmsgs)
+ return -EINVAL;
for (i = start; i <= end; i++) {
struct virtio_pcm_msg *msg = vss->msgs[i];
@@ -248,6 +404,33 @@
if (msg->length == period_bytes) {
msg->xfer.stream_id = cpu_to_le32(vss->sid);
memset(&msg->status, 0, sizeof(msg->status));
+
+#ifndef CONFIG_MMU
+ if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK) {
+ struct snd_pcm_runtime *runtime =
+ vss->substream ? vss->substream->runtime : NULL;
+ size_t data_bytes = msg->length;
+
+ if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
+ msg->data_offset > runtime->dma_bytes ||
+ data_bytes > runtime->dma_bytes - msg->data_offset ||
+ msg->data_offset > vss->owned_dma_bytes ||
+ data_bytes > vss->owned_dma_bytes - msg->data_offset) {
+ dev_err_ratelimited(&vdev->dev,
+ "SID %u: invalid playback buffer for I/O message\n",
+ vss->sid);
+ msg->length -= n;
+ if (msg_count != vss->msg_count &&
+ !(vss->features &
+ (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) &&
+ virtqueue_kick_prepare(vqueue))
+ virtqueue_notify(vqueue);
+ return -EINVAL;
+ }
+ memcpy(vss->owned_dma_area + msg->data_offset,
+ runtime->dma_area + msg->data_offset, data_bytes);
+ }
+#endif
if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK)
rc = virtqueue_add_sgs(vqueue, psgs, 2, 1, msg,
@@ -363,15 +546,48 @@
* the hw_ptr value is unconditionally advanced.
*/
guard(spinlock)(&vss->lock);
+#ifndef CONFIG_MMU
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->xfer_enabled && written_bytes > sizeof(msg->status)) {
+ struct snd_pcm_runtime *runtime =
+ vss->substream ? vss->substream->runtime : NULL;
+ size_t reported_bytes = written_bytes - sizeof(msg->status);
+ size_t data_bytes = min_t(size_t, reported_bytes, msg->length);
+
+ if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
+ msg->data_offset > runtime->dma_bytes ||
+ data_bytes > runtime->dma_bytes - msg->data_offset ||
+ msg->data_offset > vss->owned_dma_bytes ||
+ data_bytes > vss->owned_dma_bytes - msg->data_offset)
+ dev_err_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: invalid capture buffer for I/O message\n",
+ vss->sid);
+ else
+ memcpy(runtime->dma_area + msg->data_offset,
+ vss->owned_dma_area + msg->data_offset, data_bytes);
+ }
+#endif
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ written_bytes > sizeof(msg->status) &&
+ written_bytes - sizeof(msg->status) > msg->length)
+ dev_err_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: capture length exceeds I/O message capacity\n",
+ vss->sid);
/*
* If the capture substream returned an incorrect status, then just
- * increase the hw_ptr by the message size.
+ * increase the hw_ptr by the message size. Clamp valid capture lengths to
+ * the actual SG payload capacity before accounting them.
*/
if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
- written_bytes <= sizeof(msg->status))
+ written_bytes <= sizeof(msg->status)) {
vss->hw_ptr += msg->length;
- else
- vss->hw_ptr += written_bytes - sizeof(msg->status);
+ } else {
+ size_t data_bytes = min_t(size_t,
+ written_bytes - sizeof(msg->status),
+ msg->length);
+
+ vss->hw_ptr += data_bytes;
+ }
if (vss->hw_ptr >= vss->buffer_bytes)
vss->hw_ptr -= vss->buffer_bytes;
@@ -387,7 +603,6 @@
runtime->delay =
bytes_to_frames(runtime,
le32_to_cpu(msg->status.latency_bytes));
-
schedule_work(&vss->elapsed_period);
} else if (!vss->msg_count) {
wake_up_all(&vss->msg_empty);
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -25,6 +25,8 @@
* signal or due to a timeout. In this case, the device can still access
* messages even after calling ops->hw_free(). It can also issue an interrupt,
* and the interrupt handler will also try to access message structures.
+ * The messages retain references to the PCM buffer pages until they are
+ * freed, so late DMA does not target pages released by ALSA.
*
* Therefore, freeing of already allocated messages occurs:
*
@@ -228,8 +230,19 @@
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_device *vdev = vss->snd->vdev;
unsigned int msg_count;
+ size_t period_buffer_bytes;
bool stopped;
int rc;
+
+ /* Fixed descriptors require whole-byte periods covering the buffer. */
+ if (check_mul_overflow((size_t)params_periods(hw_params),
+ (size_t)params_period_bytes(hw_params),
+ &period_buffer_bytes) ||
+ period_buffer_bytes != params_buffer_bytes(hw_params) ||
+ (params_period_size(hw_params) *
+ (u64)snd_pcm_format_physical_width(params_format(hw_params)) *
+ params_channels(hw_params)) % 8)
+ return -EINVAL;
scoped_guard(spinlock_irqsave, &vss->lock) {
msg_count = vss->msg_count;
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (3 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:36 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown Joonho Lee
` (8 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Control requests can expose caller-owned payloads to the device after a
synchronous send times out. Keep request and response headers and payloads
in the reference-counted message so caller cleanup cannot invalidate DMA.
Keep the queue reference until device completion or post-reset cleanup,
even when the caller times out or is interrupted. Consume the caller's
reference on every send path, and let response readers retain an extra
reference through copyout. Check payload sizes and allocation arithmetic
before building the scatterlists.
A control value payload is also read by the CPU as a typed structure. DMA
cacheline alignment alone does not guarantee the structure's CPU alignment.
Align regions to the greater of the DMA cacheline and control-value type
alignment, and reserve prefix slack from the buffer base so typed accesses
remain aligned while scatterlist regions stay on cacheline boundaries.
Keep response values in a separately allocated aligned response buffer
so INTEGER64 control accesses are aligned from this patch onward.
Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -3,7 +3,10 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/dma-mapping.h>
+#include <linux/limits.h>
#include <linux/moduleparam.h>
+#include <linux/overflow.h>
#include <linux/virtio_config.h>
#include "virtio_card.h"
@@ -11,7 +14,12 @@
/**
* struct virtio_snd_msg - Control message.
* @sg_request: Scattergather list containing a device request (header).
+ * @sg_request_data: Scattergather list containing request data.
* @sg_response: Scattergather list containing a device response (status).
+ * @sg_response_data: Scattergather list containing response data.
+ * @dma_buffer: Message-owned storage for all DMA-visible SG regions.
+ * @request_data_size: Size of the request data buffer.
+ * @response_data_size: Size of the response data buffer.
* @list: Pending message list entry.
* @notify: Request completed notification.
* @ref_count: Reference count used to manage a message lifetime.
@@ -19,7 +27,12 @@
*/
struct virtio_snd_msg {
struct scatterlist sg_request;
+ struct scatterlist sg_request_data;
struct scatterlist sg_response;
+ struct scatterlist sg_response_data;
+ void *dma_buffer;
+ size_t request_data_size;
+ size_t response_data_size;
struct list_head list;
struct completion notify;
refcount_t ref_count;
@@ -47,8 +60,10 @@
*/
void virtsnd_ctl_msg_unref(struct virtio_snd_msg *msg)
{
- if (refcount_dec_and_test(&msg->ref_count))
+ if (refcount_dec_and_test(&msg->ref_count)) {
+ kfree(msg->dma_buffer);
kfree(msg);
+ }
}
/**
@@ -71,12 +86,73 @@
void *virtsnd_ctl_msg_response(struct virtio_snd_msg *msg)
{
return sg_virt(&msg->sg_response);
+}
+
+/**
+ * virtsnd_ctl_msg_request_data() - Get the request data buffer.
+ * @msg: Control message.
+ *
+ * Context: Any context.
+ */
+void *virtsnd_ctl_msg_request_data(struct virtio_snd_msg *msg)
+{
+ return msg->request_data_size ? sg_virt(&msg->sg_request_data) : NULL;
+}
+
+/**
+ * virtsnd_ctl_msg_response_data() - Get the response data buffer.
+ * @msg: Control message.
+ *
+ * Context: Any context.
+ */
+void *virtsnd_ctl_msg_response_data(struct virtio_snd_msg *msg)
+{
+ return msg->response_data_size ? sg_virt(&msg->sg_response_data) : NULL;
+}
+
+/**
+ * virtsnd_ctl_msg_sg_init() - Place one DMA region on cacheline boundaries.
+ * @sg: Scattergather entry for the region.
+ * @cursor: Current location in the message-owned DMA buffer.
+ * @size: Region length.
+ * @alignment: Alignment for DMA cachelines and typed CPU accesses.
+ *
+ * Context: Any context during message allocation.
+ * Return: 0 on success, -EOVERFLOW if the address cannot be represented.
+ */
+static int virtsnd_ctl_msg_sg_init(struct scatterlist *sg, u8 **cursor,
+ size_t size, size_t alignment)
+{
+ uintptr_t address = (uintptr_t)*cursor;
+ uintptr_t end;
+ size_t padding;
+
+ padding = address % alignment;
+ if (padding &&
+ check_add_overflow(address, (uintptr_t)(alignment - padding),
+ &address))
+ return -EOVERFLOW;
+
+ if (check_add_overflow(address, (uintptr_t)size, &end))
+ return -EOVERFLOW;
+
+ padding = end % alignment;
+ if (padding &&
+ check_add_overflow(end, (uintptr_t)(alignment - padding), &end))
+ return -EOVERFLOW;
+
+ sg_init_one(sg, (void *)address, (unsigned int)size);
+ *cursor = (u8 *)end;
+
+ return 0;
}
/**
* virtsnd_ctl_msg_alloc() - Allocate and initialize a control message.
* @request_size: Size of request header.
* @response_size: Size of response header.
+ * @request_data_size: Size of request data buffer.
+ * @response_data_size: Size of response data buffer.
* @gfp: Kernel flags for memory allocation.
*
* The message will be automatically freed when the ref_count value is 0.
@@ -85,20 +161,85 @@
* Return: Allocated message on success, NULL on failure.
*/
struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size,
- size_t response_size, gfp_t gfp)
+ size_t response_size,
+ size_t request_data_size,
+ size_t response_data_size,
+ gfp_t gfp)
{
struct virtio_snd_msg *msg;
-
- if (!request_size || !response_size)
+ struct scatterlist *sgs[4];
+ size_t sizes[4] = {
+ request_size,
+ response_size,
+ request_data_size,
+ response_data_size,
+ };
+ size_t alignment;
+ size_t dma_buffer_size;
+ size_t aligned_size;
+ size_t padding;
+ u8 *data;
+ unsigned int i;
+ int cache_alignment;
+ int rc;
+
+ if (!request_size || !response_size || request_size > UINT_MAX ||
+ response_size > UINT_MAX || request_data_size > UINT_MAX ||
+ response_data_size > UINT_MAX)
return NULL;
-
- msg = kzalloc(sizeof(*msg) + request_size + response_size, gfp);
+ cache_alignment = dma_get_cache_alignment();
+ if (cache_alignment <= 0)
+ return NULL;
+ /* DMA coherence does not remove the CPU's type alignment requirement. */
+ alignment = max_t(size_t, cache_alignment,
+ __alignof__(struct virtio_snd_ctl_value));
+
+ /* Reserve prefix alignment slack before the first DMA region. */
+ dma_buffer_size = alignment - 1;
+ for (i = 0; i < ARRAY_SIZE(sizes); ++i) {
+ if (!sizes[i])
+ continue;
+
+ padding = sizes[i] % alignment;
+ aligned_size = sizes[i];
+ if (padding &&
+ check_add_overflow(aligned_size, alignment - padding,
+ &aligned_size))
+ return NULL;
+
+ if (check_add_overflow(dma_buffer_size, aligned_size,
+ &dma_buffer_size))
+ return NULL;
+ }
+
+ msg = kzalloc(sizeof(*msg), gfp);
if (!msg)
return NULL;
- sg_init_one(&msg->sg_request, (u8 *)msg + sizeof(*msg), request_size);
- sg_init_one(&msg->sg_response, (u8 *)msg + sizeof(*msg) + request_size,
- response_size);
+ msg->dma_buffer = kzalloc(dma_buffer_size, gfp);
+ if (!msg->dma_buffer) {
+ kfree(msg);
+ return NULL;
+ }
+ data = msg->dma_buffer;
+ sgs[0] = &msg->sg_request;
+ sgs[1] = &msg->sg_response;
+ sgs[2] = &msg->sg_request_data;
+ sgs[3] = &msg->sg_response_data;
+ for (i = 0; i < ARRAY_SIZE(sizes); ++i) {
+ if (!sizes[i])
+ continue;
+
+ rc = virtsnd_ctl_msg_sg_init(sgs[i], &data, sizes[i], alignment);
+ if (rc) {
+ kfree(msg->dma_buffer);
+ kfree(msg);
+ return NULL;
+ }
+ }
+
+ msg->request_data_size = request_data_size;
+ msg->response_data_size = response_data_size;
INIT_LIST_HEAD(&msg->list);
init_completion(&msg->notify);
@@ -112,17 +253,15 @@
* virtsnd_ctl_msg_send() - Send a control message.
* @snd: VirtIO sound device.
* @msg: Control message.
- * @out_sgs: Additional sg-list to attach to the request header (may be NULL).
- * @in_sgs: Additional sg-list to attach to the response header (may be NULL).
* @nowait: Flag indicating whether to wait for completion.
*
* Context: Any context. Takes and releases the control queue spinlock.
* May sleep if @nowait is false.
+ * Consumes the caller's reference on every return path.
* Return: 0 on success, -errno on failure.
*/
int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
- struct scatterlist *out_sgs,
- struct scatterlist *in_sgs, bool nowait)
+ bool nowait)
{
struct virtio_device *vdev = snd->vdev;
struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
@@ -133,6 +272,7 @@
unsigned int nins = 0;
struct scatterlist *psgs[4];
bool notify = false;
+ unsigned long flags;
int rc;
virtsnd_ctl_msg_ref(msg);
@@ -142,27 +282,27 @@
response->code = cpu_to_le32(VIRTIO_SND_S_IO_ERR);
psgs[nouts++] = &msg->sg_request;
- if (out_sgs)
- psgs[nouts++] = out_sgs;
+ if (msg->request_data_size)
+ psgs[nouts++] = &msg->sg_request_data;
psgs[nouts + nins++] = &msg->sg_response;
- if (in_sgs)
- psgs[nouts + nins++] = in_sgs;
-
- scoped_guard(spinlock_irqsave, &queue->lock) {
- if (snd->ctl_msgs_blocked)
- rc = -ENODEV;
- else
- rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins,
- msg, GFP_ATOMIC);
- if (!rc) {
- notify = virtqueue_kick_prepare(queue->vqueue);
-
- list_add_tail(&msg->list, &snd->ctl_msgs);
- if (notify)
- virtqueue_notify(queue->vqueue);
- }
- }
+ if (msg->response_data_size)
+ psgs[nouts + nins++] = &msg->sg_response_data;
+
+ spin_lock_irqsave(&queue->lock, flags);
+ if (snd->ctl_msgs_blocked)
+ rc = -ENODEV;
+ else
+ rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg,
+ GFP_ATOMIC);
+ if (!rc) {
+ notify = virtqueue_kick_prepare(queue->vqueue);
+
+ list_add_tail(&msg->list, &snd->ctl_msgs);
+ if (notify)
+ virtqueue_notify(queue->vqueue);
+ }
+ spin_unlock_irqrestore(&queue->lock, flags);
if (rc) {
dev_err(&vdev->dev, "failed to send control message (0x%08x)\n",
@@ -173,46 +313,36 @@
* called, it is necessary to decrement the reference count.
*/
virtsnd_ctl_msg_unref(msg);
-
- goto on_exit;
- }
-
- if (nowait)
- goto on_exit;
-
- rc = wait_for_completion_interruptible_timeout(&msg->notify, js);
- if (rc <= 0) {
+ } else if (!nowait) {
+ rc = wait_for_completion_interruptible_timeout(&msg->notify, js);
if (!rc) {
dev_err(&vdev->dev,
"control message (0x%08x) timeout\n",
le32_to_cpu(request->code));
rc = -ETIMEDOUT;
+ } else if (rc > 0) {
+ if (READ_ONCE(msg->cancelled)) {
+ rc = -ENODEV;
+ } else {
+ switch (le32_to_cpu(response->code)) {
+ case VIRTIO_SND_S_OK:
+ rc = 0;
+ break;
+ case VIRTIO_SND_S_NOT_SUPP:
+ rc = -EOPNOTSUPP;
+ break;
+ case VIRTIO_SND_S_IO_ERR:
+ rc = -EIO;
+ break;
+ default:
+ rc = -EINVAL;
+ break;
+ }
+ }
}
-
- goto on_exit;
- }
-
- if (READ_ONCE(msg->cancelled)) {
- rc = -ENODEV;
- goto on_exit;
- }
-
- switch (le32_to_cpu(response->code)) {
- case VIRTIO_SND_S_OK:
- rc = 0;
- break;
- case VIRTIO_SND_S_NOT_SUPP:
- rc = -EOPNOTSUPP;
- break;
- case VIRTIO_SND_S_IO_ERR:
- rc = -EIO;
- break;
- default:
- rc = -EINVAL;
- break;
- }
-
-on_exit:
+ }
+
+ /* Drop the reference consumed by this send attempt. */
virtsnd_ctl_msg_unref(msg);
return rc;
@@ -308,10 +438,23 @@
{
struct virtio_snd_msg *msg;
struct virtio_snd_query_info *query;
- struct scatterlist sg;
+ size_t info_size;
+ int rc;
+
+ if (count < 0)
+ return -EINVAL;
+
+ if (size > UINT_MAX ||
+ check_mul_overflow((size_t)count, size, &info_size) ||
+ info_size > UINT_MAX)
+ return -EOVERFLOW;
+
+ if (info_size && !info)
+ return -EINVAL;
msg = virtsnd_ctl_msg_alloc(sizeof(*query),
- sizeof(struct virtio_snd_hdr), GFP_KERNEL);
+ sizeof(struct virtio_snd_hdr), 0, info_size,
+ GFP_KERNEL);
if (!msg)
return -ENOMEM;
@@ -321,9 +464,13 @@
query->count = cpu_to_le32(count);
query->size = cpu_to_le32(size);
- sg_init_one(&sg, info, count * size);
-
- return virtsnd_ctl_msg_send(snd, msg, NULL, &sg, false);
+ virtsnd_ctl_msg_ref(msg);
+ rc = virtsnd_ctl_msg_send(snd, msg, false);
+ if (!rc && info_size)
+ memcpy(info, virtsnd_ctl_msg_response_data(msg), info_size);
+ virtsnd_ctl_msg_unref(msg);
+
+ return rc;
}
/**
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -21,20 +21,28 @@
void *virtsnd_ctl_msg_response(struct virtio_snd_msg *msg);
struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size,
- size_t response_size, gfp_t gfp);
+ size_t response_size,
+ size_t request_data_size,
+ size_t response_data_size,
+ gfp_t gfp);
+
+void *virtsnd_ctl_msg_request_data(struct virtio_snd_msg *msg);
+
+void *virtsnd_ctl_msg_response_data(struct virtio_snd_msg *msg);
int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
- struct scatterlist *out_sgs,
- struct scatterlist *in_sgs, bool nowait);
+ bool nowait);
/**
* virtsnd_ctl_msg_send_sync() - Simplified sending of synchronous message.
* @snd: VirtIO sound device.
* @msg: Control message.
*
- * After returning from this function, the message will be deleted. If message
- * content is still needed, the caller must additionally to
- * virtsnd_ctl_msg_ref/unref() it.
+ * This function consumes the caller's message reference on return. A message
+ * accepted by the control queue keeps its queue reference until completion or
+ * post-reset cleanup, including when this function returns on timeout or
+ * interruption. Callers that read the response after success must retain an
+ * additional reference through that access.
*
* The msg_timeout_ms module parameter defines the message completion timeout.
* If the message is not completed within this time, the function will return an
@@ -49,7 +57,7 @@
static inline int virtsnd_ctl_msg_send_sync(struct virtio_snd *snd,
struct virtio_snd_msg *msg)
{
- return virtsnd_ctl_msg_send(snd, msg, NULL, NULL, false);
+ return virtsnd_ctl_msg_send(snd, msg, false);
}
/**
@@ -63,7 +71,7 @@
static inline int virtsnd_ctl_msg_send_async(struct virtio_snd *snd,
struct virtio_snd_msg *msg)
{
- return virtsnd_ctl_msg_send(snd, msg, NULL, NULL, true);
+ return virtsnd_ctl_msg_send(snd, msg, true);
}
void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd);
diff --git a/sound/virtio/virtio_kctl.c b/sound/virtio/virtio_kctl.c
--- a/sound/virtio/virtio_kctl.c
+++ b/sound/virtio/virtio_kctl.c
@@ -3,8 +3,10 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2022 OpenSynergy GmbH
*/
+#include <linux/limits.h>
+#include <linux/overflow.h>
+#include <linux/virtio_config.h>
#include <sound/control.h>
-#include <linux/virtio_config.h>
#include "virtio_card.h"
@@ -157,14 +159,16 @@
struct virtio_snd_ctl_hdr *hdr;
struct virtio_snd_ctl_value *kvalue;
size_t request_size = sizeof(*hdr);
- size_t response_size = sizeof(struct virtio_snd_hdr) + sizeof(*kvalue);
+ size_t response_size = sizeof(struct virtio_snd_hdr);
unsigned int i;
int rc;
- msg = virtsnd_ctl_msg_alloc(request_size, response_size, GFP_KERNEL);
+ msg = virtsnd_ctl_msg_alloc(request_size, response_size, 0,
+ sizeof(*kvalue), GFP_KERNEL);
if (!msg)
return -ENOMEM;
+ /* Response lifetime through ALSA value copyout. */
virtsnd_ctl_msg_ref(msg);
hdr = virtsnd_ctl_msg_request(msg);
@@ -175,8 +179,7 @@
if (rc)
goto on_failure;
- kvalue = (void *)((u8 *)virtsnd_ctl_msg_response(msg) +
- sizeof(struct virtio_snd_hdr));
+ kvalue = virtsnd_ctl_msg_response_data(msg);
switch (type) {
case VIRTIO_SND_CTL_TYPE_BOOLEAN:
@@ -233,7 +236,8 @@
size_t response_size = sizeof(struct virtio_snd_hdr);
unsigned int i;
- msg = virtsnd_ctl_msg_alloc(request_size, response_size, GFP_KERNEL);
+ msg = virtsnd_ctl_msg_alloc(request_size, response_size, 0, 0,
+ GFP_KERNEL);
if (!msg)
return -ENOMEM;
@@ -288,22 +292,27 @@
struct virtio_snd *snd = snd_kcontrol_chip(kcontrol);
struct virtio_snd_msg *msg;
struct virtio_snd_ctl_hdr *hdr;
- unsigned int *tlv;
- struct scatterlist sg;
+ size_t request_data_size = 0;
+ size_t response_data_size = 0;
int rc;
+ switch (op_flag) {
+ case SNDRV_CTL_TLV_OP_READ:
+ response_data_size = size;
+ break;
+ case SNDRV_CTL_TLV_OP_WRITE:
+ case SNDRV_CTL_TLV_OP_CMD:
+ request_data_size = size;
+ break;
+ default:
+ return -EINVAL;
+ }
+
msg = virtsnd_ctl_msg_alloc(sizeof(*hdr), sizeof(struct virtio_snd_hdr),
+ request_data_size, response_data_size,
GFP_KERNEL);
if (!msg)
return -ENOMEM;
-
- tlv = kzalloc(size, GFP_KERNEL);
- if (!tlv) {
- rc = -ENOMEM;
- goto on_msg_unref;
- }
-
- sg_init_one(&sg, tlv, size);
hdr = virtsnd_ctl_msg_request(msg);
hdr->control_id = cpu_to_le32(kcontrol->private_value);
@@ -312,13 +321,16 @@
case SNDRV_CTL_TLV_OP_READ:
hdr->hdr.code = cpu_to_le32(VIRTIO_SND_R_CTL_TLV_READ);
- rc = virtsnd_ctl_msg_send(snd, msg, NULL, &sg, false);
- if (!rc) {
- if (copy_to_user(utlv, tlv, size))
- rc = -EFAULT;
- }
-
- break;
+ /* Response lifetime through user copyout. */
+ virtsnd_ctl_msg_ref(msg);
+ rc = virtsnd_ctl_msg_send(snd, msg, false);
+ if (!rc && size &&
+ copy_to_user(utlv, virtsnd_ctl_msg_response_data(msg),
+ size))
+ rc = -EFAULT;
+ virtsnd_ctl_msg_unref(msg);
+
+ return rc;
case SNDRV_CTL_TLV_OP_WRITE:
case SNDRV_CTL_TLV_OP_CMD:
if (op_flag == SNDRV_CTL_TLV_OP_WRITE)
@@ -327,28 +339,16 @@
hdr->hdr.code =
cpu_to_le32(VIRTIO_SND_R_CTL_TLV_COMMAND);
- if (copy_from_user(tlv, utlv, size)) {
- rc = -EFAULT;
- goto on_msg_unref;
- } else {
- rc = virtsnd_ctl_msg_send(snd, msg, &sg, NULL, false);
+ if (size && copy_from_user(virtsnd_ctl_msg_request_data(msg), utlv,
+ size)) {
+ virtsnd_ctl_msg_unref(msg);
+ return -EFAULT;
}
- break;
- default:
- rc = -EINVAL;
- /* We never get here - we listed all values for op_flag */
- WARN_ON(1);
- goto on_msg_unref;
- }
- kfree(tlv);
- return rc;
-
-on_msg_unref:
- virtsnd_ctl_msg_unref(msg);
- kfree(tlv);
-
- return rc;
+ return virtsnd_ctl_msg_send(snd, msg, false);
+ }
+
+ return -EINVAL;
}
/**
@@ -369,27 +369,36 @@
struct virtio_snd_msg *msg;
struct virtio_snd_ctl_hdr *hdr;
unsigned int n = le32_to_cpu(kinfo->value.enumerated.items);
- struct scatterlist sg;
-
- msg = virtsnd_ctl_msg_alloc(sizeof(*hdr),
- sizeof(struct virtio_snd_hdr), GFP_KERNEL);
- if (!msg)
- return -ENOMEM;
+ size_t items_size;
+ int rc;
+
+ if (check_mul_overflow((size_t)n, sizeof(*kctl->items), &items_size) ||
+ items_size > UINT_MAX)
+ return -EOVERFLOW;
kctl->items = devm_kcalloc(&vdev->dev, n, sizeof(*kctl->items),
GFP_KERNEL);
- if (!kctl->items) {
- virtsnd_ctl_msg_unref(msg);
- return -ENOMEM;
- }
-
- sg_init_one(&sg, kctl->items, n * sizeof(*kctl->items));
+ if (!kctl->items)
+ return -ENOMEM;
+
+ msg = virtsnd_ctl_msg_alloc(sizeof(*hdr),
+ sizeof(struct virtio_snd_hdr), 0, items_size,
+ GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
hdr = virtsnd_ctl_msg_request(msg);
hdr->hdr.code = cpu_to_le32(VIRTIO_SND_R_CTL_ENUM_ITEMS);
hdr->control_id = cpu_to_le32(cid);
- return virtsnd_ctl_msg_send(snd, msg, NULL, &sg, false);
+ /* Item-buffer lifetime through the control-list copyout. */
+ virtsnd_ctl_msg_ref(msg);
+ rc = virtsnd_ctl_msg_send(snd, msg, false);
+ if (!rc && items_size)
+ memcpy(kctl->items, virtsnd_ctl_msg_response_data(msg), items_size);
+ virtsnd_ctl_msg_unref(msg);
+
+ return rc;
}
/**
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -678,7 +678,7 @@
break;
}
- msg = virtsnd_ctl_msg_alloc(request_size, response_size, gfp);
+ msg = virtsnd_ctl_msg_alloc(request_size, response_size, 0, 0, gfp);
if (msg) {
struct virtio_snd_pcm_hdr *hdr = virtsnd_ctl_msg_request(msg);
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (4 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:47 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee
` (7 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
A signal can end the interruptible wait while a STOP or RELEASE request is
still pending. A later teardown may then submit another RELEASE before the
first request completes.
Keep both waits non-interruptible and use the existing message timeout.
Other control requests remain interruptible. Device reset still cancels the
pending request and wakes the waiter.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -314,7 +314,20 @@
*/
virtsnd_ctl_msg_unref(msg);
} else if (!nowait) {
- rc = wait_for_completion_interruptible_timeout(&msg->notify, js);
+ /*
+ * Teardown must observe completion even with a pending signal.
+ * Otherwise a subsequent open can retry RELEASE while the device
+ * is still processing the previous teardown request.
+ */
+ switch (le32_to_cpu(request->code)) {
+ case VIRTIO_SND_R_PCM_STOP:
+ case VIRTIO_SND_R_PCM_RELEASE:
+ rc = wait_for_completion_timeout(&msg->notify, js);
+ break;
+ default:
+ rc = wait_for_completion_interruptible_timeout(&msg->notify, js);
+ break;
+ }
if (!rc) {
dev_err(&vdev->dev,
"control message (0x%08x) timeout\n",
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -47,6 +47,9 @@
* The msg_timeout_ms module parameter defines the message completion timeout.
* If the message is not completed within this time, the function will return an
* error.
+ * PCM STOP and RELEASE wait through pending signals within this timeout.
+ * Other requests remain interruptible. Device reset cancels and wakes either
+ * kind of waiter.
*
* Context: Any context that permits to sleep.
* Return: 0 on success, -errno on failure.
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (5 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:30 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock Joonho Lee
` (6 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
The staged virtio-snd capture path needs to sample stop_threshold from
completion context to distinguish free-running capture from bounded
capture. Software parameters can update this field while capture runs.
Mark the runtime stop-threshold assignments with WRITE_ONCE and the proc
status read with READ_ONCE. This pairs the assignments with the driver's
READ_ONCE policy sampling in patch 9.
These annotations do not provide a transaction for the entire sw_params
update. The driver serializes its cached policy and accounting with its
own substream lock.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/core/oss/pcm_oss.c b/sound/core/oss/pcm_oss.c
--- a/sound/core/oss/pcm_oss.c
+++ b/sound/core/oss/pcm_oss.c
@@ -2958,7 +2958,7 @@
runtime->oss.mmap_bytes);
#endif
/* In mmap mode we never stop */
- runtime->stop_threshold = runtime->boundary;
+ WRITE_ONCE(runtime->stop_threshold, runtime->boundary);
return 0;
}
diff --git a/sound/core/pcm.c b/sound/core/pcm.c
--- a/sound/core/pcm.c
+++ b/sound/core/pcm.c
@@ -422,7 +422,8 @@
snd_iprintf(buffer, "period_step: %u\n", runtime->period_step);
snd_iprintf(buffer, "avail_min: %lu\n", runtime->control->avail_min);
snd_iprintf(buffer, "start_threshold: %lu\n", runtime->start_threshold);
- snd_iprintf(buffer, "stop_threshold: %lu\n", runtime->stop_threshold);
+ snd_iprintf(buffer, "stop_threshold: %lu\n",
+ READ_ONCE(runtime->stop_threshold));
snd_iprintf(buffer, "silence_threshold: %lu\n", runtime->silence_threshold);
snd_iprintf(buffer, "silence_size: %lu\n", runtime->silence_size);
snd_iprintf(buffer, "boundary: %lu\n", runtime->boundary);
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -871,7 +871,7 @@
runtime->period_step = 1;
runtime->control->avail_min = runtime->period_size;
runtime->start_threshold = 1;
- runtime->stop_threshold = runtime->buffer_size;
+ WRITE_ONCE(runtime->stop_threshold, runtime->buffer_size);
runtime->silence_threshold = 0;
runtime->silence_size = 0;
runtime->boundary = runtime->buffer_size;
@@ -1062,7 +1062,8 @@
runtime->period_step = params->period_step;
runtime->control->avail_min = params->avail_min;
runtime->start_threshold = params->start_threshold;
- runtime->stop_threshold = params->stop_threshold;
+ /* Drivers may sample the stop policy from completion context. */
+ WRITE_ONCE(runtime->stop_threshold, params->stop_threshold);
runtime->silence_threshold = params->silence_threshold;
runtime->silence_size = params->silence_size;
params->boundary = runtime->boundary;
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (6 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:30 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery Joonho Lee
` (5 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Staged capture can make readable data available before a complete period
has elapsed. Skipping notification leaves a blocked reader asleep, while
using the period-interrupt path for this event can infer a false ring wrap
when the notification is delayed.
Add snd_pcm_progress_under_stream_lock() to update the hardware pointer
through the non-interrupt path and notify readers and fasync listeners.
It runs under the caller's PCM stream lock and does not tick the PCM timer.
The driver must release its pointer and trigger locks before calling it.
Existing period notification callers keep their current behavior.
Atomic and nonatomic synthetic PCM tests reproduced the missing wakeup
and false-wrap alternatives. The new helper delivered 64 final frames and
reported four available frames after delayed partial progress without XRUN.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -1182,6 +1182,7 @@
int snd_pcm_lib_ioctl(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
void snd_pcm_period_elapsed_under_stream_lock(struct snd_pcm_substream *substream);
+void snd_pcm_progress_under_stream_lock(struct snd_pcm_substream *substream);
void snd_pcm_period_elapsed(struct snd_pcm_substream *substream);
snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
void *buf, bool interleaved,
diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -1919,6 +1919,25 @@
EXPORT_SYMBOL(snd_pcm_period_elapsed_under_stream_lock);
/**
+ * snd_pcm_progress_under_stream_lock - Report progress without a period IRQ
+ * @substream: PCM substream
+ *
+ * Context: PCM stream lock held, with no driver pointer or trigger locks held.
+ * Updates the pointer and wakes readers for partial-period transfers. Unlike
+ * a period notification, this does not imply an elapsed period or tick the PCM
+ * timer. The same pointer, trigger and timestamp callbacks may be invoked.
+ */
+void snd_pcm_progress_under_stream_lock(struct snd_pcm_substream *substream)
+{
+ if (PCM_RUNTIME_CHECK(substream) || !snd_pcm_running(substream))
+ return;
+ snd_pcm_update_hw_ptr(substream);
+ snd_kill_fasync(substream->runtime->fasync, SIGIO, POLL_IN);
+}
+EXPORT_SYMBOL_GPL(snd_pcm_progress_under_stream_lock);
+
+
+/**
* snd_pcm_period_elapsed() - update the status of runtime for the next period by acquiring lock of
* PCM substream.
* @substream: the instance of PCM substream.
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (7 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 6:38 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work Joonho Lee
` (4 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Staged capture needs request-owned DMA storage and separate produced,
observed and consumed positions when shared clients use a boundary stop
threshold. Synchronize that policy with RX completion and acknowledgment.
Keep native device running state, RELEASE obligations and reset epochs
separate from the ALSA runtime state.
Serialize prepare, sync_stop, hw_params and hw_free with a lifecycle
mutex. Bind control messages to their originating epoch at enqueue and
retain request storage until device ownership ends. Keep the short,
packed, empty and pause-restart compatibility options disabled by default.
Exclude prepare from playback and capture ACK with each member's
buffer_mutex. Linked
prepare marks buffer_accessing negative only on its initiating member,
so testing that counter leaves later members exposed. Use a trylock from
the PCM-locked ACK path to preserve the buffer-to-PCM locking order.
Check the PCM state under its stream lock after acquiring lifecycle_lock
in sync_stop. Trigger holds that lock through its control requests; a
replacement active generation must retain its device state. Prepare
cannot replace an inactive generation while lifecycle_lock is held.
Refill staged RX descriptors after a definitely rejected native pause.
If recovery cannot establish a usable queue, latch XRUN and retain the
teardown obligation. Check for a detached virtqueue under its queue lock
before processing an IRQ callback.
Disable period work until open succeeds and disable it synchronously at
close. Cancel pending work after hw_free teardown even on error. This
also prevents error paths from queuing work after runtime release.
Forced interleavings reproduced the NULL-queue crash, false ACK XRUN,
empty RX queue after pause rejection and stale sync_stop interference.
The corrected paths passed recovery and reopen with KASAN and lockdep.
A forced playback ACK during prepare also reproduced stale accounting
and a first-write XRUN; the per-runtime trylock prevents that mutation.
Tests also rejected late work after close and exercised freeze, failed
restore and recovery. The original UAF itself was not reproduced.
The separate PCM notification helper supplies subperiod wakeups. RESET
conversion and notification priority follow in their own patches.
Link: https://sashiko.dev/#/patchset/20261029085032.70689-1-mynameisjune111%40gmail.com?part=9
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -348,6 +348,22 @@
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 @@
spin_unlock_irqrestore(&queue->lock, flags);
}
+ 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);
- for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i)
- snd->queues[i].vqueue = NULL;
+ for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
+ struct virtio_pcm_substream *vss = &snd->substreams[i];
+
+ cancel_work_sync(&vss->elapsed_period);
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->stopped = false;
+ vss->xfer_xrun = false;
+ vss->pause_restart_state = VIRTSND_PCM_PAUSE_RESTART_NONE;
+ }
}
/**
@@ -439,6 +466,7 @@
{
struct virtio_snd *snd = vdev->priv;
int rc;
+ unsigned int i;
rc = virtsnd_find_vqs(snd);
if (rc)
@@ -447,6 +475,12 @@
virtio_device_ready(vdev);
virtsnd_ctl_msg_enable(snd);
+ for (i = 0; i < snd->nsubstreams; ++i) {
+ struct virtio_pcm_substream *vss = &snd->substreams[i];
+
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->resetting = false;
+ }
virtsnd_enable_event_vq(snd);
return 0;
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -23,6 +23,9 @@
* @list: Pending message list entry.
* @notify: Request completed notification.
* @ref_count: Reference count used to manage a message lifetime.
+ * @pcm: Optional PCM generation owner, valid until control queue teardown.
+ * @pcm_epoch: Operation generation captured before request allocation.
+ * @submitted: Request was handed to the device.
* @cancelled: Request canceled during device teardown.
*/
struct virtio_snd_msg {
@@ -37,7 +40,49 @@
struct completion notify;
refcount_t ref_count;
bool cancelled;
+ bool submitted;
+ struct virtio_pcm_substream *pcm;
+ u64 pcm_epoch;
};
+
+/* Bind an operation's original generation before publishing the request. */
+void virtsnd_ctl_msg_bind_pcm(struct virtio_snd_msg *msg,
+ struct virtio_pcm_substream *vss, u64 epoch)
+{
+ msg->pcm = vss;
+ msg->pcm_epoch = epoch;
+}
+
+u64 virtsnd_ctl_msg_pcm_epoch(struct virtio_snd_msg *msg)
+{
+ return msg->pcm_epoch;
+}
+
+static int virtsnd_ctl_msg_enqueue(struct virtio_snd *snd,
+ struct virtio_snd_msg *msg,
+ struct scatterlist **sgs,
+ unsigned int nouts, unsigned int nins)
+{
+ struct virtqueue *vq = virtsnd_control_queue(snd)->vqueue;
+
+ if (!msg->pcm)
+ return virtqueue_add_sgs(vq, sgs, nouts, nins, msg, GFP_ATOMIC);
+
+ /* Control queue -> PCM lock serializes enqueue against device reset. */
+ guard(spinlock)(&msg->pcm->lock);
+ if (msg->pcm->resetting || msg->pcm->reset_epoch != msg->pcm_epoch)
+ return -ECANCELED;
+ return virtqueue_add_sgs(vq, sgs, nouts, nins, msg, GFP_ATOMIC);
+}
+
+/* Caller retains a reference across the synchronous send. */
+bool virtsnd_ctl_msg_uncertain(struct virtio_snd_msg *msg, int rc)
+{
+ return msg->submitted &&
+ (rc == -ETIMEDOUT || rc == -ERESTARTSYS ||
+ READ_ONCE(msg->cancelled));
+}
+
/**
* virtsnd_ctl_msg_ref() - Increment reference counter for the message.
@@ -293,9 +338,9 @@
if (snd->ctl_msgs_blocked)
rc = -ENODEV;
else
- rc = virtqueue_add_sgs(queue->vqueue, psgs, nouts, nins, msg,
- GFP_ATOMIC);
+ rc = virtsnd_ctl_msg_enqueue(snd, msg, psgs, nouts, nins);
if (!rc) {
+ msg->submitted = true;
notify = virtqueue_kick_prepare(queue->vqueue);
list_add_tail(&msg->list, &snd->ctl_msgs);
@@ -315,11 +360,14 @@
virtsnd_ctl_msg_unref(msg);
} else if (!nowait) {
/*
- * Teardown must observe completion even with a pending signal.
- * Otherwise a subsequent open can retry RELEASE while the device
- * is still processing the previous teardown request.
+ * PCM state changes must observe completion with a pending signal.
+ * Otherwise recovery can issue a conflicting command while the
+ * device is still processing the previous transition.
*/
switch (le32_to_cpu(request->code)) {
+ case VIRTIO_SND_R_PCM_SET_PARAMS:
+ case VIRTIO_SND_R_PCM_PREPARE:
+ case VIRTIO_SND_R_PCM_START:
case VIRTIO_SND_R_PCM_STOP:
case VIRTIO_SND_R_PCM_RELEASE:
rc = wait_for_completion_timeout(&msg->notify, js);
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -11,6 +11,13 @@
struct virtio_snd;
struct virtio_snd_msg;
+struct virtio_pcm_substream;
+
+void virtsnd_ctl_msg_bind_pcm(struct virtio_snd_msg *msg,
+ struct virtio_pcm_substream *vss, u64 epoch);
+u64 virtsnd_ctl_msg_pcm_epoch(struct virtio_snd_msg *msg);
+
+bool virtsnd_ctl_msg_uncertain(struct virtio_snd_msg *msg, int rc);
void virtsnd_ctl_msg_ref(struct virtio_snd_msg *msg);
@@ -47,7 +54,7 @@
* The msg_timeout_ms module parameter defines the message completion timeout.
* If the message is not completed within this time, the function will return an
* error.
- * PCM STOP and RELEASE wait through pending signals within this timeout.
+ * PCM state-changing requests wait through pending signals within this timeout.
* Other requests remain interruptible. Device reset cancels and wakes either
* kind of waiter.
*
diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c
--- a/sound/virtio/virtio_pcm.c
+++ b/sound/virtio/virtio_pcm.c
@@ -310,8 +310,105 @@
{
struct virtio_pcm_substream *vss =
container_of(work, struct virtio_pcm_substream, elapsed_period);
-
- snd_pcm_period_elapsed(vss->substream);
+ struct snd_pcm_substream *substream = vss->substream;
+ struct snd_pcm_runtime *runtime;
+ size_t period_bytes;
+ u64 period_floor;
+ u64 snapshot;
+ bool staged_capture;
+ bool paused_xrun = false;
+ bool notify = false;
+ bool partial = false;
+ bool requeue = false;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ staged_capture = vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->capture_staging;
+ }
+
+ /* Serialize the pointer snapshot with ALSA's pointer and ACK callbacks. */
+ guard(pcm_stream_lock_irqsave)(substream);
+ if (!substream->runtime)
+ return;
+ if (!snd_pcm_running(substream)) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ runtime = substream->runtime;
+ paused_xrun = runtime &&
+ runtime->state == SNDRV_PCM_STATE_PAUSED &&
+ vss->xfer_xrun;
+ if (paused_xrun)
+ virtsnd_pcm_capture_arm_paused_stop(vss);
+ }
+ if (paused_xrun) {
+ /* Stop paused transfers and wake blocked readers. */
+ snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN);
+ }
+ return;
+ }
+
+ if (!staged_capture) {
+ snd_pcm_period_elapsed_under_stream_lock(substream);
+ return;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ runtime = substream->runtime;
+ if (!vss->runtime_active ||
+ (!vss->xfer_enabled && !vss->xfer_xrun) || !runtime)
+ return;
+
+ period_bytes = snd_pcm_lib_period_bytes(substream);
+ if (vss->xfer_xrun) {
+ notify = true;
+ } else if (!period_bytes || period_bytes >= vss->buffer_bytes ||
+ !virtsnd_pcm_capture_counters_valid(vss) ||
+ vss->capture_notified_periods >
+ div64_ul(vss->capture_copied_total, period_bytes)) {
+ vss->xfer_xrun = true;
+ notify = true;
+ } else {
+ period_floor = div64_ul(vss->capture_copied_total, period_bytes);
+ if (period_floor > vss->capture_notified_periods) {
+ /* Reserve crossed boundaries before the callback. */
+ vss->capture_notified_periods = period_floor;
+ notify = true;
+ } else if (vss->capture_copied_total >
+ vss->capture_pointer_seen_total) {
+ notify = true;
+ partial = true;
+ }
+ }
+
+ if (notify) {
+ snapshot = vss->capture_copied_total;
+ vss->capture_callback_snapshot = snapshot;
+ vss->capture_callback_snapshot_active = true;
+ }
+ }
+
+ if (!notify)
+ return;
+
+ /* Pin the callback position while completions keep updating actual totals. */
+ if (partial)
+ snd_pcm_progress_under_stream_lock(substream);
+ else
+ snd_pcm_period_elapsed_under_stream_lock(substream);
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->capture_callback_snapshot_active = false;
+ vss->capture_callback_snapshot = 0;
+ if (vss->runtime_active && vss->xfer_enabled &&
+ !vss->xfer_xrun && substream->runtime) {
+ period_bytes = snd_pcm_lib_period_bytes(substream);
+ if (period_bytes &&
+ vss->capture_copied_total > vss->capture_pointer_seen_total)
+ requeue = true;
+ }
+ }
+
+ if (requeue)
+ virtsnd_pcm_schedule_period_elapsed(vss);
}
/**
@@ -351,8 +448,10 @@
vss->snd = snd;
vss->sid = i;
INIT_WORK(&vss->elapsed_period, virtsnd_pcm_period_elapsed);
+ disable_work(&vss->elapsed_period);
init_waitqueue_head(&vss->msg_empty);
spin_lock_init(&vss->lock);
+ mutex_init(&vss->lifecycle_lock);
}
info = kzalloc_objs(*info, snd->nsubstreams);
@@ -486,7 +585,7 @@
snd_pcm_set_ops(vpcm->pcm, i, &virtsnd_pcm_ops[i]);
}
- snd_pcm_set_managed_buffer_all(vpcm->pcm,
+ snd_pcm_lib_preallocate_pages_for_all(vpcm->pcm,
SNDRV_DMA_TYPE_VMALLOC, NULL,
0, 0);
}
@@ -517,8 +616,11 @@
break;
case VIRTIO_SND_EVT_PCM_XRUN:
scoped_guard(spinlock, &vss->lock) {
- if (vss->xfer_enabled)
+ if (vss->xfer_enabled) {
vss->xfer_xrun = true;
+ if (vss->capture_staging)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
}
break;
}
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -7,13 +7,32 @@
#define VIRTIO_SND_PCM_H
#include <linux/atomic.h>
+#include <linux/mutex.h>
+#include <linux/math64.h>
#include <linux/virtio_config.h>
+#include <linux/workqueue.h>
#include <sound/pcm.h>
#include <sound/pcm-indirect.h>
struct virtio_pcm;
struct virtio_pcm_msg;
struct page;
+
+enum virtio_pcm_pause_restart_state {
+ VIRTSND_PCM_PAUSE_RESTART_NONE,
+ VIRTSND_PCM_PAUSE_RESTART_CONFIGURED,
+ VIRTSND_PCM_PAUSE_RESTART_SET_PARAMS_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_PREPARE_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED,
+ VIRTSND_PCM_PAUSE_RESTART_START_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_RUNNING,
+ VIRTSND_PCM_PAUSE_RESTART_STOP_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_STOPPED,
+ VIRTSND_PCM_PAUSE_RESTART_RELEASE_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT,
+ VIRTSND_PCM_PAUSE_RESTART_RELEASED,
+ VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED,
+};
/**
* struct virtio_pcm_substream - VirtIO PCM substream.
@@ -26,16 +45,39 @@
* @pcm_indirect: Kernel indirect pcm structure.
* @hw: Kernel ALSA substream hardware descriptor.
* @elapsed_period: Kernel work to handle the elapsed period state.
+ * @lifecycle_lock: Serializes prepare with synchronous release, outside trigger.
+ * @resetting: Device queues are unavailable until restore completes.
+ * @reset_epoch: Invalidates control replies from before device reset.
* @lock: Spinlock that protects fields shared by interrupt handlers and
* substream operators, including @stopped.
* @buffer_bytes: Current buffer size in bytes.
* @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes).
+ * @capture_ready: Managed-mode bytes copied but not yet acknowledged by ALSA;
+ * always zero in free-running mode.
+ * @capture_boundary: ALSA boundary captured for this prepared epoch.
+ * @capture_copied_total: Monotonic bytes copied into the capture ring.
+ * @capture_acked_total: Monotonic bytes acknowledged by the capture client.
+ * @capture_pointer_seen_total: Last capture position sampled by ALSA.
+ * @capture_notified_periods: Highest full-period boundary reserved for notify.
+ * @capture_callback_snapshot: Capture position fixed for the period callback.
+ * @capture_callback_snapshot_active: The callback must use its fixed position.
+ * @runtime_active: ALSA runtime buffer may be accessed by the staged path.
+ * @capture_staging: Capture uses request-owned payload buffers.
+ * @capture_started: Non-empty capture data was received since prepare.
+ * @capture_empty: Consecutive empty startup completions.
+ * @capture_free_running: Cached staged-capture policy, protected by @lock.
* @xfer_enabled: Data transfer state (0 - off, 1 - on).
* @xfer_xrun: Data underflow/overflow state (0 - no xrun, 1 - xrun).
* @stopped: True if the substream is stopped and must be released on the device
* side; protected by @lock.
+ * @prepared: Device PREPARE succeeded; protected by @lock.
+ * @device_running: Native START succeeded without a subsequent successful STOP.
+ * @release_required: Successful PREPARE still needs a successful RELEASE.
+ * @control_uncertain: Native control result requires device reset before reuse.
+ * @capture_reset_observing: Reclaim staged samples during RESET pointer capture.
* @suspended: True if the substream is suspended and must be reconfigured on
* the device side at resume.
+ * @pause_restart_state: Device-side state for opt-in pause compatibility.
* @msgs: Allocated I/O messages.
* @nmsgs: Number of allocated I/O messages.
* @msg_count: Number of pending I/O messages in the virtqueue.
@@ -56,14 +98,35 @@
struct snd_pcm_hardware hw;
struct work_struct elapsed_period;
spinlock_t lock;
+ struct mutex lifecycle_lock;
+ u64 reset_epoch;
+ bool resetting;
size_t buffer_bytes;
size_t hw_ptr;
+ size_t capture_ready;
+ snd_pcm_uframes_t capture_boundary;
+ u64 capture_copied_total;
+ u64 capture_acked_total;
+ u64 capture_pointer_seen_total;
+ u64 capture_notified_periods;
+ u64 capture_callback_snapshot;
+ bool capture_callback_snapshot_active;
+ bool runtime_active;
+ bool capture_staging;
+ bool capture_started;
+ bool capture_free_running;
+ unsigned int capture_empty;
bool xfer_enabled;
bool xfer_xrun;
bool stopped;
- /* Successful PREPARE still needs RELEASE; protected by lock. */
+ bool prepared;
+ /* Native control state, protected by lock. */
+ bool device_running;
bool release_required;
+ bool control_uncertain;
+ bool capture_reset_observing;
bool suspended;
+ enum virtio_pcm_pause_restart_state pause_restart_state;
struct virtio_pcm_msg **msgs;
unsigned int nmsgs;
unsigned int msg_count;
@@ -76,6 +139,64 @@
wait_queue_head_t msg_empty;
};
+/* Caller holds the PCM stream lock and vss->lock. */
+static inline void
+virtsnd_pcm_capture_arm_paused_stop(struct virtio_pcm_substream *vss)
+{
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
+
+ lockdep_assert_held(&vss->lock);
+ if (!runtime || runtime->state != SNDRV_PCM_STATE_PAUSED)
+ return;
+
+ /* ALSA skips trigger(STOP) when moving a paused stream to XRUN. */
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ runtime->stop_operating = true;
+}
+
+/* Keep non-staged PCM notifications on schedule_work()'s normal queue. */
+static inline bool
+virtsnd_pcm_schedule_period_elapsed(struct virtio_pcm_substream *vss)
+{
+ return schedule_work(&vss->elapsed_period);
+}
+
+/*
+ * ALSA may observe a subperiod tail before its period callback. Keep actual
+ * unread occupancy separate from pointer observations and notification state;
+ * free-running streams account occupancy from monotonic totals instead.
+ */
+static inline size_t virtsnd_pcm_ring_offset(u64 total, size_t size)
+{
+ u64 remainder;
+
+ div64_u64_rem(total, size, &remainder);
+ return remainder;
+}
+
+static inline bool
+virtsnd_pcm_capture_counters_valid(const struct virtio_pcm_substream *vss)
+{
+ u64 outstanding;
+
+ if (!vss->buffer_bytes ||
+ vss->capture_copied_total < vss->capture_acked_total ||
+ vss->capture_pointer_seen_total > vss->capture_copied_total ||
+ vss->hw_ptr != virtsnd_pcm_ring_offset(vss->capture_copied_total, vss->buffer_bytes) ||
+ (vss->capture_callback_snapshot_active &&
+ (vss->capture_callback_snapshot < vss->capture_pointer_seen_total ||
+ vss->capture_callback_snapshot > vss->capture_copied_total)))
+ return false;
+
+ if (vss->capture_free_running)
+ return !vss->capture_ready;
+
+ outstanding = vss->capture_copied_total - vss->capture_acked_total;
+ return outstanding == (u64)vss->capture_ready &&
+ vss->capture_ready < vss->buffer_bytes;
+}
+
/**
* struct virtio_pcm_stream - VirtIO PCM stream.
* @substreams: VirtIO substreams belonging to the stream.
@@ -106,6 +227,11 @@
extern const struct snd_pcm_ops virtsnd_pcm_ops[];
+extern bool virtsnd_capture_staging;
+extern bool virtsnd_capture_packed_status;
+extern bool virtsnd_capture_allow_short;
+extern bool virtsnd_capture_allow_empty;
+
int virtsnd_pcm_validate(struct virtio_device *vdev);
int virtsnd_pcm_parse_cfg(struct virtio_snd *snd);
@@ -124,11 +250,16 @@
struct virtio_snd_msg *
virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss,
- unsigned int command, gfp_t gfp);
+ unsigned int command, gfp_t gfp, u64 epoch);
int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
unsigned int periods, unsigned int period_bytes);
+int virtsnd_pcm_capture_send(struct virtio_pcm_substream *vss);
+
+int virtsnd_pcm_capture_refresh_mode_locked(struct virtio_pcm_substream *vss,
+ bool *free_running);
+
void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss);
void virtsnd_pcm_msg_reset(struct virtio_pcm_substream *vss);
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -3,9 +3,11 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/compiler.h>
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/overflow.h>
+#include <linux/vmalloc.h>
#include <sound/pcm_params.h>
#include "virtio_card.h"
@@ -16,14 +18,16 @@
* @xfer: Request header payload.
* @status: Response header payload.
* @length: Data length in bytes.
+ * @data: Request-owned payload for staged capture.
* @data_offset: Offset in the driver-owned PCM buffer on NOMMU systems.
* @sgs: Payload scatter-gather table.
*/
struct virtio_pcm_msg {
struct virtio_pcm_substream *substream;
- struct virtio_snd_pcm_xfer xfer;
- struct virtio_snd_pcm_status status;
+ struct virtio_snd_pcm_xfer *xfer;
+ struct virtio_snd_pcm_status *status;
size_t length;
+ u8 *data;
#ifndef CONFIG_MMU
size_t data_offset;
#endif
@@ -228,6 +232,8 @@
if (check_mul_overflow((size_t)periods, (size_t)period_bytes,
&buffer_bytes))
return -EOVERFLOW;
+ if (vss->capture_staging && periods < 2)
+ return -EINVAL;
if (!vss->substream || !vss->substream->runtime)
return -EINVAL;
@@ -236,14 +242,19 @@
return -EINVAL;
#ifdef CONFIG_MMU
- rc = virtsnd_pcm_dma_pages_get(vss, runtime->dma_area, buffer_bytes);
- if (rc)
- return rc;
+ if (!vss->capture_staging) {
+ rc = virtsnd_pcm_dma_pages_get(vss, runtime->dma_area,
+ buffer_bytes);
+ if (rc)
+ return rc;
+ }
#else
- vss->owned_dma_area = kzalloc(buffer_bytes, GFP_KERNEL);
- if (!vss->owned_dma_area)
- return -ENOMEM;
- vss->owned_dma_bytes = buffer_bytes;
+ if (!vss->capture_staging) {
+ vss->owned_dma_area = kzalloc(buffer_bytes, GFP_KERNEL);
+ if (!vss->owned_dma_area)
+ return -ENOMEM;
+ vss->owned_dma_bytes = buffer_bytes;
+ }
#endif
vss->msgs = kzalloc_objs(*vss->msgs, periods);
@@ -255,32 +266,54 @@
vss->nmsgs = periods;
for (i = 0; i < periods; ++i) {
- int sg_num;
struct virtio_pcm_msg *msg;
u8 *data;
+ int sg_num;
+
+ if (vss->capture_staging) {
+ data = vzalloc(period_bytes);
+ } else {
#ifdef CONFIG_MMU
- data = runtime->dma_area + (size_t)period_bytes * i;
+ data = runtime->dma_area + (size_t)period_bytes * i;
#else
- data = vss->owned_dma_area + (size_t)period_bytes * i;
+ data = vss->owned_dma_area + (size_t)period_bytes * i;
#endif
+ }
+ if (!data) {
+ rc = -ENOMEM;
+ goto err_free_msgs;
+ }
+
sg_num = virtsnd_pcm_sg_num(data, period_bytes);
if (sg_num < 0) {
+ if (vss->capture_staging)
+ vfree(data);
rc = sg_num;
goto err_free_msgs;
}
msg = kzalloc_flex(*msg, sgs, sg_num + 2);
if (!msg) {
+ if (vss->capture_staging)
+ vfree(data);
rc = -ENOMEM;
goto err_free_msgs;
}
msg->substream = vss;
+ msg->data = vss->capture_staging ? data : NULL;
vss->msgs[i] = msg;
- sg_init_one(&msg->sgs[PCM_MSG_SG_XFER], &msg->xfer,
- sizeof(msg->xfer));
- sg_init_one(&msg->sgs[PCM_MSG_SG_STATUS], &msg->status,
- sizeof(msg->status));
+ /* DMA payloads must not share cachelines with CPU-owned fields. */
+ msg->xfer = kzalloc(sizeof(*msg->xfer), GFP_KERNEL);
+ msg->status = kzalloc(sizeof(*msg->status), GFP_KERNEL);
+ if (!msg->xfer || !msg->status) {
+ rc = -ENOMEM;
+ goto err_free_msgs;
+ }
+ sg_init_one(&msg->sgs[PCM_MSG_SG_XFER], msg->xfer,
+ sizeof(*msg->xfer));
+ sg_init_one(&msg->sgs[PCM_MSG_SG_STATUS], msg->status,
+ sizeof(*msg->status));
#ifndef CONFIG_MMU
msg->data_offset = (size_t)period_bytes * i;
#endif
@@ -316,8 +349,14 @@
{
unsigned int i;
- for (i = 0; vss->msgs && i < vss->nmsgs; ++i)
- kfree(vss->msgs[i]);
+ for (i = 0; vss->msgs && i < vss->nmsgs; ++i) {
+ if (vss->msgs[i]) {
+ vfree(vss->msgs[i]->data);
+ kfree(vss->msgs[i]->xfer);
+ kfree(vss->msgs[i]->status);
+ kfree(vss->msgs[i]);
+ }
+ }
kfree(vss->msgs);
#ifdef CONFIG_MMU
virtsnd_pcm_dma_pages_put(vss);
@@ -339,15 +378,26 @@
* substream was stopped are never sent to the device, so their length must be
* cleared before the substream is started again.
*
- * Context: Any context. Takes and releases the VirtIO substream spinlock.
+ * Context: Caller holds the VirtIO substream spinlock.
*/
void virtsnd_pcm_msg_reset(struct virtio_pcm_substream *vss)
{
unsigned int i;
- guard(spinlock_irqsave)(&vss->lock);
+ lockdep_assert_held(&vss->lock);
for (i = 0; vss->msgs && i < vss->nmsgs; ++i)
vss->msgs[i]->length = 0;
+ vss->capture_ready = 0;
+ vss->capture_copied_total = 0;
+ vss->capture_acked_total = 0;
+ vss->capture_pointer_seen_total = 0;
+ vss->capture_notified_periods = 0;
+ vss->capture_callback_snapshot = 0;
+ vss->capture_callback_snapshot_active = false;
+ vss->capture_boundary = 0;
+ vss->capture_free_running = false;
+ vss->capture_empty = 0;
+ vss->capture_started = false;
}
/**
@@ -377,6 +427,8 @@
bool notify = false;
int rc;
+ if (!vqueue || !vss->prepared || vss->suspended)
+ return -EBADFD;
if (!bytes)
return 0;
if (!period_bytes || !vss->msgs || offset >= vss->buffer_bytes ||
@@ -402,8 +454,8 @@
msg->length += n;
if (msg->length == period_bytes) {
- msg->xfer.stream_id = cpu_to_le32(vss->sid);
- memset(&msg->status, 0, sizeof(msg->status));
+ msg->xfer->stream_id = cpu_to_le32(vss->sid);
+ memset(msg->status, 0, sizeof(*msg->status));
#ifndef CONFIG_MMU
if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK) {
@@ -477,6 +529,290 @@
return 0;
}
+/* Caller holds vss->lock. */
+int virtsnd_pcm_capture_refresh_mode_locked(struct virtio_pcm_substream *vss,
+ bool *free_running)
+{
+ struct snd_pcm_runtime *runtime;
+ snd_pcm_uframes_t stop_threshold;
+ u64 outstanding;
+ bool new_free_running;
+
+ if (!vss->capture_staging || !vss->runtime_active || !vss->substream)
+ return -EBADFD;
+
+ runtime = vss->substream->runtime;
+ if (!runtime || !vss->buffer_bytes || !vss->capture_boundary)
+ return -EBADFD;
+
+ /* Validate the currently committed policy before observing a transition. */
+ if (!virtsnd_pcm_capture_counters_valid(vss))
+ return -EIO;
+
+ stop_threshold = READ_ONCE(runtime->stop_threshold);
+ new_free_running = stop_threshold == vss->capture_boundary;
+ if (new_free_running != vss->capture_free_running) {
+ outstanding = vss->capture_copied_total -
+ vss->capture_acked_total;
+ if (new_free_running) {
+ vss->capture_ready = 0;
+ } else {
+ if (outstanding >= vss->buffer_bytes)
+ return -EPIPE;
+ vss->capture_ready = outstanding;
+ }
+ vss->capture_free_running = new_free_running;
+ }
+
+ *free_running = new_free_running;
+ return 0;
+}
+
+/* Caller holds the RX queue and vss locks and has sampled the policy. */
+static int virtsnd_pcm_capture_enqueue(struct virtio_pcm_substream *vss,
+ bool free_running)
+{
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ struct virtqueue *vqueue = queue->vqueue;
+ unsigned long period_bytes;
+ size_t free_bytes;
+ size_t request_limit = vss->nmsgs;
+ unsigned int i;
+ bool added = false;
+
+ if (!vqueue || !vss->prepared || vss->suspended ||
+ !vss->capture_staging || !vss->runtime_active)
+ return -EBADFD;
+
+ period_bytes = snd_pcm_lib_period_bytes(vss->substream);
+ if (!period_bytes || !vss->buffer_bytes ||
+ period_bytes >= vss->buffer_bytes ||
+ free_running != vss->capture_free_running ||
+ !virtsnd_pcm_capture_counters_valid(vss) ||
+ vss->msg_count > vss->nmsgs)
+ return -EIO;
+
+ if (!free_running) {
+ free_bytes = vss->buffer_bytes - vss->capture_ready;
+ request_limit = free_bytes / period_bytes +
+ !!(free_bytes % period_bytes);
+ request_limit = min_t(size_t, request_limit, vss->nmsgs);
+ }
+
+ for (i = 0; i < vss->nmsgs && vss->msg_count < request_limit; ++i) {
+ struct virtio_pcm_msg *msg = vss->msgs[i];
+ struct scatterlist *sgs[] = {
+ &msg->sgs[PCM_MSG_SG_XFER],
+ &msg->sgs[PCM_MSG_SG_DATA],
+ &msg->sgs[PCM_MSG_SG_STATUS]
+ };
+ int rc;
+
+ if (msg->length)
+ continue;
+
+ msg->xfer->stream_id = cpu_to_le32(vss->sid);
+ /* Do not let an absent packed trailer reuse old PCM bytes. */
+ memset(msg->data, 0, period_bytes);
+ memset(msg->status, 0, sizeof(*msg->status));
+ msg->status->status = cpu_to_le32(VIRTIO_SND_S_BAD_MSG);
+ rc = virtqueue_add_sgs(vqueue, sgs, 1, 2, msg, GFP_ATOMIC);
+ if (rc) {
+ if (added && !(vss->features &
+ (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) &&
+ virtqueue_kick_prepare(vqueue))
+ virtqueue_notify(vqueue);
+ return rc;
+ }
+
+ msg->length = period_bytes;
+ vss->msg_count++;
+ added = true;
+ }
+
+ if (added && !(vss->features &
+ (1U << VIRTIO_SND_PCM_F_MSG_POLLING)) &&
+ virtqueue_kick_prepare(vqueue))
+ virtqueue_notify(vqueue);
+
+ return 0;
+}
+
+/**
+ * virtsnd_pcm_capture_send() - Refresh policy and queue staged requests.
+ * @vss: VirtIO PCM substream.
+ *
+ * Context: Expects the RX queue and substream spinlocks to be held by caller.
+ * Return: 0 on success, -errno on failure.
+ */
+int virtsnd_pcm_capture_send(struct virtio_pcm_substream *vss)
+{
+ bool free_running;
+ int rc;
+
+ rc = virtsnd_pcm_capture_refresh_mode_locked(vss, &free_running);
+ if (rc)
+ return rc;
+
+ return virtsnd_pcm_capture_enqueue(vss, free_running);
+}
+
+static int virtsnd_pcm_capture_status(const struct virtio_pcm_msg *msg,
+ size_t written_bytes,
+ size_t frame_bytes,
+ size_t *audio_bytes,
+ u32 *latency_bytes)
+{
+ struct virtio_snd_pcm_status status;
+ size_t max_written;
+ size_t first;
+
+ if (!frame_bytes ||
+ check_add_overflow(msg->length, sizeof(status), &max_written) ||
+ written_bytes < sizeof(status) || written_bytes > max_written)
+ return -EIO;
+
+ *audio_bytes = written_bytes - sizeof(status);
+ if (*audio_bytes > msg->length || *audio_bytes % frame_bytes)
+ return -EIO;
+
+ if (virtsnd_capture_packed_status) {
+ first = min_t(size_t, sizeof(status),
+ msg->length - *audio_bytes);
+ if (first)
+ memcpy(&status, msg->data + *audio_bytes, first);
+ if (first < sizeof(status))
+ memcpy((u8 *)&status + first, msg->status,
+ sizeof(status) - first);
+ } else {
+ status = *msg->status;
+ }
+
+ if (le32_to_cpu(status.status) != VIRTIO_SND_S_OK)
+ return -EIO;
+
+ *latency_bytes = le32_to_cpu(status.latency_bytes);
+ return 0;
+}
+
+static void virtsnd_pcm_capture_xrun_locked(struct virtio_pcm_substream *vss)
+{
+ if (!vss->xfer_xrun) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+}
+
+static void virtsnd_pcm_capture_complete(struct virtio_pcm_msg *msg,
+ size_t written_bytes)
+{
+ struct virtio_pcm_substream *vss = msg->substream;
+ size_t audio_bytes = 0;
+ size_t next_ready = 0;
+ u64 next_copied_total = 0;
+ bool free_running = false;
+ bool got_audio = false;
+
+ guard(spinlock)(&vss->lock);
+ vss->msg_count--;
+ if (vss->runtime_active && vss->xfer_enabled && !vss->xfer_xrun) {
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ size_t frame_bytes = runtime ? frames_to_bytes(runtime, 1) : 0;
+ u32 latency_bytes;
+ const char *invalid_reason = NULL;
+
+ /* A full ring is modulo-ambiguous, so fail closed before copying. */
+ if (!runtime || !runtime->dma_area)
+ invalid_reason = "runtime";
+ else if (virtsnd_pcm_capture_refresh_mode_locked(vss,
+ &free_running))
+ invalid_reason = "policy";
+ else if (!virtsnd_pcm_capture_counters_valid(vss))
+ invalid_reason = "counters";
+ else if (vss->hw_ptr !=
+ virtsnd_pcm_ring_offset(vss->capture_copied_total, vss->buffer_bytes))
+ invalid_reason = "hw_ptr";
+ else if (virtsnd_pcm_capture_status(msg, written_bytes,
+ frame_bytes, &audio_bytes,
+ &latency_bytes))
+ invalid_reason = "status";
+ else if (check_add_overflow(vss->capture_copied_total,
+ (u64)audio_bytes,
+ &next_copied_total))
+ invalid_reason = "total_overflow";
+ else if (free_running && audio_bytes >= vss->buffer_bytes)
+ invalid_reason = "payload_exceeds_ring";
+ else if (!free_running &&
+ audio_bytes >= vss->buffer_bytes - vss->capture_ready)
+ invalid_reason = "ring_capacity";
+ else if (!free_running &&
+ check_add_overflow(vss->capture_ready, audio_bytes,
+ &next_ready))
+ invalid_reason = "ready_overflow";
+ else if (!free_running && next_ready >= vss->buffer_bytes)
+ invalid_reason = "ring_full";
+ else if (free_running &&
+ (next_copied_total < vss->capture_pointer_seen_total ||
+ next_copied_total - vss->capture_pointer_seen_total >=
+ vss->buffer_bytes))
+ invalid_reason = "pointer_window";
+ else if (free_running)
+ next_ready = 0;
+
+ if (invalid_reason) {
+ virtsnd_pcm_capture_xrun_locked(vss);
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: staged capture completion rejected (%s)\n",
+ vss->sid, invalid_reason);
+ } else if (!audio_bytes) {
+ unsigned int limit = min_t(unsigned int, vss->nmsgs,
+ UINT_MAX / 2) * 2;
+
+ if (!virtsnd_capture_allow_empty || vss->capture_started ||
+ vss->capture_empty >= limit) {
+ virtsnd_pcm_capture_xrun_locked(vss);
+ } else {
+ vss->capture_empty++;
+ }
+ } else if (audio_bytes < msg->length &&
+ !virtsnd_capture_allow_short) {
+ virtsnd_pcm_capture_xrun_locked(vss);
+ } else {
+ size_t ring_ptr = virtsnd_pcm_ring_offset(vss->capture_copied_total,
+ vss->buffer_bytes);
+ size_t first = min(audio_bytes,
+ vss->buffer_bytes - ring_ptr);
+
+ memcpy(runtime->dma_area + ring_ptr, msg->data, first);
+ if (audio_bytes > first)
+ memcpy(runtime->dma_area, msg->data + first,
+ audio_bytes - first);
+ vss->capture_copied_total = next_copied_total;
+ vss->hw_ptr = virtsnd_pcm_ring_offset(next_copied_total, vss->buffer_bytes);
+ vss->capture_ready = next_ready;
+ vss->capture_started = true;
+ vss->capture_empty = 0;
+ runtime->delay = bytes_to_frames(runtime, latency_bytes);
+ got_audio = true;
+ }
+ }
+
+ msg->length = 0;
+ if (vss->runtime_active && vss->xfer_enabled) {
+ if (!vss->xfer_xrun &&
+ virtsnd_pcm_capture_enqueue(vss, free_running)) {
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: failed to refill capture queue\n",
+ vss->sid);
+ virtsnd_pcm_capture_xrun_locked(vss);
+ }
+ if (got_audio && !vss->xfer_xrun)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ } else if (!vss->msg_count) {
+ wake_up_all(&vss->msg_empty);
+ }
+}
+
/**
* virtsnd_pcm_msg_pending_num() - Returns the number of pending I/O messages.
* @vss: VirtIO substream.
@@ -528,9 +864,9 @@
* allowed, then each completed message is immediately placed back at the end
* of the queue.
*
- * For the playback substream, @written_bytes is equal to sizeof(msg->status).
- *
- * For the capture substream, @written_bytes is equal to sizeof(msg->status)
+ * For the playback substream, @written_bytes is equal to sizeof(*msg->status).
+ *
+ * For the capture substream, @written_bytes is equal to sizeof(*msg->status)
* plus the number of captured bytes.
*
* Context: Interrupt context. Takes and releases the VirtIO substream spinlock.
@@ -539,6 +875,12 @@
size_t written_bytes)
{
struct virtio_pcm_substream *vss = msg->substream;
+
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->capture_staging) {
+ virtsnd_pcm_capture_complete(msg, written_bytes);
+ return;
+ }
/*
* hw_ptr always indicates the buffer position of the first I/O message
@@ -548,10 +890,11 @@
guard(spinlock)(&vss->lock);
#ifndef CONFIG_MMU
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- vss->xfer_enabled && written_bytes > sizeof(msg->status)) {
+ vss->prepared && !vss->stopped &&
+ written_bytes > sizeof(*msg->status)) {
struct snd_pcm_runtime *runtime =
vss->substream ? vss->substream->runtime : NULL;
- size_t reported_bytes = written_bytes - sizeof(msg->status);
+ size_t reported_bytes = written_bytes - sizeof(*msg->status);
size_t data_bytes = min_t(size_t, reported_bytes, msg->length);
if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
@@ -568,8 +911,8 @@
}
#endif
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- written_bytes > sizeof(msg->status) &&
- written_bytes - sizeof(msg->status) > msg->length)
+ written_bytes > sizeof(*msg->status) &&
+ written_bytes - sizeof(*msg->status) > msg->length)
dev_err_ratelimited(&vss->snd->vdev->dev,
"SID %u: capture length exceeds I/O message capacity\n",
vss->sid);
@@ -579,11 +922,11 @@
* the actual SG payload capacity before accounting them.
*/
if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
- written_bytes <= sizeof(msg->status)) {
+ written_bytes <= sizeof(*msg->status)) {
vss->hw_ptr += msg->length;
} else {
size_t data_bytes = min_t(size_t,
- written_bytes - sizeof(msg->status),
+ written_bytes - sizeof(*msg->status),
msg->length);
vss->hw_ptr += data_bytes;
@@ -602,8 +945,8 @@
runtime->delay =
bytes_to_frames(runtime,
- le32_to_cpu(msg->status.latency_bytes));
- schedule_work(&vss->elapsed_period);
+ le32_to_cpu(msg->status->latency_bytes));
+ virtsnd_pcm_schedule_period_elapsed(vss);
} else if (!vss->msg_count) {
wake_up_all(&vss->msg_empty);
}
@@ -621,6 +964,9 @@
u32 written_bytes;
guard(spinlock_irqsave)(&queue->lock);
+ /* Reset can detach the queue before the transport drains its IRQs. */
+ if (!queue->vqueue)
+ return;
do {
virtqueue_disable_cb(queue->vqueue);
while ((msg = virtqueue_get_buf(queue->vqueue, &written_bytes)))
@@ -658,6 +1004,7 @@
* virtsnd_pcm_ctl_msg_alloc() - Allocate and initialize the PCM device control
* message for the specified substream.
* @vss: VirtIO PCM substream.
+ * @epoch: Operation generation captured before allocation.
* @command: Control request code (VIRTIO_SND_R_PCM_XXX).
* @gfp: Kernel flags for memory allocation.
*
@@ -666,7 +1013,7 @@
*/
struct virtio_snd_msg *
virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss,
- unsigned int command, gfp_t gfp)
+ unsigned int command, gfp_t gfp, u64 epoch)
{
size_t request_size = sizeof(struct virtio_snd_pcm_hdr);
size_t response_size = sizeof(struct virtio_snd_hdr);
@@ -684,6 +1031,7 @@
hdr->hdr.code = cpu_to_le32(command);
hdr->stream_id = cpu_to_le32(vss->sid);
+ virtsnd_ctl_msg_bind_pcm(msg, vss, epoch);
}
return msg;
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -3,9 +3,41 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/moduleparam.h>
+#include <linux/overflow.h>
#include <sound/pcm_params.h>
#include "virtio_card.h"
+
+
+bool virtsnd_capture_staging;
+module_param_named(capture_staging, virtsnd_capture_staging, bool, 0444);
+MODULE_PARM_DESC(capture_staging,
+ "Use request-owned buffers for experimental PCM capture");
+
+bool virtsnd_capture_packed_status;
+module_param_named(capture_packed_status, virtsnd_capture_packed_status,
+ bool, 0444);
+MODULE_PARM_DESC(capture_packed_status,
+ "Read staged capture status after the actual audio payload");
+
+bool virtsnd_capture_allow_short;
+module_param_named(capture_allow_short, virtsnd_capture_allow_short,
+ bool, 0444);
+MODULE_PARM_DESC(capture_allow_short,
+ "Accept non-empty short staged capture responses");
+
+bool virtsnd_capture_allow_empty;
+module_param_named(capture_allow_empty, virtsnd_capture_allow_empty,
+ bool, 0444);
+MODULE_PARM_DESC(capture_allow_empty,
+ "Retry bounded empty startup capture responses");
+
+static bool virtsnd_capture_pause_restart;
+module_param_named(capture_pause_restart, virtsnd_capture_pause_restart,
+ bool, 0444);
+MODULE_PARM_DESC(capture_pause_restart,
+ "Release and reprepare staged capture streams across pause");
/*
* I/O messages lifetime
@@ -97,14 +129,242 @@
};
static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream);
-
-static void virtsnd_pcm_start_failed(struct virtio_pcm_substream *vss)
-{
- scoped_guard(spinlock_irqsave, &vss->lock) {
+static int virtsnd_pcm_sync_stop_locked(struct snd_pcm_substream *substream);
+static int virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream);
+
+static void virtsnd_pcm_start_failed(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ bool notify;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return;
+ vss->xfer_enabled = false;
+ vss->stopped = vss->release_required || vss->msg_count;
+ notify = vss->runtime_active && vss->capture_staging &&
+ vss->direction == SNDRV_PCM_STREAM_CAPTURE;
+ if (notify)
+ vss->xfer_xrun = true;
+ }
+ vss->substream->runtime->stop_operating = true;
+ if (notify)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+}
+
+static bool
+virtsnd_pcm_pause_restart_enabled(struct virtio_pcm_substream *vss)
+{
+ return virtsnd_capture_pause_restart && vss->capture_staging &&
+ vss->direction == SNDRV_PCM_STREAM_CAPTURE;
+}
+
+static bool virtsnd_pcm_pause_restart_transitioning(enum virtio_pcm_pause_restart_state state)
+{
+ switch (state) {
+ case VIRTSND_PCM_PAUSE_RESTART_SET_PARAMS_SUBMITTED:
+ case VIRTSND_PCM_PAUSE_RESTART_PREPARE_SUBMITTED:
+ case VIRTSND_PCM_PAUSE_RESTART_START_SUBMITTED:
+ case VIRTSND_PCM_PAUSE_RESTART_STOP_SUBMITTED:
+ case VIRTSND_PCM_PAUSE_RESTART_RELEASE_SUBMITTED:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static int virtsnd_pcm_send_sync(struct virtio_pcm_substream *vss,
+ struct virtio_snd_msg *msg)
+{
+ u64 epoch = virtsnd_ctl_msg_pcm_epoch(msg);
+ bool uncertain;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch ||
+ vss->control_uncertain) {
+ rc = vss->control_uncertain ? -EIO : -ECANCELED;
+ virtsnd_ctl_msg_unref(msg);
+ return rc;
+ }
+ }
+ virtsnd_ctl_msg_ref(msg);
+ rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ uncertain = virtsnd_ctl_msg_uncertain(msg, rc);
+ virtsnd_ctl_msg_unref(msg);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (epoch != vss->reset_epoch || vss->resetting)
+ return -ECANCELED;
+ if (rc && uncertain)
+ vss->control_uncertain = true;
+ }
+ return rc;
+}
+
+static int
+virtsnd_pcm_pause_restart_send(struct virtio_pcm_substream *vss,
+ struct virtio_snd_msg *msg,
+ unsigned long allowed_states,
+ enum virtio_pcm_pause_restart_state submitted_state,
+ enum virtio_pcm_pause_restart_state success_state)
+{
+ enum virtio_pcm_pause_restart_state state;
+ bool uncertain;
+ u64 epoch = virtsnd_ctl_msg_pcm_epoch(msg);
+ int rc;
+
+ if (!virtsnd_pcm_pause_restart_enabled(vss))
+ return virtsnd_pcm_send_sync(vss, msg);
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ state = vss->pause_restart_state;
+ if (vss->resetting || vss->reset_epoch != epoch) {
+ rc = -ECANCELED;
+ } else if (state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED) {
+ rc = -EIO;
+ } else if (virtsnd_pcm_pause_restart_transitioning(state)) {
+ vss->pause_restart_state =
+ VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED;
+ rc = -EIO;
+ } else if (!(allowed_states & BIT(state))) {
+ rc = -EBADFD;
+ } else {
+ vss->pause_restart_state = submitted_state;
+ rc = 0;
+ }
+ }
+
+ if (rc) {
+ virtsnd_ctl_msg_unref(msg);
+ return rc;
+ }
+
+ virtsnd_ctl_msg_ref(msg);
+ rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ uncertain = virtsnd_ctl_msg_uncertain(msg, rc);
+ virtsnd_ctl_msg_unref(msg);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->reset_epoch != epoch || vss->resetting)
+ return -ECANCELED;
+ if ((rc && uncertain) || vss->pause_restart_state != submitted_state) {
+ vss->pause_restart_state =
+ VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED;
+ if (!rc)
+ rc = -EIO;
+ } else if (rc) {
+ vss->pause_restart_state = state;
+ } else {
+ vss->pause_restart_state = success_state;
+ }
+ }
+
+ if (rc && uncertain)
+ dev_err(&vss->snd->vdev->dev,
+ "SID %u: PCM control state is unknown; device reprobe required\n",
+ vss->sid);
+
+ return rc;
+}
+
+static void
+virtsnd_pcm_pause_restart_fail(struct virtio_pcm_substream *vss,
+ bool reset_required, u64 epoch)
+{
+ bool notify_xrun;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return;
+ notify_xrun = vss->runtime_active;
vss->xfer_enabled = false;
vss->stopped = true;
- }
+ vss->xfer_xrun = true;
+ if (reset_required)
+ vss->pause_restart_state =
+ VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED;
+ }
+ if (notify_xrun)
+ virtsnd_pcm_schedule_period_elapsed(vss);
vss->substream->runtime->stop_operating = true;
+}
+
+static int virtsnd_pcm_dev_prepare(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ struct virtio_snd_msg *msg;
+
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_PREPARE,
+ GFP_KERNEL, epoch);
+ if (!msg)
+ return -ENOMEM;
+
+ return virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_CONFIGURED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RELEASED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_STOPPED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_PREPARED),
+ VIRTSND_PCM_PAUSE_RESTART_PREPARE_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED);
+}
+
+static int virtsnd_pcm_pause_prepare(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ struct virtio_snd_msg *msg;
+
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_PREPARE,
+ GFP_KERNEL, epoch);
+ if (!msg)
+ return -ENOMEM;
+
+ return virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RELEASED),
+ VIRTSND_PCM_PAUSE_RESTART_PREPARE_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED);
+}
+
+static int virtsnd_pcm_release_wait(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ long waited;
+
+ waited = wait_event_timeout(vss->msg_empty,
+ !virtsnd_pcm_msg_pending_num(vss), js);
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ if (!waited || vss->msg_count ||
+ vss->pause_restart_state != VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT)
+ return -ETIMEDOUT;
+ vss->prepared = false;
+ vss->pause_restart_state = VIRTSND_PCM_PAUSE_RESTART_RELEASED;
+ return 0;
+}
+
+static int
+virtsnd_pcm_pause_release_flush(struct virtio_pcm_substream *vss,
+ unsigned long allowed_states, u64 epoch)
+{
+ struct virtio_snd_msg *msg;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ }
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
+ GFP_KERNEL, epoch);
+ if (!msg)
+ return -ENOMEM;
+ rc = virtsnd_pcm_pause_restart_send(vss, msg, allowed_states,
+ VIRTSND_PCM_PAUSE_RESTART_RELEASE_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT);
+ if (rc)
+ return rc;
+ return virtsnd_pcm_release_wait(vss, epoch);
+}
+
+static void virtsnd_pcm_pause_restart_failed(struct virtio_pcm_substream *vss,
+ u64 epoch)
+{
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
}
/**
@@ -119,23 +379,35 @@
struct virtio_pcm *vpcm = snd_pcm_substream_chip(substream);
struct virtio_pcm_stream *vs = &vpcm->streams[substream->stream];
struct virtio_pcm_substream *vss = vs->substreams[substream->number];
+ int rc;
substream->runtime->hw = vss->hw;
substream->private_data = vss;
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->capture_staging = virtsnd_capture_staging &&
+ vss->direction == SNDRV_PCM_STREAM_CAPTURE;
+ if (vss->capture_staging) {
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ }
+
snd_pcm_hw_constraint_integer(substream->runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
scoped_guard(spinlock_irqsave, &vss->lock)
vss->stopped |= !!vss->msg_count;
- vss->suspended = false;
/*
* If the substream has already been used, then the I/O queue may be in
* an invalid state. Just in case, we do a check and try to return the
* queue to its original state, if necessary.
*/
- return virtsnd_pcm_sync_stop(substream);
+ rc = virtsnd_pcm_sync_stop(substream);
+ if (!rc)
+ enable_work(&vss->elapsed_period);
+ return rc;
}
/**
@@ -147,6 +419,15 @@
*/
static int virtsnd_pcm_close(struct snd_pcm_substream *substream)
{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ /* Block late producers as well as drain already queued notifications. */
+ disable_work_sync(&vss->elapsed_period);
+
return 0;
}
@@ -159,6 +440,7 @@
* @channels: Selected number of channels.
* @format: Selected sample format (SNDRV_PCM_FORMAT_XXX).
* @rate: Selected frame rate.
+ * @epoch: Operation generation captured before preparing the request.
*
* Context: Any context that permits to sleep.
* Return: 0 on success, -errno on failure.
@@ -168,7 +450,7 @@
unsigned int period_bytes,
unsigned int channels,
snd_pcm_format_t format,
- unsigned int rate)
+ unsigned int rate, u64 epoch)
{
struct virtio_snd_msg *msg;
struct virtio_snd_pcm_set_params *request;
@@ -194,7 +476,7 @@
return -EINVAL;
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_SET_PARAMS,
- GFP_KERNEL);
+ GFP_KERNEL, epoch);
if (!msg)
return -ENOMEM;
@@ -213,7 +495,14 @@
request->features |=
cpu_to_le32(1U << VIRTIO_SND_PCM_F_EVT_XRUNS);
- return virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ return virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_NONE) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_CONFIGURED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_PREPARED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_STOPPED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RELEASED),
+ VIRTSND_PCM_PAUSE_RESTART_SET_PARAMS_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_CONFIGURED);
}
/**
@@ -232,8 +521,10 @@
unsigned int msg_count;
size_t period_buffer_bytes;
bool stopped;
+ u64 epoch;
int rc;
+ guard(mutex)(&vss->lifecycle_lock);
/* Fixed descriptors require whole-byte periods covering the buffer. */
if (check_mul_overflow((size_t)params_periods(hw_params),
(size_t)params_period_bytes(hw_params),
@@ -243,6 +534,25 @@
(u64)snd_pcm_format_physical_width(params_format(hw_params)) *
params_channels(hw_params)) % 8)
return -EINVAL;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ epoch = vss->reset_epoch;
+ /* Drain the previous buffer before ALSA can replace its mapping. */
+ rc = virtsnd_pcm_sync_stop_locked(substream);
+ if (rc)
+ return rc;
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->prepared = false;
+ }
+
+ if (vss->capture_staging) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ cancel_work_sync(&vss->elapsed_period);
+ }
scoped_guard(spinlock_irqsave, &vss->lock) {
msg_count = vss->msg_count;
@@ -260,11 +570,16 @@
return -EBADFD;
}
+ rc = snd_pcm_lib_malloc_pages(substream, params_buffer_bytes(hw_params));
+ if (rc < 0)
+ return rc;
+ substream->runtime->buffer_changed = rc > 0;
+
rc = virtsnd_pcm_dev_set_params(vss, params_buffer_bytes(hw_params),
params_period_bytes(hw_params),
params_channels(hw_params),
params_format(hw_params),
- params_rate(hw_params));
+ params_rate(hw_params), epoch);
if (rc)
return rc;
@@ -274,8 +589,14 @@
*/
virtsnd_pcm_msg_free(vss);
- return virtsnd_pcm_msg_alloc(vss, params_periods(hw_params),
- params_period_bytes(hw_params));
+ rc = virtsnd_pcm_msg_alloc(vss, params_periods(hw_params),
+ params_period_bytes(hw_params));
+ if (!rc && vss->capture_staging) {
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->runtime_active = true;
+ }
+
+ return rc;
}
/**
@@ -288,28 +609,93 @@
static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ int rc;
+
+ guard(mutex)(&vss->lifecycle_lock);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->stopped |= vss->prepared;
+ vss->prepared = false;
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ rc = virtsnd_pcm_sync_stop_locked(substream);
+ cancel_work_sync(&vss->elapsed_period);
+
+ if (vss->capture_staging) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ cancel_work_sync(&vss->elapsed_period);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->hw_ptr = 0;
+ vss->capture_ready = 0;
+ vss->capture_copied_total = 0;
+ vss->capture_acked_total = 0;
+ vss->capture_pointer_seen_total = 0;
+ vss->capture_notified_periods = 0;
+ vss->capture_callback_snapshot = 0;
+ vss->capture_callback_snapshot_active = false;
+ vss->capture_boundary = 0;
+ vss->capture_free_running = false;
+ }
+ }
/* If the queue is flushed, we can safely free the messages here. */
if (!virtsnd_pcm_msg_pending_num(vss))
virtsnd_pcm_msg_free(vss);
- return 0;
+ snd_pcm_lib_free_pages(substream);
+ return rc;
}
/**
- * virtsnd_pcm_prepare() - Prepare the PCM substream.
+ * virtsnd_pcm_prepare_locked() - Prepare the PCM substream.
* @substream: Kernel ALSA substream.
*
* Context: Process context.
* Return: 0 on success, -errno on failure.
*/
-static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
+static int virtsnd_pcm_prepare_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_device *vdev = vss->snd->vdev;
- struct virtio_snd_msg *msg;
+ bool pause_restart = virtsnd_pcm_pause_restart_enabled(vss);
+ bool free_running;
+ int rc;
bool stopped;
- int rc;
+ bool suspended;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ stopped = vss->stopped || vss->release_required || vss->msg_count;
+ }
+ if (stopped) {
+ rc = virtsnd_pcm_sync_stop_locked(substream);
+ if (rc)
+ return rc;
+ }
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->prepared = false;
+ suspended = vss->suspended;
+ }
+
+ if (pause_restart) {
+ enum virtio_pcm_pause_restart_state state;
+
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ state = vss->pause_restart_state;
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state)) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+ }
scoped_guard(spinlock_irqsave, &vss->lock)
stopped = vss->stopped;
@@ -320,48 +706,372 @@
return -EBADFD;
}
- if (!vss->suspended) {
+ if (!suspended) {
if (virtsnd_pcm_msg_pending_num(vss)) {
dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n",
vss->sid);
return -EBADFD;
}
- vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
- vss->hw_ptr = 0;
} else {
struct snd_pcm_runtime *runtime = substream->runtime;
unsigned int buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
unsigned int period_bytes = snd_pcm_lib_period_bytes(substream);
- int rc;
rc = virtsnd_pcm_dev_set_params(vss, buffer_bytes, period_bytes,
runtime->channels,
- runtime->format, runtime->rate);
- if (rc)
+ runtime->format, runtime->rate, epoch);
+ if (rc) {
+ if (pause_restart && rc != -ECANCELED)
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
return rc;
- }
-
- 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->pcm_indirect.hw_buffer_size =
- snd_pcm_lib_buffer_bytes(substream);
-
- msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_PREPARE,
- GFP_KERNEL);
+ }
+ }
+
+ 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;
+ }
+ }
+
+ 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;
+ }
+ if (pause_restart) {
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ } else {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->stopped = false;
+ vss->xfer_xrun = false;
+ }
+ }
+ }
+
+ return rc;
+}
+
+static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ guard(mutex)(&vss->lifecycle_lock);
+ return virtsnd_pcm_prepare_locked(substream);
+}
+
+static int virtsnd_pcm_pause_restart_start(struct virtio_pcm_substream *vss,
+ bool resume)
+{
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ struct virtio_snd_msg *msg;
+ enum virtio_pcm_pause_restart_state state;
+ bool reset_required;
+ bool invalid_state;
+ int rc = 0;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ state = vss->pause_restart_state;
+ reset_required =
+ state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state);
+ invalid_state =
+ vss->xfer_xrun ||
+ (resume &&
+ (state != VIRTSND_PCM_PAUSE_RESTART_RELEASED ||
+ !vss->stopped || vss->msg_count ||
+ !vss->runtime_active)) ||
+ (!resume &&
+ (state != VIRTSND_PCM_PAUSE_RESTART_PREPARED ||
+ vss->stopped));
+ }
+ if (reset_required) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+ if (invalid_state) {
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return -EBADFD;
+ }
+
+ if (resume) {
+ rc = virtsnd_pcm_pause_prepare(vss, epoch);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch) {
+ rc = -ECANCELED;
+ } else if (vss->pause_restart_state !=
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED ||
+ vss->msg_count || !vss->runtime_active) {
+ rc = -EBADFD;
+ } else {
+ /* Preserve ring accounting, restarting only startup grace. */
+ vss->prepared = true;
+ vss->capture_started = false;
+ vss->capture_empty = 0;
+ }
+ }
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+ }
+
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch) {
+ rc = -ECANCELED;
+ } else if (vss->pause_restart_state !=
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED ||
+ vss->msg_count || !vss->runtime_active || vss->xfer_xrun) {
+ rc = -EBADFD;
+ } else {
+ rc = virtsnd_pcm_capture_send(vss);
+ if (!rc)
+ vss->xfer_enabled = true;
+ }
+ }
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_START, GFP_KERNEL, epoch);
+ if (!msg) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -ENOMEM;
+ }
+
+ rc = virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_PREPARED),
+ VIRTSND_PCM_PAUSE_RESTART_START_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_RUNNING);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->stopped = false;
+ }
+ virtsnd_pcm_schedule_period_elapsed(vss);
+
+ return 0;
+}
+
+static int virtsnd_pcm_pause_restart_pause(struct virtio_pcm_substream *vss)
+{
+ struct virtio_snd_msg *msg;
+ enum virtio_pcm_pause_restart_state state;
+ int rc;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ state = vss->pause_restart_state;
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RELEASED) {
+ if (vss->xfer_xrun)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return vss->xfer_xrun ? -EPIPE : 0;
+ }
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT) {
+ /* Trigger holds the PCM lock; this helper never cancels work. */
+ rc = virtsnd_pcm_release_wait(vss, epoch);
+ if (!rc) {
+ guard(spinlock_irqsave)(&vss->lock);
+ rc = vss->xfer_xrun ? -EPIPE : 0;
+ }
+ if (rc && rc != -ECANCELED)
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state)) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RUNNING) {
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
+ GFP_KERNEL, epoch);
+ if (!msg) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -ENOMEM;
+ }
+
+ rc = virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RUNNING),
+ VIRTSND_PCM_PAUSE_RESTART_STOP_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_STOPPED);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+ state = VIRTSND_PCM_PAUSE_RESTART_STOPPED;
+ } else if (state != VIRTSND_PCM_PAUSE_RESTART_STOPPED &&
+ state != VIRTSND_PCM_PAUSE_RESTART_PREPARED) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -EBADFD;
+ }
+
+ rc = virtsnd_pcm_pause_release_flush(vss,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_STOPPED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_PREPARED), epoch);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return rc;
+ }
+
+ return 0;
+}
+
+static int virtsnd_pcm_pause_restart_stop(struct virtio_pcm_substream *vss,
+ bool suspend)
+{
+ struct virtio_snd_msg *msg;
+ enum virtio_pcm_pause_restart_state state;
+ int rc;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ if (!vss->prepared && !vss->msg_count)
+ return 0;
+ if (suspend)
+ vss->suspended = true;
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ state = vss->pause_restart_state;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state)) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+ if (state != VIRTSND_PCM_PAUSE_RESTART_RUNNING)
+ return 0;
+
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP, GFP_KERNEL, epoch);
+ if (!msg) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -ENOMEM;
+ }
+
+ rc = virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RUNNING),
+ VIRTSND_PCM_PAUSE_RESTART_STOP_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_STOPPED);
+ if (rc && rc != -ECANCELED)
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+
+ return rc;
+}
+
+static int virtsnd_pcm_stop_device(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ struct virtio_snd_msg *msg;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ if (vss->control_uncertain)
+ return -EIO;
+ if (!vss->device_running)
+ return 0;
+ }
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
+ GFP_KERNEL, epoch);
if (!msg)
return -ENOMEM;
-
- rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
- if (!rc) {
- guard(spinlock_irqsave)(&vss->lock);
- vss->release_required = true;
- }
+ rc = virtsnd_pcm_send_sync(vss, msg);
+ if (rc)
+ return rc;
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->device_running = false;
+ return 0;
+}
+
+static int virtsnd_pcm_start_queue(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ int rc = 0;
+
+ guard(spinlock_irqsave)(&queue->lock);
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ if (vss->stopped || !vss->prepared || vss->xfer_xrun)
+ return vss->xfer_xrun ? -EPIPE : -EBADFD;
+ if (vss->control_uncertain)
+ return -EIO;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ if (vss->capture_staging)
+ rc = vss->runtime_active ? virtsnd_pcm_capture_send(vss) : -EBADFD;
+ else if (!vss->device_running)
+ rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
+ }
+ if (!rc)
+ vss->xfer_enabled = true;
return rc;
}
@@ -377,68 +1087,219 @@
static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
- struct virtio_snd *snd = vss->snd;
- struct virtio_snd_queue *queue;
struct virtio_snd_msg *msg;
int rc = 0;
+ bool running;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ epoch = vss->reset_epoch;
+
+ if (virtsnd_pcm_pause_restart_enabled(vss)) {
+ switch (command) {
+ case SNDRV_PCM_TRIGGER_START:
+ return virtsnd_pcm_pause_restart_start(vss, false);
+ case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+ return virtsnd_pcm_pause_restart_start(vss, true);
+ case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+ return virtsnd_pcm_pause_restart_pause(vss);
+ case SNDRV_PCM_TRIGGER_STOP:
+ return virtsnd_pcm_pause_restart_stop(vss, false);
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ return virtsnd_pcm_pause_restart_stop(vss, true);
+ default:
+ return -EINVAL;
+ }
+ }
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;
- }
- vss->xfer_enabled = true;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ running = vss->device_running;
+ /* Refill RX descriptors returned during a rejected native pause. */
+ if (running)
+ return virtsnd_pcm_start_queue(vss, epoch);
+ 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;
- }
-
- msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
- GFP_KERNEL);
- if (!msg)
- return -ENOMEM;
-
- return virtsnd_ctl_msg_send_sync(snd, msg);
+ if (!vss->device_running)
+ return 0;
+ }
+
+ rc = virtsnd_pcm_stop_device(vss, epoch);
+ if (rc && command == SNDRV_PCM_TRIGGER_PAUSE_PUSH &&
+ virtsnd_pcm_start_queue(vss, epoch)) {
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->reset_epoch == epoch && !vss->resetting) {
+ vss->xfer_xrun = true;
+ vss->stopped = true;
+ substream->runtime->stop_operating = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+ }
+ return rc;
default:
return -EINVAL;
}
+
+}
+
+static int
+virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ enum virtio_pcm_pause_restart_state state;
+ struct virtio_snd_msg *msg;
+ long waited;
+ int rc;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ state = vss->pause_restart_state;
+ }
+ cancel_work_sync(&vss->elapsed_period);
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state)) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ dev_err(&vss->snd->vdev->dev,
+ "SID %u: PCM state is unknown; device reprobe required\n",
+ vss->sid);
+ return -EIO;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_NONE ||
+ state == VIRTSND_PCM_PAUSE_RESTART_CONFIGURED) {
+ if (virtsnd_pcm_msg_pending_num(vss)) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->stopped = false;
+ }
+ return 0;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RUNNING) {
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
+ GFP_KERNEL, epoch);
+ if (!msg) {
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return -ENOMEM;
+ }
+
+ rc = virtsnd_pcm_pause_restart_send(vss, msg,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_RUNNING),
+ VIRTSND_PCM_PAUSE_RESTART_STOP_SUBMITTED,
+ VIRTSND_PCM_PAUSE_RESTART_STOPPED);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return rc;
+ }
+ state = VIRTSND_PCM_PAUSE_RESTART_STOPPED;
+ }
+
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT ||
+ state == VIRTSND_PCM_PAUSE_RESTART_RELEASED) {
+ waited = wait_event_timeout(vss->msg_empty,
+ !virtsnd_pcm_msg_pending_num(vss), js);
+ if (waited <= 0) {
+ rc = waited ? (int)waited : -ETIMEDOUT;
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return rc;
+ }
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ if (vss->msg_count) {
+ rc = -ETIMEDOUT;
+ } else {
+ vss->pause_restart_state =
+ VIRTSND_PCM_PAUSE_RESTART_RELEASED;
+ vss->prepared = false;
+ vss->stopped = false;
+ rc = 0;
+ }
+ }
+ if (rc)
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return rc;
+ }
+
+ if (state != VIRTSND_PCM_PAUSE_RESTART_PREPARED &&
+ state != VIRTSND_PCM_PAUSE_RESTART_STOPPED) {
+ virtsnd_pcm_pause_restart_fail(vss, true, epoch);
+ return -EIO;
+ }
+
+ rc = virtsnd_pcm_pause_release_flush(vss,
+ BIT(VIRTSND_PCM_PAUSE_RESTART_PREPARED) |
+ BIT(VIRTSND_PCM_PAUSE_RESTART_STOPPED), epoch);
+ if (rc) {
+ if (rc == -ECANCELED)
+ return rc;
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return rc;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->prepared = false;
+ vss->stopped = false;
+ }
+
+ return 0;
}
/**
- * virtsnd_pcm_sync_stop() - Synchronous PCM substream stop.
+ * virtsnd_pcm_sync_stop_locked() - Synchronous PCM substream stop.
* @substream: Kernel ALSA substream.
*
* The function can be called both from the upper level or from the driver
@@ -447,7 +1308,7 @@
* Context: Process context. Takes and releases the VirtIO substream spinlock.
* Return: 0 on success, -errno on failure.
*/
-static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
+static int virtsnd_pcm_sync_stop_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd *snd = vss->snd;
@@ -455,31 +1316,49 @@
unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
bool need_release;
int rc;
-
- scoped_guard(spinlock_irqsave, &vss->lock) {
+ 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);
+ /* A rejected STOP leaves the device running and must precede RELEASE. */
+ rc = virtsnd_pcm_stop_device(vss, epoch);
+ if (rc) {
+ substream->runtime->stop_operating = true;
+ return rc;
+ }
if (need_release) {
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
- GFP_KERNEL);
+ GFP_KERNEL, epoch);
if (!msg) {
substream->runtime->stop_operating = true;
return -ENOMEM;
}
- rc = virtsnd_ctl_msg_send_sync(snd, msg);
+ rc = virtsnd_pcm_send_sync(vss, msg);
if (rc) {
substream->runtime->stop_operating = true;
return rc;
}
- scoped_guard(spinlock_irqsave, &vss->lock)
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
vss->release_required = false;
+ vss->device_running = false;
+ vss->prepared = false;
+ }
}
/*
@@ -488,7 +1367,7 @@
* Thus, we consider the absence of I/O messages in the queue as an
* indication that the substream has been released.
*/
- rc = wait_event_interruptible_timeout(vss->msg_empty,
+ rc = wait_event_timeout(vss->msg_empty,
!virtsnd_pcm_msg_pending_num(vss),
js);
if (rc <= 0) {
@@ -499,8 +1378,12 @@
return !rc ? -ETIMEDOUT : rc;
}
- scoped_guard(spinlock_irqsave, &vss->lock)
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->prepared = false;
vss->stopped = false;
+ }
return 0;
}
@@ -535,6 +1418,55 @@
virtsnd_pcm_cp_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ size_t hw_ptr = 0;
+ u64 total;
+ bool free_running;
+ bool xrun;
+
+ if (vss->capture_staging) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ xrun = vss->xfer_xrun;
+ if (!xrun &&
+ virtsnd_pcm_capture_refresh_mode_locked(vss,
+ &free_running))
+ xrun = true;
+ if (!xrun && !virtsnd_pcm_capture_counters_valid(vss))
+ xrun = true;
+ if (!xrun) {
+ total = vss->capture_callback_snapshot_active ?
+ vss->capture_callback_snapshot :
+ vss->capture_copied_total;
+ /* A full-ring observation gap is modulo-ambiguous. */
+ if (total < vss->capture_pointer_seen_total ||
+ total - vss->capture_pointer_seen_total >=
+ vss->buffer_bytes) {
+ xrun = true;
+ vss->xfer_xrun = true;
+ } else {
+ vss->capture_pointer_seen_total = total;
+ if (vss->capture_reset_observing) {
+ size_t period = snd_pcm_lib_period_bytes(substream);
+
+ vss->capture_acked_total = total;
+ vss->capture_ready = 0;
+ vss->capture_notified_periods =
+ period ? div64_ul(total, period) : 0;
+ }
+ hw_ptr = virtsnd_pcm_ring_offset(total, vss->buffer_bytes);
+ }
+ }
+ if (xrun)
+ vss->xfer_xrun = true;
+ }
+
+ if (xrun)
+ return SNDRV_PCM_POS_XRUN;
+
+ return bytes_to_frames(substream->runtime, hw_ptr);
+ }
+
+ if (READ_ONCE(vss->xfer_xrun))
+ return SNDRV_PCM_POS_XRUN;
return snd_pcm_indirect_capture_pointer(substream,
&vss->pcm_indirect,
@@ -553,7 +1485,9 @@
struct virtsnd_pcm_transfer *transfer =
container_of(rec, struct virtsnd_pcm_transfer, indirect);
- if (transfer->error)
+ if (transfer->error ||
+ (substream->stream == SNDRV_PCM_STREAM_CAPTURE &&
+ substream->runtime->state == SNDRV_PCM_STATE_DRAINING))
return;
transfer->error = virtsnd_pcm_msg_send(vss, rec->sw_data, bytes);
@@ -567,7 +1501,7 @@
vss->substream->runtime->stop_operating = true;
}
-static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
+static int virtsnd_pcm_pb_ack_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
@@ -576,6 +1510,8 @@
scoped_guard(spinlock_irqsave, &queue->lock) {
guard(spinlock)(&vss->lock);
+ if (!vss->prepared)
+ return -EBADFD;
transfer.indirect = vss->pcm_indirect;
if (vss->stopped) {
rc = -EPIPE;
@@ -602,17 +1538,133 @@
return 0;
}
-static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
+/* The PCM lock is held; exclude every member's buffer reconfiguration. */
+static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
+{
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ int rc;
+
+ if (!mutex_trylock(&runtime->buffer_mutex))
+ return -EBADFD;
+ rc = virtsnd_pcm_pb_ack_locked(substream);
+ mutex_unlock(&runtime->buffer_mutex);
+ return rc;
+}
+
+static int virtsnd_pcm_capture_staged_ack(struct virtio_pcm_substream *vss,
+ struct snd_pcm_runtime *runtime)
+{
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ int rc = 0;
+
+ /*
+ * The RX callback holds the queue lock while draining completions.
+ * Account ALSA's read under the substream lock alone so completions later
+ * in that drain observe the newly released ring space.
+ */
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ snd_pcm_uframes_t appl_ptr;
+ snd_pcm_sframes_t diff;
+ size_t bytes;
+ u64 outstanding;
+ u64 next_acked_total;
+ bool free_running;
+
+ if (!vss->runtime_active || !runtime || !runtime->control ||
+ !runtime->dma_area || vss->suspended)
+ return -EBADFD;
+ 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;
+ if (vss->xfer_xrun)
+ return -EPIPE;
+ rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
+ &free_running);
+ if (rc || !virtsnd_pcm_capture_counters_valid(vss)) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return -EPIPE;
+ }
+
+ appl_ptr = runtime->control->appl_ptr;
+ diff = appl_ptr - vss->pcm_indirect.appl_ptr;
+ if (diff < -(snd_pcm_sframes_t)(runtime->boundary / 2))
+ diff += runtime->boundary;
+ if (diff < 0 ||
+ (snd_pcm_uframes_t)diff > runtime->buffer_size) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return -EPIPE;
+ }
+
+ /* ACK includes any subperiod tail already copied to the ring. */
+ bytes = frames_to_bytes(runtime, diff);
+ outstanding = vss->capture_copied_total -
+ vss->capture_acked_total;
+ if ((!free_running && bytes > vss->capture_ready) ||
+ (free_running &&
+ (bytes > vss->buffer_bytes || bytes > outstanding)) ||
+ check_add_overflow(vss->capture_acked_total, (u64)bytes,
+ &next_acked_total) ||
+ next_acked_total > vss->capture_pointer_seen_total) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return -EPIPE;
+ }
+
+ vss->capture_acked_total = next_acked_total;
+ if (!free_running)
+ vss->capture_ready -= bytes;
+ vss->pcm_indirect.appl_ptr = appl_ptr;
+ }
+
+ /* Preserve the queue -> substream lock order while refilling requests. */
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ if (vss->runtime_active && vss->xfer_enabled &&
+ !vss->xfer_xrun) {
+ rc = virtsnd_pcm_capture_send(vss);
+ if (rc) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+ }
+ }
+
+ if (rc)
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: failed to refill capture queue after ACK: %d\n",
+ vss->sid, rc);
+
+ /* The ACK is committed; a refill failure is reported by the next XRUN. */
+ return 0;
+}
+
+static int virtsnd_pcm_cp_ack_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ struct snd_pcm_runtime *runtime = substream->runtime;
struct virtsnd_pcm_transfer transfer = {};
int rc;
+ if (vss->capture_staging) {
+ rc = virtsnd_pcm_capture_staged_ack(vss, runtime);
+ if (rc == -EPIPE) {
+ guard(spinlock_irqsave)(&vss->lock);
+ virtsnd_pcm_capture_arm_paused_stop(vss);
+ }
+ return rc;
+ }
+
scoped_guard(spinlock_irqsave, &queue->lock) {
guard(spinlock)(&vss->lock);
+ if (!vss->prepared && runtime->state != SNDRV_PCM_STATE_DRAINING)
+ return -EBADFD;
transfer.indirect = vss->pcm_indirect;
- if (vss->stopped) {
+ if (vss->stopped && runtime->state != SNDRV_PCM_STATE_DRAINING) {
rc = -EPIPE;
} else {
rc = snd_pcm_indirect_capture_transfer(substream,
@@ -635,6 +1687,50 @@
}
return 0;
+}
+
+/*
+ * Linked prepare locks each member's buffer_mutex, while buffer_accessing is
+ * negative only on its initiating member. ACK must exclude every prepare.
+ * The PCM lock is already held, so a failed trylock must return immediately.
+ */
+static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
+{
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ int rc;
+
+ if (!mutex_trylock(&runtime->buffer_mutex))
+ return -EBADFD;
+ rc = virtsnd_pcm_cp_ack_locked(substream);
+ mutex_unlock(&runtime->buffer_mutex);
+ return rc;
+}
+
+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(pcm_stream_lock_irqsave, substream) {
+ snd_pcm_state_t state = substream->runtime->state;
+
+ /*
+ * Trigger holds the PCM lock through its control requests. Wait
+ * for it, then leave a replacement active generation alone.
+ * lifecycle_lock keeps prepare from replacing an inactive one
+ * until sync_stop_locked has finished its teardown.
+ */
+ if (state == SNDRV_PCM_STATE_RUNNING ||
+ state == SNDRV_PCM_STATE_PAUSED ||
+ state == SNDRV_PCM_STATE_PREPARED ||
+ (state == SNDRV_PCM_STATE_DRAINING &&
+ substream->stream == SNDRV_PCM_STREAM_PLAYBACK))
+ return 0;
+ guard(spinlock_irqsave)(&vss->lock);
+ if (!vss->stopped)
+ return 0;
+ }
+ return virtsnd_pcm_sync_stop_locked(substream);
}
/* PCM substream operators map. */
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (8 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:33 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 11/14] docs: describe virtio-sound capture options Joonho Lee
` (3 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Queue staged capture period notifications on system_highpri_wq through
the common scheduling helper. This allows capture notifications to make
progress when ordinary workqueue workers are delayed by CPU contention.
Playback and ordinary capture keep schedule_work().
This preserves the existing priority proposal without mixing RESET or
lifecycle changes into it. It provides no hard real-time deadline and
cannot prevent a userspace reader from overrunning its buffer.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -159,6 +159,10 @@
static inline bool
virtsnd_pcm_schedule_period_elapsed(struct virtio_pcm_substream *vss)
{
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->capture_staging)
+ return queue_work(system_highpri_wq, &vss->elapsed_period);
+
return schedule_work(&vss->elapsed_period);
}
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 11/14] docs: describe virtio-sound capture options
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (9 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:33 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock Joonho Lee
` (2 subsequent siblings)
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Document the five opt-in capture module parameters introduced by this
series. All default to false. Describe the startup-empty, short-payload,
packed-status and pause-restart compatibility behavior alongside staging,
and require validation with the target device.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -8328,6 +8328,36 @@
brightness level.
default: 1
+ virtio_snd.capture_allow_empty= [HW,SOUND]
+ Retry a bounded number of empty startup capture replies.
+ Format: <bool>
+ Default: false. Compatibility option for devices that
+ require staged capture; validate with the target device.
+
+ virtio_snd.capture_allow_short= [HW,SOUND]
+ Accept non-empty, frame-aligned short capture replies.
+ Format: <bool>
+ Default: false. Compatibility option for devices that
+ require staged capture; validate with the target device.
+
+ virtio_snd.capture_packed_status= [HW,SOUND]
+ Read status immediately after the returned capture data.
+ Format: <bool>
+ Default: false. Compatibility option for devices that
+ require staged capture; validate with the target device.
+
+ virtio_snd.capture_pause_restart= [HW,SOUND]
+ Release and reprepare capture streams across pause.
+ Format: <bool>
+ Default: false. Compatibility option for devices that
+ require staged capture; validate with the target device.
+
+ virtio_snd.capture_staging= [HW,SOUND]
+ Use request-owned buffers for capture data.
+ Format: <bool>
+ Default: false. Compatibility option for devices that
+ require staged capture; validate with the target device.
+
virtio_mmio.device=
[VMMIO] Memory mapped virtio (platform) device.
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (10 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 11/14] docs: describe virtio-sound capture options Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:54 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET Joonho Lee
2026-10-30 5:26 ` [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET Joonho Lee
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
A driver may need to synchronize private capture accounting with the
common RESET pointer observation and the core's position rebase.
Add an optional reset operation called under the member's stream lock.
Expose snd_pcm_lib_reset() so the callback can retain common pointer and
time handling. On zero return the core completes that member's rebase
before unlocking it. Drivers without the callback retain the ioctl path.
The operation is also used after a successful prepare.
Zero permits the position rebase; it does not promise XRUN recovery.
A negative return skips the core rebase, so a callback returning an error
must leave coherent pointer state even after partial work.
Linked RESET and prepare stop at the first error without rollback.
Earlier callback members may already have completed. Legacy members keep
the existing post_action policy. Applications must recover the group
after partial failure before relying on shared positions.
This adds an internal kernel operation and no userspace ioctl or layout.
Out-of-tree modules using snd_pcm_ops need to be rebuilt.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -77,6 +77,26 @@
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
+ /*
+ * Optional RESET operation, also used after a successful prepare.
+ * Called with this substream's PCM lock held; atomic streams must not
+ * sleep. Do not reacquire the PCM lock through snd_pcm_lib_ioctl().
+ * snd_pcm_lib_reset() provides the common pointer update under this
+ * lock. The core completes the position and appl_ptr rebase before
+ * unlocking a successful member.
+ *
+ * Return zero to let the core complete the position rebase. As with
+ * the legacy RESET ioctl, zero does not guarantee recovery from XRUN;
+ * callers must still handle the stream state and subsequent I/O errors.
+ * On a negative return the core does not finish the rebase, so the
+ * callback must leave coherent pointer state even after partial work.
+ *
+ * Linked RESET/prepare
+ * stops at the first error without rollback. Earlier callback members
+ * retain their completed reset; legacy members retain the existing
+ * post_action policy. An error does not guarantee unchanged state.
+ */
+ int (*reset)(struct snd_pcm_substream *substream);
};
/*
@@ -1179,6 +1199,7 @@
{
substream->runtime->std_sync_id = true;
}
+int snd_pcm_lib_reset(struct snd_pcm_substream *substream);
int snd_pcm_lib_ioctl(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
void snd_pcm_period_elapsed_under_stream_lock(struct snd_pcm_substream *substream);
diff --git a/sound/core/pcm_lib.c b/sound/core/pcm_lib.c
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -1760,20 +1760,39 @@
}
EXPORT_SYMBOL(snd_pcm_hw_params_bits);
-static int snd_pcm_lib_ioctl_reset(struct snd_pcm_substream *substream,
- void *arg)
+/**
+ * snd_pcm_lib_reset - Reset the pointer while holding the PCM stream lock
+ * @substream: PCM substream
+ *
+ * Context: PCM stream lock held. The caller must not hold driver locks
+ * acquired by the pointer or XRUN callbacks.
+ *
+ * Update a running stream through the ordinary pointer path. Other states
+ * start at zero. Pointer errors keep the existing RESET return semantics;
+ * a zero return does not imply recovery from XRUN.
+ * This helper does not update appl_ptr or acquire the stream lock.
+ *
+ * Return: zero.
+ */
+int snd_pcm_lib_reset(struct snd_pcm_substream *substream)
{
struct snd_pcm_runtime *runtime = substream->runtime;
- guard(pcm_stream_lock_irqsave)(substream);
- if (snd_pcm_running(substream) &&
- snd_pcm_update_hw_ptr(substream) >= 0)
+ if (snd_pcm_running(substream) && snd_pcm_update_hw_ptr(substream) >= 0)
runtime->status->hw_ptr %= runtime->buffer_size;
else {
runtime->status->hw_ptr = 0;
runtime->hw_ptr_wrap = 0;
}
return 0;
+}
+EXPORT_SYMBOL(snd_pcm_lib_reset);
+
+static int snd_pcm_lib_ioctl_reset(struct snd_pcm_substream *substream,
+ void *arg)
+{
+ guard(pcm_stream_lock_irqsave)(substream);
+ return snd_pcm_lib_reset(substream);
}
static int snd_pcm_lib_ioctl_channel_info(struct snd_pcm_substream *substream,
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1988,7 +1988,31 @@
snd_pcm_state_t state)
{
struct snd_pcm_runtime *runtime = substream->runtime;
- int err = snd_pcm_ops_ioctl(substream, SNDRV_PCM_IOCTL1_RESET, NULL);
+ int err;
+
+ if (substream->ops->reset) {
+ /*
+ * Complete this member before releasing its PCM lock. A later
+ * linked member may fail; RESET has no group rollback operation.
+ */
+ guard(pcm_stream_lock_irq)(substream);
+
+ err = substream->ops->reset(substream);
+ if (err < 0)
+ return err;
+ runtime->hw_ptr_base = 0;
+ runtime->hw_ptr_interrupt = runtime->status->hw_ptr -
+ runtime->status->hw_ptr % runtime->period_size;
+ runtime->silence_start = runtime->status->hw_ptr;
+ runtime->silence_filled = 0;
+ runtime->control->appl_ptr = runtime->status->hw_ptr;
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
+ runtime->silence_size > 0)
+ snd_pcm_playback_silence(substream, ULONG_MAX);
+ return 0;
+ }
+
+ err = snd_pcm_ops_ioctl(substream, SNDRV_PCM_IOCTL1_RESET, NULL);
if (err < 0)
return err;
guard(pcm_stream_lock_irq)(substream);
@@ -2004,6 +2028,9 @@
snd_pcm_state_t state)
{
struct snd_pcm_runtime *runtime = substream->runtime;
+
+ if (substream->ops->reset)
+ return;
guard(pcm_stream_lock_irq)(substream);
runtime->control->appl_ptr = runtime->status->hw_ptr;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (11 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 5:53 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET Joonho Lee
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Use the locked PCM reset operation for staged capture. Reclaim RX space
at the same locked pointer observation used by the common reset and keep
completions arriving after that observation. Rebase the notified-period
counter there to avoid replaying old period notifications.
After common pointer mutation, finish the matching core rebase even if
refill fails. Latch XRUN and schedule notification. RESET can return zero
before the asynchronous error is visible; userspace still needs ordinary
XRUN recovery. A nonblocking read can initially return -EAGAIN and later
-EPIPE. This avoids leaving core and driver positions partly rebased.
A forced 90 ms RX interval reproduced -EPIPE with the earlier accounting.
The corrected sequence matched the next 48,000 deterministic frames and
passed reopen. Linked RESET and prepare partial-failure fixtures also
passed with atomic and nonatomic stream locking.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -9,6 +9,7 @@
#include "virtio_card.h"
+static const struct snd_pcm_ops virtsnd_pcm_capture_staged_ops;
bool virtsnd_capture_staging;
module_param_named(capture_staging, virtsnd_capture_staging, bool, 0444);
@@ -392,6 +393,10 @@
}
}
+ /* Keep RESET operation selection fixed for the opened runtime. */
+ substream->ops = vss->capture_staging ?
+ &virtsnd_pcm_capture_staged_ops :
+ &virtsnd_pcm_ops[substream->stream];
snd_pcm_hw_constraint_integer(substream->runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
@@ -1731,6 +1736,61 @@
return 0;
}
return virtsnd_pcm_sync_stop_locked(substream);
+}
+
+/* The core holds the stream lock through the matching appl_ptr reset. */
+static int virtsnd_pcm_reset(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ size_t position;
+ size_t period;
+ int rc;
+
+ /* Reclaim RX space atomically with the pointer observation. */
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->capture_reset_observing = true;
+ rc = snd_pcm_lib_reset(substream);
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->capture_reset_observing = false;
+ if (rc || !vss->capture_staging)
+ return rc;
+
+ guard(spinlock_irqsave)(&queue->lock);
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting)
+ return 0;
+ /* The common reset has already handled a pointer-reported XRUN. */
+ if (vss->xfer_xrun)
+ return 0;
+ if (snd_pcm_running(substream)) {
+ /* Keep completions arriving after the core's pointer observation. */
+ vss->capture_acked_total = vss->capture_pointer_seen_total;
+ vss->capture_ready = vss->capture_free_running ? 0 :
+ vss->capture_copied_total - vss->capture_acked_total;
+ } else {
+ position = frames_to_bytes(runtime, runtime->status->hw_ptr);
+ vss->hw_ptr = position;
+ period = snd_pcm_lib_period_bytes(substream);
+ vss->capture_copied_total = position;
+ vss->capture_acked_total = position;
+ vss->capture_pointer_seen_total = position;
+ vss->capture_ready = 0;
+ vss->capture_notified_periods = period ? position / period : 0;
+ }
+ vss->capture_callback_snapshot = 0;
+ vss->capture_callback_snapshot_active = false;
+ vss->pcm_indirect.appl_ptr = runtime->status->hw_ptr;
+ if (snd_pcm_running(substream) && vss->xfer_enabled && vss->prepared) {
+ rc = virtsnd_pcm_capture_send(vss);
+ if (rc) {
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+ }
+ /* Complete the matching core pointer rebase before reporting XRUN. */
+ return 0;
}
/* PCM substream operators map. */
@@ -1760,3 +1820,18 @@
.ack = virtsnd_pcm_cp_ack,
},
};
+
+/* Selected at open; the staging policy remains fixed for that runtime. */
+static const struct snd_pcm_ops virtsnd_pcm_capture_staged_ops = {
+ .open = virtsnd_pcm_open,
+ .close = virtsnd_pcm_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .reset = virtsnd_pcm_reset,
+ .hw_params = virtsnd_pcm_hw_params,
+ .hw_free = virtsnd_pcm_hw_free,
+ .prepare = virtsnd_pcm_prepare,
+ .trigger = virtsnd_pcm_trigger,
+ .sync_stop = virtsnd_pcm_sync_stop,
+ .pointer = virtsnd_pcm_cp_pointer,
+ .ack = virtsnd_pcm_cp_ack,
+};
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
` (12 preceding siblings ...)
2026-10-30 5:26 ` [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET Joonho Lee
@ 2026-10-30 5:26 ` Joonho Lee
2026-10-30 6:35 ` sashiko-bot
13 siblings, 1 reply; 29+ messages in thread
From: Joonho Lee @ 2026-10-30 5:26 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, anton.yakovlev, mst, virtualization, linux-kernel,
corbet, skhan, rdunlap, linux-doc, Joonho Lee
Ordinary capture's indirect positions must follow the common RESET
observation while retaining the device's fixed DMA ring offsets. Select
its reset callback at open alongside the staged capture callback.
Suppress ACK refilling while snd_pcm_lib_reset() observes the pointer.
Refilling from that nested ACK can fail before the common reset returns,
leaving driver accounting partly advanced while the core rebases to zero.
Observe the latest device pointer while paused too, since completions can
advance it after the last ordinary PCM pointer update.
After pointer observation, return discarded RX space and finish the core
rebase. A late refill failure latches XRUN and stops transfers while RESET
returns zero so the core can finish. Preserve that error across late I/O
capture completions. Playback keeps its previous successful-completion
behavior. The notification worker reports capture errors to paused streams
as well.
Do not schedule fresh work while the device is resetting.
With the worker delayed, an initial nonblocking read can return -EAGAIN;
after notification it returns -EPIPE, and paused resume is rejected.
Prepare or reopen is required for recovery. An earlier negative result
retains the existing core completion policy.
Fault tests reproduced nested refilling and loss of the latched error.
A playback XRUN-event and pause/resume check also verified that limiting
the latch to capture preserves playback completion behavior.
Paused pointer tests reproduced a stale reset position. The corrected
paths passed recovery and reopen. Ordinary and staged 70-second RW and
MMAP captures matched the deterministic backend sequence, including
reopen and the intended RESET discard boundary.
Link: https://sashiko.dev/#/patchset/20261029085032.70689-1-mynameisjune111%40gmail.com?part=14
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -75,6 +75,8 @@
* @release_required: Successful PREPARE still needs a successful RELEASE.
* @control_uncertain: Native control result requires device reset before reuse.
* @capture_reset_observing: Reclaim staged samples during RESET pointer capture.
+ * @indirect_reset_observing: Suppress ordinary ACK refill during RESET pointer
+ * capture; protected by the PCM stream lock.
* @suspended: True if the substream is suspended and must be reconfigured on
* the device side at resume.
* @pause_restart_state: Device-side state for opt-in pause compatibility.
@@ -125,6 +127,7 @@
bool release_required;
bool control_uncertain;
bool capture_reset_observing;
+ bool indirect_reset_observing;
bool suspended;
enum virtio_pcm_pause_restart_state pause_restart_state;
struct virtio_pcm_msg **msgs;
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -937,7 +937,9 @@
msg->length = 0;
- vss->xfer_xrun = false;
+ /* Preserve a latched capture error until prepare recovers it. */
+ if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK)
+ vss->xfer_xrun = false;
vss->msg_count--;
if (vss->xfer_enabled) {
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -10,6 +10,7 @@
#include "virtio_card.h"
static const struct snd_pcm_ops virtsnd_pcm_capture_staged_ops;
+static const struct snd_pcm_ops virtsnd_pcm_capture_indirect_ops;
bool virtsnd_capture_staging;
module_param_named(capture_staging, virtsnd_capture_staging, bool, 0444);
@@ -398,6 +399,9 @@
&virtsnd_pcm_capture_staged_ops :
&virtsnd_pcm_ops[substream->stream];
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE && !vss->capture_staging)
+ substream->ops = &virtsnd_pcm_capture_indirect_ops;
+
snd_pcm_hw_constraint_integer(substream->runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
@@ -1701,9 +1705,13 @@
*/
static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
int rc;
+ /* RESET observes the pointer before returning discarded RX space. */
+ if (vss->indirect_reset_observing)
+ return 0;
if (!mutex_trylock(&runtime->buffer_mutex))
return -EBADFD;
rc = virtsnd_pcm_cp_ack_locked(substream);
@@ -1736,6 +1744,77 @@
return 0;
}
return virtsnd_pcm_sync_stop_locked(substream);
+}
+
+/*
+ * The PCM lock covers indirect accounting and core completion. Once lib_reset
+ * changes hw_ptr, complete the core rebase even if refilling fails. The latched
+ * XRUN rejects subsequent pointer/ACK operations; the worker publishes XRUN to
+ * paused streams too. RESET success therefore does not imply stream recovery.
+ */
+static int virtsnd_pcm_indirect_reset(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
+ struct virtsnd_pcm_transfer transfer = {};
+ snd_pcm_uframes_t paused_ptr = 0;
+ bool paused = runtime->state == SNDRV_PCM_STATE_PAUSED;
+ unsigned int hw_ptr;
+ int rc;
+
+ /* Only PCM-locked callbacks access this observation flag. */
+ vss->indirect_reset_observing = true;
+ if (paused) {
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ hw_ptr = vss->hw_ptr;
+ paused_ptr = snd_pcm_indirect_capture_pointer(substream,
+ &vss->pcm_indirect, hw_ptr);
+ }
+ rc = snd_pcm_lib_reset(substream);
+ vss->indirect_reset_observing = false;
+ if (rc)
+ return rc;
+ if (!snd_pcm_running(substream) &&
+ runtime->state != SNDRV_PCM_STATE_PAUSED)
+ return 0;
+
+ guard(spinlock_irqsave)(&queue->lock);
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting)
+ return 0;
+ if (!vss->prepared || vss->stopped ||
+ (paused && paused_ptr == SNDRV_PCM_POS_XRUN)) {
+ virtsnd_pcm_transfer_failed(vss);
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return 0;
+ }
+
+ /* Discard completions observed through the paused device pointer too. */
+ if (runtime->state == SNDRV_PCM_STATE_PAUSED)
+ runtime->status->hw_ptr = paused_ptr;
+
+ transfer.indirect = vss->pcm_indirect;
+ transfer.indirect.sw_ready = 0;
+ /* Consume no application delta while returning discarded RX space. */
+ transfer.indirect.appl_ptr = runtime->control->appl_ptr;
+ rc = snd_pcm_indirect_capture_transfer(substream, &transfer.indirect,
+ virtsnd_pcm_trans_copy);
+ if (!rc)
+ rc = transfer.error;
+ if (rc) {
+ virtsnd_pcm_transfer_failed(vss);
+ vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return 0;
+ }
+
+ /* RESET discards all samples through the observed software pointer. */
+ transfer.indirect.sw_ready = 0;
+ transfer.indirect.appl_ptr = runtime->status->hw_ptr;
+ vss->pcm_indirect = transfer.indirect;
+ return 0;
}
/* The core holds the stream lock through the matching appl_ptr reset. */
@@ -1835,3 +1914,17 @@
.pointer = virtsnd_pcm_cp_pointer,
.ack = virtsnd_pcm_cp_ack,
};
+
+static const struct snd_pcm_ops virtsnd_pcm_capture_indirect_ops = {
+ .open = virtsnd_pcm_open,
+ .close = virtsnd_pcm_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .reset = virtsnd_pcm_indirect_reset,
+ .hw_params = virtsnd_pcm_hw_params,
+ .hw_free = virtsnd_pcm_hw_free,
+ .prepare = virtsnd_pcm_prepare,
+ .trigger = virtsnd_pcm_trigger,
+ .sync_stop = virtsnd_pcm_sync_stop,
+ .pointer = virtsnd_pcm_cp_pointer,
+ .ack = virtsnd_pcm_cp_ack,
+};
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling
2026-10-30 5:26 ` [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee
@ 2026-10-30 5:30 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:30 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> The staged virtio-snd capture path needs to sample stop_threshold from
> completion context to distinguish free-running capture from bounded
> capture. Software parameters can update this field while capture runs.
>
> Mark the runtime stop-threshold assignments with WRITE_ONCE and the proc
> status read with READ_ONCE. This pairs the assignments with the driver's
> READ_ONCE policy sampling in patch 9.
>
> These annotations do not provide a transaction for the entire sw_params
> update. The driver serializes its cached policy and accounting with its
> own substream lock.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=7
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock
2026-10-30 5:26 ` [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock Joonho Lee
@ 2026-10-30 5:30 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:30 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Staged capture can make readable data available before a complete period
> has elapsed. Skipping notification leaves a blocked reader asleep, while
> using the period-interrupt path for this event can infer a false ring wrap
> when the notification is delayed.
>
> Add snd_pcm_progress_under_stream_lock() to update the hardware pointer
> through the non-interrupt path and notify readers and fasync listeners.
> It runs under the caller's PCM stream lock and does not tick the PCM timer.
> The driver must release its pointer and trigger locks before calling it.
> Existing period notification callers keep their current behavior.
>
> Atomic and nonatomic synthetic PCM tests reproduced the missing wakeup
> and false-wrap alternatives. The new helper delivered 64 final frames and
> reported four available frames after delayed partial progress without XRUN.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=8
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 11/14] docs: describe virtio-sound capture options
2026-10-30 5:26 ` [PATCH v10 11/14] docs: describe virtio-sound capture options Joonho Lee
@ 2026-10-30 5:33 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:33 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Document the five opt-in capture module parameters introduced by this
> series. All default to false. Describe the startup-empty, short-payload,
> packed-status and pause-restart compatibility behavior alongside staging,
> and require validation with the target device.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=11
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work
2026-10-30 5:26 ` [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work Joonho Lee
@ 2026-10-30 5:33 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:33 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Queue staged capture period notifications on system_highpri_wq through
> the common scheduling helper. This allows capture notifications to make
> progress when ordinary workqueue workers are delayed by CPU contention.
> Playback and ordinary capture keep schedule_work().
>
> This preserves the existing priority proposal without mixing RESET or
> lifecycle changes into it. It provides no hard real-time deadline and
> cannot prevent a userspace reader from overrunning its buffer.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=10
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion
2026-10-30 5:26 ` [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee
@ 2026-10-30 5:36 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:36 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Control requests can expose caller-owned payloads to the device after a
> synchronous send times out. Keep request and response headers and payloads
> in the reference-counted message so caller cleanup cannot invalidate DMA.
>
> Keep the queue reference until device completion or post-reset cleanup,
> even when the caller times out or is interrupted. Consume the caller's
> reference on every send path, and let response readers retain an extra
> reference through copyout. Check payload sizes and allocation arithmetic
> before building the scatterlists.
>
> A control value payload is also read by the CPU as a typed structure. DMA
> cacheline alignment alone does not guarantee the structure's CPU alignment.
> Align regions to the greater of the DMA cacheline and control-value type
> alignment, and reserve prefix slack from the buffer base so typed accesses
> remain aligned while scatterlist regions stay on cacheline boundaries.
>
> Keep response values in a separately allocated aligned response buffer
> so INTEGER64 control accesses are aligned from this patch onward.
>
> Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=5
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown
2026-10-30 5:26 ` [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown Joonho Lee
@ 2026-10-30 5:47 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:47 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> A signal can end the interruptible wait while a STOP or RELEASE request is
> still pending. A later teardown may then submit another RELEASE before the
> first request completes.
>
> Keep both waits non-interruptible and use the existing message timeout.
> Other control requests remain interruptible. Device reset still cancels the
> pending request and wakes the waiter.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=6
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK
2026-10-30 5:26 ` [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
@ 2026-10-30 5:48 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:48 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> virtsnd_pcm_msg_send() can fail while queueing a transfer. Restore the
> failed message's length and notify the device about earlier messages
> queued by the same call, so they do not remain stranded.
>
> Propagate that failure through the indirect PCM transfer callback instead
> of discarding it. Commit the updated indirect state only after successful
> submission and stop further transfers when a send fails. Return an XRUN
> error to ALSA so the stream can recover.
>
> Keep the polling feature path unchanged.
>
> Fixes: fe981e67568c ("ALSA: virtio: use ack callback")
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=1
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start
2026-10-30 5:26 ` [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start Joonho Lee
@ 2026-10-30 5:50 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:50 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Capture trigger queues I/O messages before PCM_START. If the START
> message cannot be allocated or sent, transfers may remain queued, and
> ALSA does not invoke the action undo callback for the failed trigger.
>
> Mark the stream stopped and disable transfers for every failed START
> path. Arm runtime->stop_operating so sync_stop releases pending I/O and
> prevents prepare, hw_params, or START retries until release succeeds.
>
> The stopped flag is shared with ACK callbacks that can run without the
> ALSA stream lock. Protect every stopped access with vss->lock. Read the
> pending message count under the same lock in open and hw_params. In
> sync_stop, decide whether RELEASE is needed and mark the stream stopped
> in one locked transition; leave it stopped when release or draining fails.
>
> Always cancel elapsed-period work in sync_stop, even when no RELEASE
> is needed.
>
> Record the RELEASE obligation after successful PREPARE and clear it as
> soon as RELEASE succeeds. If the subsequent RX wait fails, retry that
> wait without submitting RELEASE again.
>
> Fixes: da76e9f3e43a ("ALSA: virtio: PCM substream operators")
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=2
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET
2026-10-30 5:26 ` [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET Joonho Lee
@ 2026-10-30 5:53 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:53 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Use the locked PCM reset operation for staged capture. Reclaim RX space
> at the same locked pointer observation used by the common reset and keep
> completions arriving after that observation. Rebase the notified-period
> counter there to avoid replaying old period notifications.
>
> After common pointer mutation, finish the matching core rebase even if
> refill fails. Latch XRUN and schedule notification. RESET can return zero
> before the asynchronous error is visible; userspace still needs ordinary
> XRUN recovery. A nonblocking read can initially return -EAGAIN and later
> -EPIPE. This avoids leaving core and driver positions partly rebased.
>
> A forced 90 ms RX interval reproduced -EPIPE with the earlier accounting.
> The corrected sequence matched the next 48,000 deterministic frames and
> passed reopen. Linked RESET and prepare partial-failure fixtures also
> passed with atomic and nonatomic stream locking.
>
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=13
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock
2026-10-30 5:26 ` [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock Joonho Lee
@ 2026-10-30 5:54 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:54 UTC (permalink / raw)
To: Joonho Lee, linux-sound; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] ALSA: pcm: unlocked appl_ptr overwrite in snd_pcm_post_prepare() with reset op
--
commit 963c7726f3bbfa33b12ff37026f791cf2e45509e
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: pcm: add an optional reset operation under the stream lock
This commit adds an optional reset callback to struct snd_pcm_ops that runs
under the substream's PCM stream lock, and exposes snd_pcm_lib_reset() for
drivers to retain the common pointer update logic.
> diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
> index a2ea44c3c1d29..41c691512f54c 100644
> --- a/sound/core/pcm_native.c
> +++ b/sound/core/pcm_native.c
> @@ -1988,7 +1988,31 @@ static int snd_pcm_do_reset(struct snd_pcm_substream *substream,
> snd_pcm_state_t state)
> {
> struct snd_pcm_runtime *runtime = substream->runtime;
> - int err = snd_pcm_ops_ioctl(substream, SNDRV_PCM_IOCTL1_RESET, NULL);
> + int err;
> +
> + if (substream->ops->reset) {
> + /*
> + * Complete this member before releasing its PCM lock. A later
> + * linked member may fail; RESET has no group rollback operation.
> + */
> + guard(pcm_stream_lock_irq)(substream);
> +
> + err = substream->ops->reset(substream);
> + if (err < 0)
> + return err;
> + runtime->hw_ptr_base = 0;
> + runtime->hw_ptr_interrupt = runtime->status->hw_ptr -
> + runtime->status->hw_ptr % runtime->period_size;
> + runtime->silence_start = runtime->status->hw_ptr;
> + runtime->silence_filled = 0;
> + runtime->control->appl_ptr = runtime->status->hw_ptr;
> + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
> + runtime->silence_size > 0)
> + snd_pcm_playback_silence(substream, ULONG_MAX);
> + return 0;
> + }
> +
> + err = snd_pcm_ops_ioctl(substream, SNDRV_PCM_IOCTL1_RESET, NULL);
> if (err < 0)
> return err;
> guard(pcm_stream_lock_irq)(substream);
> @@ -2004,6 +2028,9 @@ static void snd_pcm_post_reset(struct snd_pcm_substream *substream,
> snd_pcm_state_t state)
> {
> struct snd_pcm_runtime *runtime = substream->runtime;
> +
> + if (substream->ops->reset)
> + return;
[Severity: Medium]
Does snd_pcm_post_prepare() also need to account for substream->ops->reset?
In sound/core/pcm_native.c, the prepare path runs:
snd_pcm_prepare()
-> snd_pcm_action_nonatomic(&snd_pcm_action_prepare, ...)
-> snd_pcm_do_prepare()
-> snd_pcm_do_reset()
-> snd_pcm_post_prepare()
When substream->ops->reset is implemented, snd_pcm_do_reset() rebases
runtime->control->appl_ptr = runtime->status->hw_ptr under
guard(pcm_stream_lock_irq)(substream) and then releases the lock. While
snd_pcm_post_reset() skips the redundant assignment when substream->ops->reset
is set, snd_pcm_post_prepare() was not updated:
static void snd_pcm_post_prepare(struct snd_pcm_substream *substream,
snd_pcm_state_t state)
{
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->control->appl_ptr = runtime->status->hw_ptr;
snd_pcm_set_state(substream, SNDRV_PCM_STATE_PREPARED);
}
Between snd_pcm_do_reset() releasing pcm_stream_lock_irq and
snd_pcm_post_prepare() running (for example, while subsequent linked group
members execute snd_pcm_do_prepare()), a concurrent SNDRV_PCM_IOCTL_SYNC_PTR in
snd_pcm_sync_ptr() or a transfer/rewind/forward on another linked member that
was already in SNDRV_PCM_STATE_PREPARED can take pcm_stream_lock_irq and update
runtime->control->appl_ptr via pcm_lib_apply_appl_ptr(). Afterward,
snd_pcm_post_prepare() overwrites runtime->control->appl_ptr without holding
pcm_stream_lock_irq, which can desynchronize runtime->control->appl_ptr from
driver state.
In addition, because snd_pcm_do_prepare() calls snd_pcm_do_reset(), calling
snd_pcm_playback_silence(substream, ULONG_MAX) inside snd_pcm_do_reset() when
substream->ops->reset is set causes prepare on a playback stream with
runtime->silence_size > 0 to fill silence during do_prepare before the stream
transitions to SNDRV_PCM_STATE_PREPARED, whereas the legacy path only runs
snd_pcm_playback_silence() in snd_pcm_post_reset() and snd_pcm_post_start().
Should snd_pcm_post_prepare() skip the appl_ptr assignment when
substream->ops->reset is set, and should snd_pcm_do_reset() avoid calling
snd_pcm_playback_silence() when invoked from snd_pcm_do_prepare()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=12
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership
2026-10-30 5:26 ` [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership Joonho Lee
@ 2026-10-30 5:56 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:56 UTC (permalink / raw)
To: Joonho Lee, linux-sound; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] ALSA: virtio: pause release inflates msg->length in virtsnd_pcm_msg_send()
--
commit ec627d670b7dce9aa5415f6f76196d33998b7e23
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: Keep PCM DMA buffers alive through device ownership
This commit retains references to ALSA's vmalloc buffer pages while I/O
messages are queued (or uses a driver-owned buffer on NOMMU systems), validates
buffer and period boundaries, and clamps device-reported capture completion
lengths to msg->length.
> diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
> index 762480363b861..bc2002312a282 100644
> --- a/sound/virtio/virtio_pcm_msg.c
> +++ b/sound/virtio/virtio_pcm_msg.c
[ ... ]
> @@ -363,15 +546,48 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
> * the hw_ptr value is unconditionally advanced.
> */
> guard(spinlock)(&vss->lock);
> +#ifndef CONFIG_MMU
> + if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
> + vss->xfer_enabled && written_bytes > sizeof(msg->status)) {
> + struct snd_pcm_runtime *runtime =
> + vss->substream ? vss->substream->runtime : NULL;
> + size_t reported_bytes = written_bytes - sizeof(msg->status);
> + size_t data_bytes = min_t(size_t, reported_bytes, msg->length);
> +
> + if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
> + msg->data_offset > runtime->dma_bytes ||
> + data_bytes > runtime->dma_bytes - msg->data_offset ||
> + msg->data_offset > vss->owned_dma_bytes ||
> + data_bytes > vss->owned_dma_bytes - msg->data_offset)
> + dev_err_ratelimited(&vss->snd->vdev->dev,
> + "SID %u: invalid capture buffer for I/O message\n",
> + vss->sid);
> + else
> + memcpy(runtime->dma_area + msg->data_offset,
> + vss->owned_dma_area + msg->data_offset, data_bytes);
> + }
> +#endif
> + if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
> + written_bytes > sizeof(msg->status) &&
> + written_bytes - sizeof(msg->status) > msg->length)
> + dev_err_ratelimited(&vss->snd->vdev->dev,
> + "SID %u: capture length exceeds I/O message capacity\n",
> + vss->sid);
> /*
> * If the capture substream returned an incorrect status, then just
> - * increase the hw_ptr by the message size.
> + * increase the hw_ptr by the message size. Clamp valid capture lengths to
> + * the actual SG payload capacity before accounting them.
> */
> if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
> - written_bytes <= sizeof(msg->status))
> + written_bytes <= sizeof(msg->status)) {
> vss->hw_ptr += msg->length;
> - else
> - vss->hw_ptr += written_bytes - sizeof(msg->status);
> + } else {
> + size_t data_bytes = min_t(size_t,
> + written_bytes - sizeof(msg->status),
> + msg->length);
> +
> + vss->hw_ptr += data_bytes;
> + }
[Severity: High]
Can SNDRV_PCM_TRIGGER_PAUSE_RELEASE on a capture substream inflate msg->length
beyond period_bytes in virtsnd_pcm_msg_send(), bypassing the new clamp in
virtsnd_pcm_msg_complete() and advancing vss->hw_ptr out of bounds?
In sound/virtio/virtio_pcm_ops.c, virtsnd_pcm_trigger() handles both
SNDRV_PCM_TRIGGER_START and SNDRV_PCM_TRIGGER_PAUSE_RELEASE by submitting the
entire buffer on capture substreams:
sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_trigger() {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
...
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
}
When a capture stream is paused with SNDRV_PCM_TRIGGER_PAUSE_PUSH, the driver
sends VIRTIO_SND_R_PCM_STOP rather than VIRTIO_SND_R_PCM_RELEASE, so uncompleted
RX messages remain queued in the virtqueue with msg->length == period_bytes.
When SNDRV_PCM_TRIGGER_PAUSE_RELEASE subsequently calls
virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes), the loop adds n to msg->length
unconditionally for every period:
sound/virtio/virtio_pcm_msg.c:virtsnd_pcm_msg_send() {
n = period_bytes - (offset % period_bytes);
if (n > bytes)
n = bytes;
msg->length += n;
if (msg->length == period_bytes) {
...
}
For messages already queued in the virtqueue, msg->length becomes
2 * period_bytes (or (K + 1) * period_bytes after K pause/release cycles), and
for any partially acknowledged message (0 < msg->length < period_bytes),
msg->length is incremented past period_bytes without ever matching
msg->length == period_bytes to be re-queued.
In virtsnd_pcm_msg_complete(), clamping data_bytes to msg->length then permits
oversized device completions up to the inflated msg->length rather than the
single-period SG payload capacity (copying across adjacent periods on NOMMU and
advancing vss->hw_ptr by multiple periods). Likewise, if the stream is released
and pending RX descriptors complete with written_bytes <= sizeof(msg->status),
vss->hw_ptr += msg->length advances vss->hw_ptr by a multiple of period_bytes,
where the single subtraction of vss->buffer_bytes can leave
vss->hw_ptr >= vss->buffer_bytes.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=4
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset
2026-10-30 5:26 ` [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset Joonho Lee
@ 2026-10-30 5:57 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 5:57 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> Keep the control queue available through snd_card_free() so close-time
> STOP and RELEASE commands can still be submitted. Block new control
> requests and mark pending messages canceled before reset, then release
> their queue references only after reset and queue deletion.
>
> For PCM queues, detach each remaining virtqueue buffer after the device
> has stopped DMA and retire its message without reporting a normal
> completion. Detach all queues before deleting them so outstanding DMA
> mappings are unmapped and no token remains for a late device completion.
>
> Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
> Assisted-by: LLM
> Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=3
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET
2026-10-30 5:26 ` [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET Joonho Lee
@ 2026-10-30 6:35 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 6:35 UTC (permalink / raw)
To: Joonho Lee, linux-sound; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] ALSA: virtio: missing xfer_xrun checks in virtsnd_pcm_indirect_reset() and ACK
- [High] ALSA: virtio: RX msg->length corruption on PAUSE_RELEASE after paused RESET
- [Medium] ALSA: virtio: unlocked paused pointer update in virtsnd_pcm_indirect_reset()
--
commit 1a67c9c10a3b46bd06be6cb2ef4d737b79406386
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: rebase ordinary capture accounting on RESET
This commit adds an indirect reset callback for ordinary capture streams so
that indirect accounting follows the common ALSA reset observation while
preserving fixed DMA ring offsets. It also suppresses nested ACK refills
during pointer observation and preserves latched capture XRUN errors across
late I/O completions.
Link: https://sashiko.dev/#/patchset/20261029085032.70689-1-mynameisjune111%40gmail.com?part=14
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index 8060858d88203..4ab73c5e78f8e 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
[ ... ]
> @@ -1738,6 +1746,77 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
> return virtsnd_pcm_sync_stop_locked(substream);
> }
>
> +/*
> + * The PCM lock covers indirect accounting and core completion. Once lib_reset
> + * changes hw_ptr, complete the core rebase even if refilling fails. The latched
> + * XRUN rejects subsequent pointer/ACK operations; the worker publishes XRUN to
> + * paused streams too. RESET success therefore does not imply stream recovery.
> + */
> +static int virtsnd_pcm_indirect_reset(struct snd_pcm_substream *substream)
> +{
> + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
> + struct snd_pcm_runtime *runtime = substream->runtime;
> + struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
> + struct virtsnd_pcm_transfer transfer = {};
> + snd_pcm_uframes_t paused_ptr = 0;
> + bool paused = runtime->state == SNDRV_PCM_STATE_PAUSED;
> + unsigned int hw_ptr;
> + int rc;
> +
> + /* Only PCM-locked callbacks access this observation flag. */
> + vss->indirect_reset_observing = true;
> + if (paused) {
> + scoped_guard(spinlock_irqsave, &vss->lock)
> + hw_ptr = vss->hw_ptr;
> + paused_ptr = snd_pcm_indirect_capture_pointer(substream,
> + &vss->pcm_indirect, hw_ptr);
> + }
[Severity: Medium]
When resetting a paused stream in virtsnd_pcm_indirect_reset(), vss->hw_ptr
is sampled under a temporary vss->lock guard and
snd_pcm_indirect_capture_pointer() mutates vss->pcm_indirect before
queue->lock and vss->lock are acquired below.
Because snd_pcm_running(substream) is false in SNDRV_PCM_STATE_PAUSED,
snd_pcm_lib_reset() does not inspect vss->pcm_indirect or call
substream->ops->pointer(), and simply zeros runtime->status->hw_ptr and
runtime->hw_ptr_wrap.
If a late RX completion interrupt arrives via
virtsnd_pcm_rx_notify_cb()->virtsnd_pcm_msg_complete() between releasing
vss->lock here and acquiring queue->lock and vss->lock below, vss->hw_ptr
advances and msg->length is cleared to 0 while vss->xfer_enabled is false.
Wouldn't virtsnd_pcm_indirect_reset() then rebase runtime->status->hw_ptr to
the stale paused_ptr and miss re-enqueueing the newly completed RX
descriptor?
In addition, since snd_pcm_indirect_capture_pointer() mutates
vss->pcm_indirect (hw_io, hw_ready, sw_io) before the locked checks for
vss->resetting, !vss->prepared, and vss->stopped, can those early returns
leave vss->pcm_indirect out of sync with runtime->status->hw_ptr?
> + rc = snd_pcm_lib_reset(substream);
> + vss->indirect_reset_observing = false;
> + if (rc)
> + return rc;
> + if (!snd_pcm_running(substream) &&
> + runtime->state != SNDRV_PCM_STATE_PAUSED)
> + return 0;
> +
> + guard(spinlock_irqsave)(&queue->lock);
> + guard(spinlock)(&vss->lock);
> + if (vss->resetting)
> + return 0;
> + if (!vss->prepared || vss->stopped ||
> + (paused && paused_ptr == SNDRV_PCM_POS_XRUN)) {
> + virtsnd_pcm_transfer_failed(vss);
> + vss->xfer_xrun = true;
> + virtsnd_pcm_schedule_period_elapsed(vss);
> + return 0;
> + }
[Severity: High]
As reported by Sashiko on v9 [1], what happens if vss->xfer_xrun is already
latched when virtsnd_pcm_indirect_reset() or virtsnd_pcm_cp_ack_locked()
runs?
In sound/virtio/virtio_pcm.c:virtsnd_pcm_event(), receiving
VIRTIO_SND_EVT_PCM_XRUN on an active ordinary capture stream sets
vss->xfer_xrun = true without setting vss->stopped = true:
sound/virtio/virtio_pcm.c:virtsnd_pcm_event() {
case VIRTIO_SND_EVT_PCM_XRUN:
scoped_guard(spinlock, &vss->lock) {
if (vss->xfer_enabled) {
vss->xfer_xrun = true;
if (vss->capture_staging)
virtsnd_pcm_schedule_period_elapsed(vss);
}
}
break;
}
And with the change in virtsnd_pcm_msg_complete(), capture I/O completions no
longer clear vss->xfer_xrun. If the stream is paused via
SNDRV_PCM_TRIGGER_PAUSE_PUSH before a pointer query observes the XRUN (or if
VIRTIO_SND_EVT_PCM_XRUN arrives on a running stream just after
snd_pcm_lib_reset()), virtsnd_pcm_indirect_reset() calls
snd_pcm_indirect_capture_pointer() directly instead of
virtsnd_pcm_cp_pointer() and does not check vss->xfer_xrun in this condition
(unlike virtsnd_pcm_reset()). Won't it skip virtsnd_pcm_transfer_failed() and
virtsnd_pcm_schedule_period_elapsed(), overwrite runtime->status->hw_ptr with
paused_ptr, and re-enqueue RX descriptors onto the virtqueue despite the
latched XRUN?
Similarly, in sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_cp_ack_locked(), the
ordinary capture path checks vss->stopped but not vss->xfer_xrun:
sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_cp_ack_locked() {
if (vss->stopped && runtime->state != SNDRV_PCM_STATE_DRAINING) {
rc = -EPIPE;
} else {
rc = snd_pcm_indirect_capture_transfer(substream,
&transfer.indirect,
virtsnd_pcm_trans_copy);
}
}
If an ACK operation does not first pass through virtsnd_pcm_cp_pointer()
(such as reading buffered frames while paused or SNDRV_PCM_IOCTL_SYNC_PTR
without HWSYNC), won't virtsnd_pcm_cp_ack_locked() succeed and enqueue RX
buffers instead of rejecting the ACK with -EPIPE?
> +
> + /* Discard completions observed through the paused device pointer too. */
> + if (runtime->state == SNDRV_PCM_STATE_PAUSED)
> + runtime->status->hw_ptr = paused_ptr;
> +
> + transfer.indirect = vss->pcm_indirect;
> + transfer.indirect.sw_ready = 0;
> + /* Consume no application delta while returning discarded RX space. */
> + transfer.indirect.appl_ptr = runtime->control->appl_ptr;
> + rc = snd_pcm_indirect_capture_transfer(substream, &transfer.indirect,
> + virtsnd_pcm_trans_copy);
[Severity: High]
When virtsnd_pcm_indirect_reset() runs while the stream is in
SNDRV_PCM_STATE_PAUSED, snd_pcm_indirect_capture_transfer() invokes
virtsnd_pcm_trans_copy()->virtsnd_pcm_msg_send() to re-enqueue completed RX
descriptors into the virtqueue, setting their msg->length to period_bytes
while vss->device_running is false (cleared during
SNDRV_PCM_TRIGGER_PAUSE_PUSH in virtsnd_pcm_stop_device()).
When the stream is subsequently resumed via SNDRV_PCM_TRIGGER_PAUSE_RELEASE,
virtsnd_pcm_trigger() calls virtsnd_pcm_start_queue(), which sees
!vss->device_running and calls virtsnd_pcm_msg_send(vss, 0,
vss->buffer_bytes) across the entire buffer:
sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_start_queue() {
if (vss->capture_staging)
rc = virtsnd_pcm_capture_send(vss);
else if (!vss->device_running)
rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
}
In sound/virtio/virtio_pcm_msg.c:virtsnd_pcm_msg_send(), msg->length += n is
executed unconditionally before checking if msg->length == period_bytes:
sound/virtio/virtio_pcm_msg.c:virtsnd_pcm_msg_send() {
msg->length += n;
if (msg->length == period_bytes) {
...
}
}
For every RX descriptor already queued in the virtqueue with
msg->length == period_bytes, doesn't this increment msg->length to
2 * period_bytes on PAUSE_RELEASE (and similarly corrupt msg->length if
unread completed descriptors are re-submitted by virtsnd_pcm_start_queue()
and later ACKed by the application)?
[1] https://lore.kernel.org/r/20261029085032.70689-15-mynameisjune111@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=14
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery
2026-10-30 5:26 ` [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery Joonho Lee
@ 2026-10-30 6:38 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-30 6:38 UTC (permalink / raw)
To: Joonho Lee, linux-sound; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] ALSA: virtio: duplicate RX buffer enqueue on non-staged pause release
- [Medium] ALSA: virtio: detached virtqueue race in virtsnd_reset_vqs() and notify_cb
- [Medium] ALSA: virtio: incomplete pause rejection recovery in virtsnd_pcm_trigger()
- [Medium] ALSA: virtio: missing virtsnd_pcm_start_failed() on running trigger error
--
commit 4e7eab1d20a52ea1742792d7fd3cc04245ceba41
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: add staged capture accounting and lifecycle recovery
This commit adds staged capture accounting using request-owned DMA buffers
and introduces lifecycle recovery and epoch tracking across PCM operations
and device resets.
Link: https://sashiko.dev/#/patchset/20261029085032.70689-1-mynameisjune111%40gmail.com?part=9
> 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;
> + }
[Severity: Medium]
As reported by Sashiko on v9 [1], can an IRQ callback still observe a
non-NULL queue->vqueue after its buffers have been detached in
virtsnd_reset_vqs()?
In virtsnd_reset_vqs(), the first loop over VIRTIO_SND_VQ_MAX detaches
unused buffers under queue->lock and then drops the lock:
for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i) {
struct virtio_snd_queue *queue = &snd->queues[i];
unsigned long flags;
void *msg;
spin_lock_irqsave(&queue->lock, flags);
if (queue->vqueue)
while ((msg = virtqueue_detach_unused_buf(queue->vqueue)))
if (i == VIRTIO_SND_VQ_TX || i == VIRTIO_SND_VQ_RX)
virtsnd_pcm_msg_cancel(msg);
spin_unlock_irqrestore(&queue->lock, flags);
}
Only in the separate loop above does virtsnd_reset_vqs() re-acquire
snd->queues[i].lock to set snd->queues[i].vqueue = NULL.
Because virtqueue_detach_unused_buf_split() clears
vq->split.desc_state[i].data = NULL without advancing vq->last_used_idx,
if virtsnd_pcm_notify_cb() acquires queue->lock between those two loops,
it will see queue->vqueue != NULL and call virtqueue_get_buf() on a ring
whose descriptor state was already cleared, triggering BAD_RING() in
virtqueue_get_buf_ctx_split() if the device wrote a used entry just before
virtio_reset_device().
In addition, virtsnd_ctl_notify_cb() in sound/virtio/virtio_ctl_msg.c and
virtsnd_event_notify_cb() in sound/virtio/virtio_card.c do not check
queue->vqueue under queue->lock at all before calling virtqueue_get_buf(),
virtqueue_enable_cb(), or virtsnd_event_send().
Should queue->vqueue = NULL be set inside the same queue->lock critical
section that calls virtqueue_detach_unused_buf(), and should
virtsnd_ctl_notify_cb() and virtsnd_event_notify_cb() also check
if (!queue->vqueue) under queue->lock?
[ ... ]
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index de11bf162006e..8a6499d0cfea7 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
[ ... ]
> +static int virtsnd_pcm_start_queue(struct virtio_pcm_substream *vss, u64 epoch)
> +{
> + struct virtio_snd_queue *queue = virtsnd_pcm_queue(vss);
> + int rc = 0;
> +
> + guard(spinlock_irqsave)(&queue->lock);
> + guard(spinlock)(&vss->lock);
> + if (vss->resetting || vss->reset_epoch != epoch)
> + return -ECANCELED;
> + if (vss->stopped || !vss->prepared || vss->xfer_xrun)
> + return vss->xfer_xrun ? -EPIPE : -EBADFD;
> + if (vss->control_uncertain)
> + return -EIO;
> + if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
> + if (vss->capture_staging)
> + rc = vss->runtime_active ? virtsnd_pcm_capture_send(vss) : -EBADFD;
> + else if (!vss->device_running)
> + rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
> + }
[Severity: High]
On a non-staged capture stream, won't resuming from pause via
SNDRV_PCM_TRIGGER_PAUSE_RELEASE re-submit the entire capture buffer while
descriptors from before the pause are still queued or hold unread data?
When SNDRV_PCM_TRIGGER_START initially starts a non-staged capture stream,
virtsnd_pcm_start_queue() calls virtsnd_pcm_msg_send(vss, 0,
vss->buffer_bytes) to queue all vss->nmsgs descriptors with
msg->length = period_bytes.
When SNDRV_PCM_TRIGGER_PAUSE_PUSH pauses the stream,
virtsnd_pcm_stop_device() sends VIRTIO_SND_R_PCM_STOP and sets
vss->device_running = false without releasing the pending RX descriptors.
When SNDRV_PCM_TRIGGER_PAUSE_RELEASE later resumes the stream,
virtsnd_pcm_start_queue() sees !vss->device_running == true and calls
virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes) again across the whole
buffer:
msg->length += n;
if (msg->length == period_bytes) {
For descriptors still pending in the virtqueue, msg->length is incremented
to 2 * period_bytes, corrupting vss->hw_ptr accounting on error status and
preventing subsequent ACK re-submissions. For descriptors that already
completed before or during pause and are waiting for userspace to read and
ACK them, virtsnd_pcm_msg_send() re-queues them into the RX virtqueue,
allowing the device to overwrite unread audio samples via DMA.
> + if (!rc)
> + vss->xfer_enabled = true;
> + return rc;
> +}
> +
> /**
> * virtsnd_pcm_trigger() -Start/stop/pause/resume the PCM substream.
> * @substream: Kernel ALSA substream.
> @@ -377,68 +1087,219 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
> static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
> {
> struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
> - struct virtio_snd *snd = vss->snd;
> - struct virtio_snd_queue *queue;
> struct virtio_snd_msg *msg;
> int rc = 0;
> + bool running;
> + u64 epoch;
> +
> + scoped_guard(spinlock_irqsave, &vss->lock)
> + epoch = vss->reset_epoch;
> +
> + if (virtsnd_pcm_pause_restart_enabled(vss)) {
> + switch (command) {
> + case SNDRV_PCM_TRIGGER_START:
> + return virtsnd_pcm_pause_restart_start(vss, false);
> + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
> + return virtsnd_pcm_pause_restart_start(vss, true);
> + case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
> + return virtsnd_pcm_pause_restart_pause(vss);
> + case SNDRV_PCM_TRIGGER_STOP:
> + return virtsnd_pcm_pause_restart_stop(vss, false);
> + case SNDRV_PCM_TRIGGER_SUSPEND:
> + return virtsnd_pcm_pause_restart_stop(vss, true);
> + default:
> + return -EINVAL;
> + }
> + }
>
> 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;
> - }
> - vss->xfer_enabled = true;
> + scoped_guard(spinlock_irqsave, &vss->lock)
> + running = vss->device_running;
> + /* Refill RX descriptors returned during a rejected native pause. */
> + if (running)
> + return virtsnd_pcm_start_queue(vss, epoch);
[Severity: Medium]
As reported by Sashiko on v9 [1], what happens when running is true here
and virtsnd_pcm_start_queue(vss, epoch) fails, or when it succeeds on a
staged capture stream?
When SNDRV_PCM_TRIGGER_PAUSE_PUSH is rejected by the device in
virtsnd_pcm_stop_device(), vss->device_running remains true. When
SNDRV_PCM_TRIGGER_PAUSE_RELEASE subsequently runs (either from
snd_pcm_undo_pause(), which ignores the trigger return value, or a later
resume on a linked substream), virtsnd_pcm_trigger() sees running == true
and returns virtsnd_pcm_start_queue(vss, epoch) directly.
If virtsnd_pcm_start_queue() fails (for example, if
virtsnd_pcm_capture_refresh_mode_locked() returns -EPIPE after a
stop_threshold update, or virtqueue_add_sgs() fails while refilling
descriptors completed after PAUSE_PUSH), this early return bypasses
virtsnd_pcm_start_failed(vss, epoch), leaving vss->xfer_enabled,
vss->stopped, substream->runtime->stop_operating, and vss->xfer_xrun
unlatched and failing to schedule elapsed_period work to wake blocked
staged-capture readers.
Conversely, if virtsnd_pcm_start_queue() succeeds for staged capture on
this running == true path, returning early skips the
virtsnd_pcm_schedule_period_elapsed(vss) call at the end of the
SNDRV_PCM_TRIGGER_START / SNDRV_PCM_TRIGGER_PAUSE_RELEASE case.
> + 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 &&
> + virtsnd_pcm_start_queue(vss, epoch)) {
> + guard(spinlock_irqsave)(&vss->lock);
> + if (vss->reset_epoch == epoch && !vss->resetting) {
> + vss->xfer_xrun = true;
> + vss->stopped = true;
> + substream->runtime->stop_operating = true;
> + virtsnd_pcm_schedule_period_elapsed(vss);
> + }
> + }
> + return rc;
[Severity: Medium]
As reported by Sashiko on v9 [1], can a non-staged capture stream stall if
VIRTIO_SND_R_PCM_STOP is rejected during SNDRV_PCM_TRIGGER_PAUSE_PUSH?
Before calling virtsnd_pcm_stop_device(vss, epoch),
SNDRV_PCM_TRIGGER_PAUSE_PUSH sets vss->xfer_enabled = false. While
VIRTIO_SND_R_PCM_STOP is in flight, the device is still running and can
complete queued RX buffers. In virtsnd_pcm_msg_complete(), completions for
non-staged capture advance vss->hw_ptr, clear msg->length = 0, and
decrement vss->msg_count, but skip virtsnd_pcm_schedule_period_elapsed(vss)
because vss->xfer_enabled is false.
When virtsnd_pcm_stop_device() fails and leaves vss->device_running = true,
both virtsnd_pcm_start_queue(vss, epoch) here and the subsequent
SNDRV_PCM_TRIGGER_PAUSE_RELEASE recovery path check:
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
if (vss->capture_staging)
rc = vss->runtime_active ? virtsnd_pcm_capture_send(vss) : -EBADFD;
else if (!vss->device_running)
rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
}
if (!rc)
vss->xfer_enabled = true;
Because vss->device_running is still true, virtsnd_pcm_start_queue() only
sets vss->xfer_enabled = true without scheduling elapsed_period work or
refilling descriptors. If all queued RX descriptors completed while
vss->xfer_enabled was false, vss->msg_count is 0 and no period work is
scheduled, so ALSA never runs pointer/ack to re-enqueue RX buffers and the
capture stream stalls indefinitely.
[1] https://lore.kernel.org/r/20261029085032.70689-10-mynameisjune111@gmail.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261030052629.27742-1-mynameisjune111@gmail.com?part=9
^ permalink raw reply [flat|nested] 29+ messages in thread
end of thread, other threads:[~2026-10-30 6:38 UTC | newest]
Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-30 5:26 [PATCH v10 00/14] ALSA: virtio: staged capture recovery and PCM RESET synchronization Joonho Lee
2026-10-30 5:26 ` [PATCH v10 01/14] ALSA: virtio: Propagate PCM send failures through ACK Joonho Lee
2026-10-30 5:48 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 02/14] ALSA: virtio: Recover PCM state after a failed start Joonho Lee
2026-10-30 5:50 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 03/14] ALSA: virtio: Retain queued messages through device reset Joonho Lee
2026-10-30 5:57 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 04/14] ALSA: virtio: Keep PCM DMA buffers alive through device ownership Joonho Lee
2026-10-30 5:56 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 05/14] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee
2026-10-30 5:36 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 06/14] ALSA: virtio: wait through signals for PCM teardown Joonho Lee
2026-10-30 5:47 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 07/14] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee
2026-10-30 5:30 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 08/14] ALSA: pcm: report partial progress under the stream lock Joonho Lee
2026-10-30 5:30 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 09/14] ALSA: virtio: add staged capture accounting and lifecycle recovery Joonho Lee
2026-10-30 6:38 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 10/14] ALSA: virtio: prioritize staged capture period work Joonho Lee
2026-10-30 5:33 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 11/14] docs: describe virtio-sound capture options Joonho Lee
2026-10-30 5:33 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 12/14] ALSA: pcm: add an optional reset operation under the stream lock Joonho Lee
2026-10-30 5:54 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 13/14] ALSA: virtio: synchronize staged capture accounting during RESET Joonho Lee
2026-10-30 5:53 ` sashiko-bot
2026-10-30 5:26 ` [PATCH v10 14/14] ALSA: virtio: rebase ordinary capture accounting on RESET Joonho Lee
2026-10-30 6:35 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.