* [PATCH v32 01/20] ALSA: pcm: mark stop-threshold accesses for capture policy sampling
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:46 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 02/20] ALSA: pcm: report partial progress under the stream lock Joonho Lee via B4 Relay
` (18 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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
staged-capture policy sampling.
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>
---
sound/core/oss/pcm_oss.c | 2 +-
sound/core/pcm.c | 3 ++-
sound/core/pcm_native.c | 5 +++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/sound/core/oss/pcm_oss.c b/sound/core/oss/pcm_oss.c
index e8df27d09b97..897c8dddcfd2 100644
--- a/sound/core/oss/pcm_oss.c
+++ b/sound/core/oss/pcm_oss.c
@@ -2958,7 +2958,7 @@ static int snd_pcm_oss_mmap(struct file *file, struct vm_area_struct *area)
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
index 064df4cd3b50..356ddaf33f63 100644
--- a/sound/core/pcm.c
+++ b/sound/core/pcm.c
@@ -422,7 +422,8 @@ static void snd_pcm_substream_proc_sw_params_read(struct snd_info_entry *entry,
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
index 35152fae74ca..a2ea44c3c1d2 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -871,7 +871,7 @@ static int snd_pcm_hw_params(struct snd_pcm_substream *substream,
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 @@ static int snd_pcm_sw_params(struct snd_pcm_substream *substream,
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;
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 02/20] ALSA: pcm: report partial progress under the stream lock
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
2026-11-13 6:41 ` [PATCH v32 01/20] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:05 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 03/20] ALSA: pcm: add an optional reset operation " Joonho Lee via B4 Relay
` (17 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Allow a driver to publish subperiod progress under the PCM stream lock
without generating a period timer notification. Use the ordinary pointer
update, XRUN and wakeup path. Capture DRAINING can publish its final
tail while playback and other inactive states retain their existing
behavior.
Handle capture DRAINING separately in the state update: a full buffer is
still readable capture data, while progress beyond the buffer capacity
is an overrun. Playback continues to finish draining when its buffer is
empty.
Refresh capture availability after the unlocked copy and application
pointer commit while DRAINING. A final tail can arrive during the copy;
subtracting only the copied frames from the earlier snapshot can
otherwise finish draining with unread samples left in the ring.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
include/sound/pcm.h | 1 +
sound/core/pcm_lib.c | 40 +++++++++++++++++++++++++++++++++++++---
2 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index 1cf511f8d880..965be5603729 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -1182,6 +1182,7 @@ static inline void snd_pcm_set_sync(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);
+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
index bb16738bb591..4fb581b03480 100644
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -215,8 +215,14 @@ int snd_pcm_update_state(struct snd_pcm_substream *substream,
if (avail > runtime->avail_max)
runtime->avail_max = avail;
if (runtime->state == SNDRV_PCM_STATE_DRAINING) {
- if (avail >= runtime->buffer_size) {
- snd_pcm_drain_done(substream);
+ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
+ if (avail >= runtime->buffer_size) {
+ snd_pcm_drain_done(substream);
+ return -EPIPE;
+ }
+ } else if (avail > runtime->buffer_size) {
+ /* A full capture buffer remains readable while draining. */
+ __snd_pcm_xrun(substream);
return -EPIPE;
}
} else {
@@ -1918,6 +1924,30 @@ void snd_pcm_period_elapsed_under_stream_lock(struct snd_pcm_substream *substrea
}
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. Capture
+ * streams may report their final tail while DRAINING. 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))
+ return;
+ if (!snd_pcm_running(substream) &&
+ !(substream->stream == SNDRV_PCM_STREAM_CAPTURE &&
+ substream->runtime->state == SNDRV_PCM_STATE_DRAINING))
+ 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.
@@ -2405,7 +2435,11 @@ snd_pcm_sframes_t __snd_pcm_lib_xfer(struct snd_pcm_substream *substream,
offset += frames;
size -= frames;
xfer += frames;
- avail -= frames;
+ if (!is_playback &&
+ runtime->state == SNDRV_PCM_STATE_DRAINING)
+ avail = snd_pcm_avail(substream);
+ else
+ avail -= frames;
if (is_playback &&
runtime->state == SNDRV_PCM_STATE_PREPARED &&
snd_pcm_playback_hw_avail(runtime) >= (snd_pcm_sframes_t)runtime->start_threshold) {
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 02/20] ALSA: pcm: report partial progress under the stream lock
2026-11-13 6:41 ` [PATCH v32 02/20] ALSA: pcm: report partial progress under the stream lock Joonho Lee via B4 Relay
@ 2026-11-13 7:05 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:05 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> Allow a driver to publish subperiod progress under the PCM stream lock
> without generating a period timer notification. Use the ordinary pointer
> update, XRUN and wakeup path. Capture DRAINING can publish its final
> tail while playback and other inactive states retain their existing
> behavior.
>
> Handle capture DRAINING separately in the state update: a full buffer is
> still readable capture data, while progress beyond the buffer capacity
> is an overrun. Playback continues to finish draining when its buffer is
> empty.
>
> Refresh capture availability after the unlocked copy and application
> pointer commit while DRAINING. A final tail can arrive during the copy;
> subtracting only the copied frames from the earlier snapshot can
> otherwise finish draining with unread samples left in the ring.
>
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=2
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 03/20] ALSA: pcm: add an optional reset operation under the stream lock
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
2026-11-13 6:41 ` [PATCH v32 01/20] ALSA: pcm: mark stop-threshold accesses for capture policy sampling Joonho Lee via B4 Relay
2026-11-13 6:41 ` [PATCH v32 02/20] ALSA: pcm: report partial progress under the stream lock Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:51 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 04/20] ALSA: pcm: notify drivers after linked prepare completes Joonho Lee via B4 Relay
` (16 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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.
Complete appl_ptr rebasing once in the callback path. A later linked
member can sleep in prepare while the earlier member accepts SYNC_PTR; a
subsequent unlocked post_prepare assignment would overwrite that
acknowledgment. Keep explicit RESET playback silence processing and
leave prepare-time filling to the normal START path, preserving samples
written and rewound after prepare.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
include/sound/pcm.h | 21 +++++++++++++++++++++
sound/core/pcm_lib.c | 29 ++++++++++++++++++++++++-----
sound/core/pcm_native.c | 45 ++++++++++++++++++++++++++++++++++++++++-----
3 files changed, 85 insertions(+), 10 deletions(-)
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index 965be5603729..6c386d576841 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -77,6 +77,26 @@ struct snd_pcm_ops {
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 @@ static inline void snd_pcm_set_sync(struct snd_pcm_substream *substream)
{
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
index 4fb581b03480..c751f7dad1d3 100644
--- a/sound/core/pcm_lib.c
+++ b/sound/core/pcm_lib.c
@@ -1766,14 +1766,25 @@ int snd_pcm_hw_params_bits(const struct snd_pcm_hw_params *p)
}
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;
@@ -1781,6 +1792,14 @@ static int snd_pcm_lib_ioctl_reset(struct snd_pcm_substream *substream,
}
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,
void *arg)
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index a2ea44c3c1d2..a547ac720b51 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1984,11 +1984,35 @@ static int snd_pcm_pre_reset(struct snd_pcm_substream *substream,
}
}
-static int snd_pcm_do_reset(struct snd_pcm_substream *substream,
- snd_pcm_state_t state)
+static int snd_pcm_reset_substream(struct snd_pcm_substream *substream,
+ bool for_prepare)
{
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 (!for_prepare && 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);
@@ -2000,10 +2024,19 @@ static int snd_pcm_do_reset(struct snd_pcm_substream *substream,
return 0;
}
+static int snd_pcm_do_reset(struct snd_pcm_substream *substream,
+ snd_pcm_state_t state)
+{
+ return snd_pcm_reset_substream(substream, false);
+}
+
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;
guard(pcm_stream_lock_irq)(substream);
runtime->control->appl_ptr = runtime->status->hw_ptr;
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK &&
@@ -2053,14 +2086,16 @@ static int snd_pcm_do_prepare(struct snd_pcm_substream *substream,
if (err < 0)
return err;
pcm_qos_req_update(substream, period_to_usecs(substream->runtime));
- return snd_pcm_do_reset(substream, state);
+ return snd_pcm_reset_substream(substream, true);
}
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;
+ /* The reset callback path already completed this rebase under the lock. */
+ if (!substream->ops->reset)
+ runtime->control->appl_ptr = runtime->status->hw_ptr;
snd_pcm_set_state(substream, SNDRV_PCM_STATE_PREPARED);
}
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 03/20] ALSA: pcm: add an optional reset operation under the stream lock
2026-11-13 6:41 ` [PATCH v32 03/20] ALSA: pcm: add an optional reset operation " Joonho Lee via B4 Relay
@ 2026-11-13 6:51 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 6:51 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> 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.
>
> [ ... ]
> leave prepare-time filling to the normal START path, preserving samples
> written and rewound after prepare.
>
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=3
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 04/20] ALSA: pcm: notify drivers after linked prepare completes
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (2 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 03/20] ALSA: pcm: add an optional reset operation " Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:53 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 05/20] ALSA: pcm: serialize linked buffer actions Joonho Lee via B4 Relay
` (15 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
A linked prepare can finish one member before another member's driver
prepare or reset fails. Drivers that admit transfers after their own
prepare callback can then accept an ACK while the group action is still
incomplete.
Add an optional prepare_complete callback. After all sleeping prepare
and reset actions have succeeded, take the group and member PCM locks,
publish every PREPARED state, and notify opted-in drivers before
releasing those locks. Keep the buffer locks and access gates held
throughout the operation. A concurrent DROP observes the group before or
after this commit point.
On a driver error, notify only the attempted prefix, including the
failing member. On any revoked prepare, opted-in members in the affected
prefix or group return to SETUP. Preserve DISCONNECTED members and keep
SUSPENDED members suspended, but set both saved suspended-state fields
to SETUP so a later prepare can recover. Leave unreached members
untouched after a driver error. Preflight failures have no completion
callback. A successful driver action canceled by a terminal state
returns -ESTRPIPE or -EBADFD, respectively. A driver error keeps its
original return value. DISCONNECTED takes precedence when a linked group
contains both terminal states.
Do not generate a stop-timer event while closing prepare admission.
Ordinary legacy drivers keep their existing successful preparation and
pointer rebasing. The new callback runs with the group and member locks
held and must neither sleep nor acquire another PCM stream lock.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
include/sound/pcm.h | 18 ++++++
sound/core/pcm_native.c | 160 ++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 159 insertions(+), 19 deletions(-)
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index 6c386d576841..7150f8356dc8 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -77,6 +77,24 @@ struct snd_pcm_ops {
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma);
int (*ack)(struct snd_pcm_substream *substream);
+ /*
+ * Optional prepare completion notification. Called while the core holds
+ * all linked PCM stream locks, after prepare/reset actions succeed or stop
+ * at the first error. On success, the core publishes PREPARED for every
+ * member before calling this with true. If any member is DISCONNECTED or
+ * SUSPENDED, opted-in members in the affected group receive false.
+ * Nonterminal opted-in members become SETUP. DISCONNECTED members remain
+ * DISCONNECTED. SUSPENDED members remain SUSPENDED with both saved
+ * suspended-state fields set to SETUP. On a driver failure, called with false
+ * for opted-in members whose action ran, including the failing member.
+ * Members not reached and pre-action failures receive no callback. Standalone
+ * RESET does not call this callback. It must not sleep or acquire another PCM
+ * stream lock.
+ * A terminal-state cancellation returns -EBADFD for DISCONNECTED or
+ * -ESTRPIPE for SUSPENDED.
+ */
+ void (*prepare_complete)(struct snd_pcm_substream *substream,
+ bool success);
/*
* Optional RESET operation, also used after a successful prepare.
* Called with this substream's PCM lock held; atomic streams must not
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index a547ac720b51..0a8dd1bbc4c9 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1310,6 +1310,10 @@ struct action_ops {
snd_pcm_state_t state);
void (*post_action)(struct snd_pcm_substream *substream,
snd_pcm_state_t state);
+ int (*finish_action)(struct snd_pcm_substream *substream,
+ snd_pcm_state_t state, bool success,
+ struct snd_pcm_substream *failed_substream,
+ bool stream_lock);
};
/*
@@ -1350,12 +1354,19 @@ static int snd_pcm_action_group(const struct action_ops *ops,
ops->undo_action(s1, state);
}
}
+ if (ops->finish_action) {
+ ops->finish_action(substream, state, false, s,
+ stream_lock);
+ }
s = NULL; /* unlock all */
goto _unlock;
}
}
- snd_pcm_group_for_each_entry(s, substream) {
- ops->post_action(s, state);
+ if (ops->finish_action) {
+ res = ops->finish_action(substream, state, true, NULL, stream_lock);
+ } else {
+ snd_pcm_group_for_each_entry(s, substream)
+ ops->post_action(s, state);
}
_unlock:
/* unlock streams */
@@ -1379,7 +1390,8 @@ static int snd_pcm_action_group(const struct action_ops *ops,
*/
static int snd_pcm_action_single(const struct action_ops *ops,
struct snd_pcm_substream *substream,
- snd_pcm_state_t state)
+ snd_pcm_state_t state,
+ bool stream_lock)
{
int res;
@@ -1387,10 +1399,19 @@ static int snd_pcm_action_single(const struct action_ops *ops,
if (res < 0)
return res;
res = ops->do_action(substream, state);
- if (res == 0)
- ops->post_action(substream, state);
- else if (ops->undo_action)
- ops->undo_action(substream, state);
+ if (res == 0) {
+ if (ops->finish_action)
+ res = ops->finish_action(substream, state, true, NULL,
+ stream_lock);
+ else
+ ops->post_action(substream, state);
+ } else {
+ if (ops->undo_action)
+ ops->undo_action(substream, state);
+ if (ops->finish_action)
+ ops->finish_action(substream, state, false, substream,
+ stream_lock);
+ }
return res;
}
@@ -1469,7 +1490,7 @@ static int snd_pcm_action(const struct action_ops *ops,
if (group)
res = snd_pcm_action_group(ops, substream, state, true);
else
- res = snd_pcm_action_single(ops, substream, state);
+ res = snd_pcm_action_single(ops, substream, state, true);
snd_pcm_group_unref(group, substream);
return res;
}
@@ -1501,7 +1522,7 @@ static int snd_pcm_action_nonatomic(const struct action_ops *ops,
if (snd_pcm_stream_linked(substream))
res = snd_pcm_action_group(ops, substream, state, false);
else
- res = snd_pcm_action_single(ops, substream, state);
+ res = snd_pcm_action_single(ops, substream, state, false);
snd_pcm_buffer_access_unlock(substream->runtime);
return res;
}
@@ -1660,7 +1681,7 @@ EXPORT_SYMBOL(snd_pcm_stop);
int snd_pcm_drain_done(struct snd_pcm_substream *substream)
{
return snd_pcm_action_single(&snd_pcm_action_stop, substream,
- SNDRV_PCM_STATE_SETUP);
+ SNDRV_PCM_STATE_SETUP, true);
}
/**
@@ -2063,9 +2084,12 @@ static int snd_pcm_reset(struct snd_pcm_substream *substream)
static int snd_pcm_pre_prepare(struct snd_pcm_substream *substream,
snd_pcm_state_t state)
{
- snd_pcm_state_t cur_state = snd_pcm_get_state(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ snd_pcm_state_t cur_state;
int f_flags = state;
+ guard(pcm_stream_lock_irq)(substream);
+ cur_state = runtime->state;
if (cur_state == SNDRV_PCM_STATE_OPEN ||
cur_state == SNDRV_PCM_STATE_DISCONNECTED)
return -EBADFD;
@@ -2089,20 +2113,118 @@ static int snd_pcm_do_prepare(struct snd_pcm_substream *substream,
return snd_pcm_reset_substream(substream, true);
}
-static void snd_pcm_post_prepare(struct snd_pcm_substream *substream,
- snd_pcm_state_t state)
+static int snd_pcm_finish_prepare(struct snd_pcm_substream *substream,
+ snd_pcm_state_t state, bool success,
+ struct snd_pcm_substream *failed_substream,
+ bool stream_lock)
{
- struct snd_pcm_runtime *runtime = substream->runtime;
- /* The reset callback path already completed this rebase under the lock. */
- if (!substream->ops->reset)
- runtime->control->appl_ptr = runtime->status->hw_ptr;
- snd_pcm_set_state(substream, SNDRV_PCM_STATE_PREPARED);
+ struct snd_pcm_substream *s;
+ struct snd_pcm_group *group = NULL;
+ bool cancel = false;
+ int cancel_err = 0;
+ int depth = 1;
+
+ if (!stream_lock) {
+ snd_pcm_stream_lock_irq(substream);
+ group = snd_pcm_stream_group_ref(substream);
+ if (group) {
+ snd_pcm_group_for_each_entry(s, substream) {
+ if (s == substream)
+ continue;
+ if (s->pcm->nonatomic)
+ mutex_lock_nested(&s->self_group.mutex, depth);
+ else
+ spin_lock_nested(&s->self_group.lock, depth);
+ depth++;
+ }
+ }
+ }
+
+ /* Preserve terminal states instead of publishing a partial group commit. */
+ snd_pcm_group_for_each_entry(s, substream) {
+ struct snd_pcm_runtime *runtime = s->runtime;
+
+ if (runtime->state == SNDRV_PCM_STATE_DISCONNECTED) {
+ cancel = true;
+ cancel_err = -EBADFD;
+ } else if (runtime->state == SNDRV_PCM_STATE_SUSPENDED) {
+ cancel = true;
+ if (!cancel_err)
+ cancel_err = -ESTRPIPE;
+ }
+ if (!success && s == failed_substream)
+ break;
+ }
+
+ if (success && !cancel) {
+ snd_pcm_group_for_each_entry(s, substream) {
+ struct snd_pcm_runtime *runtime = s->runtime;
+
+ /* The reset callback path already completed this rebase. */
+ if (!s->ops->reset)
+ runtime->control->appl_ptr = runtime->status->hw_ptr;
+ __snd_pcm_set_state(runtime, SNDRV_PCM_STATE_PREPARED);
+ }
+ } else if (!success || cancel) {
+ snd_pcm_group_for_each_entry(s, substream) {
+ struct snd_pcm_runtime *runtime = s->runtime;
+
+ if (s->ops->prepare_complete) {
+ if (runtime->state == SNDRV_PCM_STATE_SUSPENDED) {
+ runtime->suspended_state =
+ SNDRV_PCM_STATE_SETUP;
+ runtime->status->suspended_state =
+ SNDRV_PCM_STATE_SETUP;
+ } else if (runtime->state !=
+ SNDRV_PCM_STATE_DISCONNECTED &&
+ runtime->state != SNDRV_PCM_STATE_SETUP) {
+ __snd_pcm_set_state(runtime,
+ SNDRV_PCM_STATE_SETUP);
+ }
+ }
+ if (!success && s == failed_substream)
+ break;
+ }
+ }
+
+ if (!success || cancel) {
+ snd_pcm_group_for_each_entry(s, substream) {
+ wake_up(&s->runtime->sleep);
+ wake_up(&s->runtime->tsleep);
+ if (!success && s == failed_substream)
+ break;
+ }
+ }
+
+ /* Publish all states before any driver observes completion. */
+ snd_pcm_group_for_each_entry(s, substream) {
+ if (s->ops->prepare_complete)
+ s->ops->prepare_complete(s, success && !cancel);
+ if (!success && s == failed_substream)
+ break;
+ }
+
+ if (!stream_lock) {
+ if (group) {
+ snd_pcm_group_for_each_entry(s, substream) {
+ if (s == substream)
+ continue;
+ if (s->pcm->nonatomic)
+ mutex_unlock(&s->self_group.mutex);
+ else
+ spin_unlock(&s->self_group.lock);
+ }
+ snd_pcm_group_unref(group, substream);
+ }
+ snd_pcm_stream_unlock_irq(substream);
+ }
+ return success && cancel ? cancel_err : 0;
}
static const struct action_ops snd_pcm_action_prepare = {
.pre_action = snd_pcm_pre_prepare,
.do_action = snd_pcm_do_prepare,
- .post_action = snd_pcm_post_prepare
+ .finish_action = snd_pcm_finish_prepare
};
/**
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 04/20] ALSA: pcm: notify drivers after linked prepare completes
2026-11-13 6:41 ` [PATCH v32 04/20] ALSA: pcm: notify drivers after linked prepare completes Joonho Lee via B4 Relay
@ 2026-11-13 6:53 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 6:53 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> A linked prepare can finish one member before another member's driver
> prepare or reset fails. Drivers that admit transfers after their own
> prepare callback can then accept an ACK while the group action is still
> incomplete.
>
> Add an optional prepare_complete callback. After all sleeping prepare
> and reset actions have succeeded, take the group and member PCM locks,
> publish every PREPARED state, and notify opted-in drivers before
> releasing those locks. Keep the buffer locks and access gates held
> throughout the operation. A concurrent DROP observes the group before or
> after this commit point.
>
> On a driver error, notify only the attempted prefix, including the
> [ ... ]
> pointer rebasing. The new callback runs with the group and member locks
> held and must neither sleep nor acquire another PCM stream 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=4
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 05/20] ALSA: pcm: serialize linked buffer actions
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (3 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 04/20] ALSA: pcm: notify drivers after linked prepare completes Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:51 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 06/20] ALSA: virtio: propagate playback send failures through ACK Joonho Lee via B4 Relay
` (14 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Concurrent non-atomic actions on different members of a linked PCM group
currently lock their own buffer first, then the other members. That
acquisition order can differ between callers. Sibling buffer mutexes
also do not close their buffer-access admission counters.
Close buffer access for every member before taking any buffer mutex, and
acquire those mutexes in the stable linked-list order while group
membership is protected. Release every access gate on success and on
failure, including when a member already has an active buffer access.
The group action then uses the buffers already held by its caller.
The PCM stream and group locks are not held across sleeping driver
prepare callbacks. Single-stream non-atomic actions keep their existing
buffer-access locking path.
Fixes: 3c3201f8c7bb ("ALSA: pcm: Fix races among concurrent prepare and hw_params/hw_free calls")
Fixes: bc55cfd5718c ("ALSA: pcm: Fix potential AB/BA lock with buffer_mutex and mmap_lock")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/core/pcm_native.c | 71 ++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 56 insertions(+), 15 deletions(-)
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index 0a8dd1bbc4c9..8131a7be5dfd 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -733,6 +733,44 @@ static void snd_pcm_buffer_access_unlock(struct snd_pcm_runtime *runtime)
atomic_inc(&runtime->buffer_accessing);
}
+/* Gate all linked buffers before taking their mutexes in group-list order. */
+static int snd_pcm_group_buffer_access_lock(struct snd_pcm_substream *substream)
+{
+ struct snd_pcm_substream *s, *s1;
+ int depth = 0;
+
+ snd_pcm_group_for_each_entry(s, substream) {
+ if (!atomic_dec_unless_positive(&s->runtime->buffer_accessing)) {
+ snd_pcm_group_for_each_entry(s1, substream) {
+ if (s1 == s)
+ break;
+ atomic_inc(&s1->runtime->buffer_accessing);
+ }
+ return -EBUSY;
+ }
+ }
+
+ snd_pcm_group_for_each_entry(s, substream) {
+ if (depth == 0)
+ mutex_lock(&s->runtime->buffer_mutex);
+ else
+ mutex_lock_nested(&s->runtime->buffer_mutex, depth);
+ depth++;
+ }
+
+ return 0;
+}
+
+static void snd_pcm_group_buffer_access_unlock(struct snd_pcm_substream *substream)
+{
+ struct snd_pcm_substream *s;
+
+ list_for_each_entry_reverse(s, &substream->group->substreams, link_list)
+ mutex_unlock(&s->runtime->buffer_mutex);
+ snd_pcm_group_for_each_entry(s, substream)
+ atomic_inc(&s->runtime->buffer_accessing);
+}
+
/* fill the PCM buffer with the current silence format; called from pcm_oss.c */
int snd_pcm_runtime_buffer_set_silence(struct snd_pcm_runtime *runtime)
{
@@ -1317,9 +1355,9 @@ struct action_ops {
};
/*
- * this functions is core for handling of linked stream
- * Note: the stream state might be changed also on failure
- * Note2: call with calling stream lock + link lock
+ * Handle an action on all linked streams.
+ * When stream_lock is true, the caller holds the calling stream and group
+ * locks. Otherwise, the caller holds link_rwsem and every group buffer_mutex.
*/
static int snd_pcm_action_group(const struct action_ops *ops,
struct snd_pcm_substream *substream,
@@ -1331,10 +1369,8 @@ static int snd_pcm_action_group(const struct action_ops *ops,
int res = 0, depth = 1;
snd_pcm_group_for_each_entry(s, substream) {
- if (s != substream) {
- if (!stream_lock)
- mutex_lock_nested(&s->runtime->buffer_mutex, depth);
- else if (s->pcm->nonatomic)
+ if (s != substream && stream_lock) {
+ if (s->pcm->nonatomic)
mutex_lock_nested(&s->self_group.mutex, depth);
else
spin_lock_nested(&s->self_group.lock, depth);
@@ -1371,10 +1407,8 @@ static int snd_pcm_action_group(const struct action_ops *ops,
_unlock:
/* unlock streams */
snd_pcm_group_for_each_entry(s1, substream) {
- if (s1 != substream) {
- if (!stream_lock)
- mutex_unlock(&s1->runtime->buffer_mutex);
- else if (s1->pcm->nonatomic)
+ if (s1 != substream && stream_lock) {
+ if (s1->pcm->nonatomic)
mutex_unlock(&s1->self_group.mutex);
else
spin_unlock(&s1->self_group.lock);
@@ -1512,18 +1546,25 @@ static int snd_pcm_action_nonatomic(const struct action_ops *ops,
struct snd_pcm_substream *substream,
snd_pcm_state_t state)
{
+ bool linked;
int res;
/* Guarantee the group members won't change during non-atomic action */
guard(rwsem_read)(&snd_pcm_link_rwsem);
- res = snd_pcm_buffer_access_lock(substream->runtime);
+ linked = snd_pcm_stream_linked(substream);
+ if (linked)
+ res = snd_pcm_group_buffer_access_lock(substream);
+ else
+ res = snd_pcm_buffer_access_lock(substream->runtime);
if (res < 0)
return res;
- if (snd_pcm_stream_linked(substream))
+ if (linked) {
res = snd_pcm_action_group(ops, substream, state, false);
- else
+ snd_pcm_group_buffer_access_unlock(substream);
+ } else {
res = snd_pcm_action_single(ops, substream, state, false);
- snd_pcm_buffer_access_unlock(substream->runtime);
+ snd_pcm_buffer_access_unlock(substream->runtime);
+ }
return res;
}
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 05/20] ALSA: pcm: serialize linked buffer actions
2026-11-13 6:41 ` [PATCH v32 05/20] ALSA: pcm: serialize linked buffer actions Joonho Lee via B4 Relay
@ 2026-11-13 6:51 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 6:51 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> Concurrent non-atomic actions on different members of a linked PCM group
> currently lock their own buffer first, then the other members. That
> acquisition order can differ between callers. Sibling buffer mutexes
> also do not close their buffer-access admission counters.
>
> Close buffer access for every member before taking any buffer mutex, and
> acquire those mutexes in the stable linked-list order while group
> membership is protected. Release every access gate on success and on
> failure, including when a member already has an active buffer access.
> The group action then uses the buffers already held by its caller.
>
> The PCM stream and group locks are not held across sleeping driver
> prepare callbacks. Single-stream non-atomic actions keep their existing
> buffer-access locking path.
>
> Fixes: 3c3201f8c7bb ("ALSA: pcm: Fix races among concurrent prepare and hw_params/hw_free calls")
> Fixes: bc55cfd5718c ("ALSA: pcm: Fix potential AB/BA lock with buffer_mutex and mmap_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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=5
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 06/20] ALSA: virtio: propagate playback send failures through ACK
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (4 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 05/20] ALSA: pcm: serialize linked buffer actions Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:08 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 07/20] ALSA: virtio: Recover PCM state after a failed start Joonho Lee via B4 Relay
` (13 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Propagate playback submission failures through ACK and commit indirect
accounting only after transport submission succeeds. Keep the PCM state
and buffer-access checks synchronized with playback transfer admission.
The existing capture ACK callback and initial queue reservation behavior
remain together until the request-owned capture conversion in patch 12.
That conversion introduces RX reservation, validation and refill as one
contract.
Fixes: fe981e67568c ("ALSA: virtio: use ack callback")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_pcm.h | 6 ++
sound/virtio/virtio_pcm_msg.c | 12 +++
sound/virtio/virtio_pcm_ops.c | 219 ++++++++++++++++++++++++++++++++++++++----
3 files changed, 219 insertions(+), 18 deletions(-)
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 8837dfc12828..34d494920913 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -33,6 +33,10 @@ struct virtio_pcm_msg;
* @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.
+ * @prepared: True after device PREPARE and the core pointer reset succeed;
+ * protected by @lock.
+ * @preparing: ACK gate through teardown and prepare's core reset;
+ * 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 +60,8 @@ struct virtio_pcm_substream {
bool xfer_enabled;
bool xfer_xrun;
bool stopped;
+ bool prepared;
+ bool preparing;
bool suspended;
struct virtio_pcm_msg **msgs;
unsigned int nmsgs;
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
index 72103f606716..5371f4985fd0 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -260,6 +260,18 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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
index 1091100a4c98..839f71b950e3 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -96,6 +96,38 @@ static const struct virtsnd_a2v_rate g_a2v_rate_map[] = {
static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream);
+/**
+ * virtsnd_pcm_close_ack() - Update PCM prepare admission state.
+ * @substream: Kernel ALSA substream.
+ * @preparing: True while driver prepare and the core pointer reset are active.
+ *
+ * Context: Process context.
+ */
+static void virtsnd_pcm_close_ack(struct snd_pcm_substream *substream,
+ bool preparing)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->prepared = false;
+ vss->xfer_enabled = false;
+ vss->preparing = preparing;
+ }
+}
+
+/* Keep completed RX data valid while RELEASE drains the device queue. */
+static void virtsnd_pcm_block_ack(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->preparing = true;
+ vss->xfer_enabled = false;
+ }
+}
+
/**
* virtsnd_pcm_open() - Open the PCM substream.
* @substream: Kernel ALSA substream.
@@ -115,7 +147,8 @@ static int virtsnd_pcm_open(struct snd_pcm_substream *substream)
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;
/*
@@ -219,6 +252,8 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
struct virtio_device *vdev = vss->snd->vdev;
int rc;
+ virtsnd_pcm_close_ack(substream, false);
+
if (virtsnd_pcm_msg_pending_num(vss)) {
dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n",
vss->sid);
@@ -254,6 +289,8 @@ static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ virtsnd_pcm_close_ack(substream, 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);
@@ -262,17 +299,18 @@ static int virtsnd_pcm_hw_free(struct snd_pcm_substream *substream)
}
/**
- * virtsnd_pcm_prepare() - Prepare the PCM substream.
+ * virtsnd_pcm_prepare_device() - Prepare the PCM substream on the device.
* @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_device(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;
+ int rc;
if (!vss->suspended) {
if (virtsnd_pcm_msg_pending_num(vss)) {
@@ -287,16 +325,14 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
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);
+ runtime->channels,
+ runtime->format, runtime->rate);
if (rc)
return rc;
}
- vss->xfer_xrun = false;
vss->suspended = false;
vss->msg_count = 0;
virtsnd_pcm_msg_reset(vss);
@@ -311,7 +347,33 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
if (!msg)
return -ENOMEM;
- return virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
+ if (rc)
+ return rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->xfer_xrun = false;
+ }
+ return 0;
+}
+
+/* Keep ACK closed through the core's locked pointer reset. */
+static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
+{
+ virtsnd_pcm_close_ack(substream, true);
+ return virtsnd_pcm_prepare_device(substream);
+}
+
+/* Complete admission only after the linked core prepare action ends. */
+static void virtsnd_pcm_prepare_complete(
+ struct snd_pcm_substream *substream, bool success)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ vss->prepared = success;
+ vss->preparing = false;
+ }
}
/**
@@ -338,6 +400,8 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
scoped_guard(spinlock_irqsave, &queue->lock) {
guard(spinlock)(&vss->lock);
+ if (vss->preparing || !vss->prepared)
+ return -EBADFD;
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
if (rc)
@@ -356,10 +420,12 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
return virtsnd_ctl_msg_send_sync(snd, msg);
case SNDRV_PCM_TRIGGER_SUSPEND:
- vss->suspended = true;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->suspended = true;
fallthrough;
case SNDRV_PCM_TRIGGER_STOP:
- vss->stopped = true;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->stopped = true;
fallthrough;
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
scoped_guard(spinlock_irqsave, &vss->lock) {
@@ -393,11 +459,16 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
struct virtio_snd *snd = vss->snd;
struct virtio_snd_msg *msg;
unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ bool stopped;
int rc;
+ virtsnd_pcm_block_ack(substream);
+
cancel_work_sync(&vss->elapsed_period);
- if (!vss->stopped)
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ stopped = vss->stopped;
+ if (!stopped)
return 0;
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
@@ -425,7 +496,8 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
return !rc ? -ETIMEDOUT : rc;
}
- vss->stopped = false;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->stopped = false;
return 0;
}
@@ -443,6 +515,10 @@ virtsnd_pcm_pb_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ if (vss->xfer_xrun)
+ return SNDRV_PCM_POS_XRUN;
+
return snd_pcm_indirect_playback_pointer(substream,
&vss->pcm_indirect,
vss->hw_ptr);
@@ -461,41 +537,144 @@ virtsnd_pcm_cp_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ if (vss->xfer_xrun)
+ return SNDRV_PCM_POS_XRUN;
+
return snd_pcm_indirect_capture_pointer(substream,
&vss->pcm_indirect,
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);
+ struct virtsnd_pcm_transfer *transfer =
+ container_of(rec, struct virtsnd_pcm_transfer, indirect);
- virtsnd_pcm_msg_send(vss, rec->sw_data, bytes);
+ 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->xfer_xrun = true;
+ vss->substream->runtime->stop_operating = true;
+}
+
+/* The caller holds the PCM lock, also used by DROP and state transitions. */
+static int virtsnd_pcm_ack_state(struct snd_pcm_substream *substream)
+{
+ switch (substream->runtime->state) {
+ case SNDRV_PCM_STATE_XRUN:
+ /* The member is already XRUN; avoid stopping its linked group twice. */
+ return -EBADFD;
+ case SNDRV_PCM_STATE_PREPARED:
+ case SNDRV_PCM_STATE_RUNNING:
+ case SNDRV_PCM_STATE_PAUSED:
+ case SNDRV_PCM_STATE_DRAINING:
+ if (atomic_read(&substream->runtime->buffer_accessing) < 0)
+ return -EBADFD;
+ return 0;
+ default:
+ return -EBADFD;
+ }
}
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);
+ struct virtsnd_pcm_transfer transfer = {};
+ int rc;
- guard(spinlock_irqsave)(&queue->lock);
- guard(spinlock)(&vss->lock);
+ rc = virtsnd_pcm_ack_state(substream);
+ if (rc)
+ return rc;
+
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ if (vss->preparing || !vss->prepared)
+ return -EBADFD;
+ if (vss->stopped)
+ return -EBADFD;
+ if (vss->xfer_xrun) {
+ rc = -EPIPE;
+ } else {
+ transfer.indirect = vss->pcm_indirect;
+ 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);
+ return -EPIPE;
+ }
- return snd_pcm_indirect_playback_transfer(substream, &vss->pcm_indirect,
- virtsnd_pcm_trans_copy);
+ return 0;
+}
+
+static void virtsnd_pcm_legacy_capture_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);
}
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);
+ int rc;
+ rc = virtsnd_pcm_ack_state(substream);
+ if (rc)
+ return rc;
guard(spinlock_irqsave)(&queue->lock);
guard(spinlock)(&vss->lock);
+ if (vss->preparing || !vss->prepared)
+ return -EBADFD;
+ if (vss->xfer_xrun)
+ return -EPIPE;
+ if (vss->stopped &&
+ substream->runtime->state != SNDRV_PCM_STATE_DRAINING)
+ return -EBADFD;
return snd_pcm_indirect_capture_transfer(substream, &vss->pcm_indirect,
- virtsnd_pcm_trans_copy);
+ virtsnd_pcm_legacy_capture_copy);
+}
+
+/**
+ * virtsnd_pcm_reset() - Reset PCM pointers.
+ * @substream: Kernel ALSA substream.
+ *
+ * Context: PCM stream lock held.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_pcm_reset(struct snd_pcm_substream *substream)
+{
+ return snd_pcm_lib_reset(substream);
}
/* PCM substream operators map. */
@@ -507,6 +686,8 @@ const struct snd_pcm_ops virtsnd_pcm_ops[] = {
.hw_params = virtsnd_pcm_hw_params,
.hw_free = virtsnd_pcm_hw_free,
.prepare = virtsnd_pcm_prepare,
+ .prepare_complete = virtsnd_pcm_prepare_complete,
+ .reset = virtsnd_pcm_reset,
.trigger = virtsnd_pcm_trigger,
.sync_stop = virtsnd_pcm_sync_stop,
.pointer = virtsnd_pcm_pb_pointer,
@@ -519,6 +700,8 @@ const struct snd_pcm_ops virtsnd_pcm_ops[] = {
.hw_params = virtsnd_pcm_hw_params,
.hw_free = virtsnd_pcm_hw_free,
.prepare = virtsnd_pcm_prepare,
+ .prepare_complete = virtsnd_pcm_prepare_complete,
+ .reset = virtsnd_pcm_reset,
.trigger = virtsnd_pcm_trigger,
.sync_stop = virtsnd_pcm_sync_stop,
.pointer = virtsnd_pcm_cp_pointer,
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 07/20] ALSA: virtio: Recover PCM state after a failed start
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (5 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 06/20] ALSA: virtio: propagate playback send failures through ACK Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:08 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 08/20] ALSA: virtio: Retain queued messages through device reset Joonho Lee via B4 Relay
` (12 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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.
Latch a transfer XRUN in both playback and capture, and schedule the
existing period worker so a running or draining core stream sees the
pointer error. This also retires a failed playback START during drain,
where drain-init can already have advanced the core state to DRAINING.
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>
---
sound/virtio/virtio_pcm.h | 6 ++-
sound/virtio/virtio_pcm_ops.c | 109 ++++++++++++++++++++++++++++++++----------
2 files changed, 87 insertions(+), 28 deletions(-)
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 34d494920913..a5260ae9dd78 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -26,13 +26,13 @@ struct virtio_pcm_msg;
* @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.
* @prepared: True after device PREPARE and the core pointer reset succeed;
* protected by @lock.
* @preparing: ACK gate through teardown and prepare's core reset;
@@ -62,6 +62,8 @@ struct virtio_pcm_substream {
bool stopped;
bool prepared;
bool preparing;
+ /* 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
index 839f71b950e3..feb2a5bcee87 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -116,6 +116,17 @@ static void virtsnd_pcm_close_ack(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->xfer_xrun = true;
+ }
+ vss->substream->runtime->stop_operating = true;
+ schedule_work(&vss->elapsed_period);
+}
+
/* Keep completed RX data valid while RELEASE drains the device queue. */
static void virtsnd_pcm_block_ack(struct snd_pcm_substream *substream)
{
@@ -148,7 +159,7 @@ static int virtsnd_pcm_open(struct snd_pcm_substream *substream)
SNDRV_PCM_HW_PARAM_PERIODS);
scoped_guard(spinlock_irqsave, &vss->lock)
- vss->stopped = !!vss->msg_count;
+ vss->stopped |= !!vss->msg_count;
vss->suspended = false;
/*
@@ -250,15 +261,28 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
{
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;
virtsnd_pcm_close_ack(substream, false);
- 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);
+ scoped_guard(pcm_stream_lock_irqsave, substream)
+ substream->runtime->stop_operating = true;
+ return -EBADFD;
+ }
rc = virtsnd_pcm_dev_set_params(vss, params_buffer_bytes(hw_params),
params_period_bytes(hw_params),
@@ -310,8 +334,19 @@ static int virtsnd_pcm_prepare_device(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 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);
+ scoped_guard(pcm_stream_lock_irqsave, substream)
+ substream->runtime->stop_operating = true;
+ return -EBADFD;
+ }
+
if (!vss->suspended) {
if (virtsnd_pcm_msg_pending_num(vss)) {
dev_err(&vdev->dev, "SID %u: invalid I/O queue state\n",
@@ -353,6 +388,7 @@ static int virtsnd_pcm_prepare_device(struct snd_pcm_substream *substream)
scoped_guard(spinlock_irqsave, &vss->lock) {
vss->xfer_xrun = false;
+ vss->release_required = true;
}
return 0;
}
@@ -402,33 +438,40 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
guard(spinlock)(&vss->lock);
if (vss->preparing || !vss->prepared)
return -EBADFD;
+ 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)
- return rc;
- vss->xfer_enabled = true;
+ if (!rc)
+ vss->xfer_enabled = true;
+ }
+ if (rc) {
+ virtsnd_pcm_start_failed(vss);
+ return rc;
}
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:
- scoped_guard(spinlock_irqsave, &vss->lock)
- vss->suspended = true;
+ vss->suspended = true;
fallthrough;
case SNDRV_PCM_TRIGGER_STOP:
- scoped_guard(spinlock_irqsave, &vss->lock)
- 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;
}
@@ -459,26 +502,38 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
struct virtio_snd *snd = vss->snd;
struct virtio_snd_msg *msg;
unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
- bool stopped;
+ bool need_release;
int rc;
virtsnd_pcm_block_ack(substream);
+ 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);
- scoped_guard(spinlock_irqsave, &vss->lock)
- stopped = vss->stopped;
- if (!stopped)
- return 0;
+ if (need_release) {
+ msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
+ GFP_KERNEL);
+ if (!msg) {
+ scoped_guard(pcm_stream_lock_irqsave, substream)
+ substream->runtime->stop_operating = true;
+ return -ENOMEM;
+ }
- 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) {
+ scoped_guard(pcm_stream_lock_irqsave, substream)
+ substream->runtime->stop_operating = true;
+ return rc;
+ }
- rc = virtsnd_ctl_msg_send_sync(snd, msg);
- if (rc)
- return rc;
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->release_required = false;
+ }
/*
* The spec states that upon receipt of the RELEASE command "the device
@@ -492,6 +547,8 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
if (rc <= 0) {
dev_warn(&snd->vdev->dev, "SID %u: failed to flush I/O queue\n",
vss->sid);
+ scoped_guard(pcm_stream_lock_irqsave, substream)
+ substream->runtime->stop_operating = true;
return !rc ? -ETIMEDOUT : rc;
}
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 07/20] ALSA: virtio: Recover PCM state after a failed start
2026-11-13 6:41 ` [PATCH v32 07/20] ALSA: virtio: Recover PCM state after a failed start Joonho Lee via B4 Relay
@ 2026-11-13 7:08 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:08 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> 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.
>
> Latch a transfer XRUN in both playback and capture, and schedule the
> existing period worker so a running or draining core stream sees the
> pointer error. This also retires a failed playback START during drain,
> where drain-init can already have advanced the core state to DRAINING.
>
> [ ... ]
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=7
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 08/20] ALSA: virtio: Retain queued messages through device reset
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (6 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 07/20] ALSA: virtio: Recover PCM state after a failed start Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:01 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 09/20] ALSA: virtio: bound PCM transfers and retain DMA buffers Joonho Lee via B4 Relay
` (11 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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.
Protect event callback admission with the event queue lock. Disabling
events waits for an active dispatch, and later callbacks see a closed
gate before ALSA objects are freed. Enable and pending-event draining
share the same lock.
Reclaim pending event buffers after a failed probe without dispatching
notifications into ALSA objects that have not been constructed. Record
the queue's prior enabled state under its lock before closing admission.
Track successful virtqueue initialization so teardown and failed restore
cleanup call del_vqs() only for an initialized queue set.
Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_card.c | 100 ++++++++++++++++++++++++++++++++++--------
sound/virtio/virtio_card.h | 6 +++
sound/virtio/virtio_ctl_msg.c | 64 ++++++++++++++++++++++++---
sound/virtio/virtio_ctl_msg.h | 4 ++
sound/virtio/virtio_pcm.h | 2 +
sound/virtio/virtio_pcm_msg.c | 42 ++++++++++++++++--
6 files changed, 188 insertions(+), 30 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
index 6f35276416fe..fe31eb7805d8 100644
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -70,6 +70,22 @@ static void virtsnd_event_dispatch(struct virtio_snd *snd,
}
}
+static void virtsnd_event_drain_locked(struct virtio_snd *snd,
+ struct virtqueue *vqueue)
+{
+ struct virtio_snd_event *event;
+ u32 length;
+
+ lockdep_assert_held(&virtsnd_event_queue(snd)->lock);
+ do {
+ virtqueue_disable_cb(vqueue);
+ while ((event = virtqueue_get_buf(vqueue, &length))) {
+ virtsnd_event_dispatch(snd, event);
+ virtsnd_event_send(vqueue, event, true, GFP_ATOMIC);
+ }
+ } while (!virtqueue_enable_cb(vqueue));
+}
+
/**
* virtsnd_event_notify_cb() - Dispatch all reported events from the event queue.
* @vqueue: Underlying event virtqueue.
@@ -83,17 +99,11 @@ static void virtsnd_event_notify_cb(struct virtqueue *vqueue)
{
struct virtio_snd *snd = vqueue->vdev->priv;
struct virtio_snd_queue *queue = virtsnd_event_queue(snd);
- struct virtio_snd_event *event;
- u32 length;
guard(spinlock_irqsave)(&queue->lock);
- do {
- virtqueue_disable_cb(vqueue);
- while ((event = virtqueue_get_buf(vqueue, &length))) {
- virtsnd_event_dispatch(snd, event);
- virtsnd_event_send(vqueue, event, true, GFP_ATOMIC);
- }
- } while (!virtqueue_enable_cb(vqueue));
+ if (queue->vqueue != vqueue || !snd->event_vq_enabled)
+ return;
+ virtsnd_event_drain_locked(snd, vqueue);
}
/**
@@ -131,6 +141,7 @@ static int virtsnd_find_vqs(struct virtio_snd *snd)
for (i = 0; i < VIRTIO_SND_VQ_MAX; ++i)
snd->queues[i].vqueue = vqs[i];
+ snd->vqs_initialized = true;
/* Allocate events and populate the event queue */
virtqueue_disable_cb(vqs[VIRTIO_SND_VQ_EVENT]);
@@ -158,8 +169,12 @@ static void virtsnd_enable_event_vq(struct virtio_snd *snd)
{
struct virtio_snd_queue *queue = virtsnd_event_queue(snd);
+ guard(spinlock_irqsave)(&queue->lock);
+ if (!queue->vqueue)
+ return;
+ snd->event_vq_enabled = true;
if (!virtqueue_enable_cb(queue->vqueue))
- virtsnd_event_notify_cb(queue->vqueue);
+ virtsnd_event_drain_locked(snd, queue->vqueue);
}
/**
@@ -172,13 +187,19 @@ static void virtsnd_disable_event_vq(struct virtio_snd *snd)
{
struct virtio_snd_queue *queue = virtsnd_event_queue(snd);
struct virtio_snd_event *event;
+ bool dispatch_events;
u32 length;
+ guard(spinlock_irqsave)(&queue->lock);
+ dispatch_events = snd->event_vq_enabled;
+ snd->event_vq_enabled = false;
if (queue->vqueue) {
- guard(spinlock_irqsave)(&queue->lock);
virtqueue_disable_cb(queue->vqueue);
- while ((event = virtqueue_get_buf(queue->vqueue, &length)))
- virtsnd_event_dispatch(snd, event);
+ while ((event = virtqueue_get_buf(queue->vqueue, &length))) {
+ /* Reclaim used buffers even when failed probe cannot dispatch. */
+ if (dispatch_events)
+ virtsnd_event_dispatch(snd, event);
+ }
}
}
@@ -337,6 +358,43 @@ static int virtsnd_probe(struct virtio_device *vdev)
return rc;
}
+/**
+ * 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);
+ if (i == VIRTIO_SND_VQ_EVENT)
+ snd->event_vq_enabled = false;
+ /* IRQ callbacks must never see a detached ring as live. */
+ queue->vqueue = NULL;
+ spin_unlock_irqrestore(&queue->lock, flags);
+ }
+
+ if (snd->vqs_initialized) {
+ vdev->config->del_vqs(vdev);
+ snd->vqs_initialized = false;
+ }
+}
+
/**
* virtsnd_remove() - Remove VirtIO and ALSA devices.
* @vdev: VirtIO parent device.
@@ -349,13 +407,14 @@ static void virtsnd_remove(struct virtio_device *vdev)
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 +442,8 @@ static int virtsnd_freeze(struct virtio_device *vdev)
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);
@@ -408,10 +467,13 @@ static int virtsnd_restore(struct virtio_device *vdev)
int rc;
rc = virtsnd_find_vqs(snd);
- if (rc)
+ if (rc) {
+ virtsnd_reset_vqs(snd);
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
index 3ceee4e416fc..a733fa684c86 100644
--- a/sound/virtio/virtio_card.h
+++ b/sound/virtio/virtio_card.h
@@ -45,8 +45,11 @@ struct virtio_kctl {
* struct virtio_snd - VirtIO sound card device.
* @vdev: Underlying virtio device.
* @queues: Virtqueue wrappers.
+ * @vqs_initialized: Virtqueues allocated by virtio_find_vqs().
* @card: ALSA sound card.
* @ctl_msgs: Pending control request list.
+ * @ctl_msgs_blocked: Control requests are blocked during teardown or suspend.
+ * @event_vq_enabled: Event callbacks may dispatch, protected by the event queue lock.
* @event_msgs: Device events.
* @pcm_list: VirtIO PCM device list.
* @jacks: VirtIO jacks.
@@ -62,8 +65,11 @@ struct virtio_kctl {
struct virtio_snd {
struct virtio_device *vdev;
struct virtio_snd_queue queues[VIRTIO_SND_VQ_MAX];
+ bool vqs_initialized;
struct snd_card *card;
struct list_head ctl_msgs;
+ bool ctl_msgs_blocked;
+ bool event_vq_enabled;
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
index 6433c870f271..6093298cdf5a 100644
--- 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 virtio_snd_msg {
struct list_head list;
struct completion notify;
refcount_t ref_count;
+ bool cancelled;
};
/**
@@ -100,7 +102,7 @@ struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size,
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 virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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);
}
}
@@ -169,9 +177,6 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
goto on_exit;
}
- if (notify)
- virtqueue_notify(queue->vqueue);
-
if (nowait)
goto on_exit;
@@ -187,6 +192,11 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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;
@@ -227,9 +237,33 @@ void virtsnd_ctl_msg_complete(struct virtio_snd_msg *msg)
* 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);
@@ -243,6 +277,20 @@ void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd)
}
}
+/**
+ * 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;
+}
+
/**
* virtsnd_ctl_query_info() - Query the item configuration from the device.
* @snd: VirtIO sound device.
@@ -295,6 +343,8 @@ void virtsnd_ctl_notify_cb(struct virtqueue *vqueue)
u32 length;
guard(spinlock_irqsave)(&queue->lock);
+ if (queue->vqueue != vqueue)
+ return;
do {
virtqueue_disable_cb(vqueue);
while ((msg = virtqueue_get_buf(vqueue, &length)))
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
index 7f4db044f28e..d917435409d0 100644
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -68,6 +68,10 @@ static inline int virtsnd_ctl_msg_send_async(struct virtio_snd *snd,
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
index a5260ae9dd78..39ba6a48f90a 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -128,6 +128,8 @@ void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss);
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
index 5371f4985fd0..59ad99e21e89 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -307,6 +307,35 @@ unsigned int virtsnd_pcm_msg_pending_num(struct virtio_pcm_substream *vss)
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);
+}
+
/**
* virtsnd_pcm_msg_complete() - Complete an I/O message.
* @msg: I/O message.
@@ -367,16 +396,21 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
/**
* virtsnd_pcm_notify_cb() - Process all completed I/O messages.
- * @queue: Underlying tx/rx virtqueue.
+ * @queue: Underlying tx/rx queue wrapper.
+ * @vqueue: Queue that raised this callback.
*
* Context: Interrupt context. Takes and releases the tx/rx queue spinlock.
*/
-static inline void virtsnd_pcm_notify_cb(struct virtio_snd_queue *queue)
+static inline void
+virtsnd_pcm_notify_cb(struct virtio_snd_queue *queue, struct virtqueue *vqueue)
{
struct virtio_pcm_msg *msg;
u32 written_bytes;
guard(spinlock_irqsave)(&queue->lock);
+ /* Reset can detach the queue before the transport drains its IRQs. */
+ if (queue->vqueue != vqueue)
+ return;
do {
virtqueue_disable_cb(queue->vqueue);
while ((msg = virtqueue_get_buf(queue->vqueue, &written_bytes)))
@@ -394,7 +428,7 @@ void virtsnd_pcm_tx_notify_cb(struct virtqueue *vqueue)
{
struct virtio_snd *snd = vqueue->vdev->priv;
- virtsnd_pcm_notify_cb(virtsnd_tx_queue(snd));
+ virtsnd_pcm_notify_cb(virtsnd_tx_queue(snd), vqueue);
}
/**
@@ -407,7 +441,7 @@ void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue)
{
struct virtio_snd *snd = vqueue->vdev->priv;
- virtsnd_pcm_notify_cb(virtsnd_rx_queue(snd));
+ virtsnd_pcm_notify_cb(virtsnd_rx_queue(snd), vqueue);
}
/**
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 08/20] ALSA: virtio: Retain queued messages through device reset
2026-11-13 6:41 ` [PATCH v32 08/20] ALSA: virtio: Retain queued messages through device reset Joonho Lee via B4 Relay
@ 2026-11-13 7:01 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:01 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> 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.
>
> Protect event callback admission with the event queue lock. Disabling
> events waits for an active dispatch, and later callbacks see a closed
> gate before ALSA objects are freed. Enable and pending-event draining
> share the same lock.
>
> Reclaim pending event buffers after a failed probe without dispatching
> notifications into ALSA objects that have not been constructed. Record
> the queue's prior enabled state under its lock before closing admission.
>
> Track successful virtqueue initialization so teardown and failed restore
> cleanup call del_vqs() only for an initialized queue set.
>
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=8
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 09/20] ALSA: virtio: bound PCM transfers and retain DMA buffers
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (7 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 08/20] ALSA: virtio: Retain queued messages through device reset Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:11 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 10/20] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee via B4 Relay
` (10 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Validate ACK geometry, conversion bounds and queued-byte accounting
before submitting transfers. Retain the backing DMA pages until message
retirement. Check RX status and upper payload bounds while accepting
bounded short responses, including at STOP and PAUSE. Preserve the
legacy RX completion and ACK policy until the request-owned conversion,
including the legacy handling of empty PAUSE returns. The fixed-offset
layout still has short-response data gaps; request-owned capture and its
queue/accounting contract arrive together in patch 12. Non-MMU copies
use only the validated returned payload length.
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>
---
sound/virtio/virtio_pcm.h | 11 ++
sound/virtio/virtio_pcm_msg.c | 300 ++++++++++++++++++++++++++++++++++++------
sound/virtio/virtio_pcm_ops.c | 113 +++++++++++++++-
3 files changed, 383 insertions(+), 41 deletions(-)
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 39ba6a48f90a..b75961c705cc 100644
--- 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.
@@ -42,6 +43,10 @@ struct virtio_pcm_msg;
* @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 {
@@ -68,6 +73,12 @@ struct virtio_pcm_substream {
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
index 59ad99e21e89..f8d191952d41 100644
--- 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 of the fixed PCM ring slice for ordinary I/O.
* @sgs: Payload scatter-gather table.
*/
struct virtio_pcm_msg {
@@ -20,6 +24,7 @@ struct virtio_pcm_msg {
struct virtio_snd_pcm_xfer xfer;
struct virtio_snd_pcm_status status;
size_t length;
+ size_t data_offset;
struct scatterlist sgs[];
};
@@ -37,9 +42,8 @@ enum pcm_msg_sg_index {
};
/**
- * 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,15 +51,19 @@ enum pcm_msg_sg_index {
*/
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)
pg_length = length;
@@ -75,11 +83,80 @@ static int virtsnd_pcm_sg_num(u8 *data, unsigned int length)
return num;
}
+#ifdef CONFIG_MMU
/**
- * virtsnd_pcm_sg_from() - Build sg-list from vmalloc'ed buffer.
+ * 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 +209,97 @@ static void virtsnd_pcm_sg_from(struct scatterlist *sgs, int nsgs, u8 *data,
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;
- vss->msgs = kzalloc_objs(*vss->msgs, periods);
- if (!vss->msgs)
+ 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) {
+ 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));
+ msg->data_offset = (size_t)period_bytes * i;
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 +315,13 @@ void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss)
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 +373,15 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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];
@@ -249,6 +401,33 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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,
GFP_ATOMIC);
@@ -356,42 +535,83 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
size_t written_bytes)
{
struct virtio_pcm_substream *vss = msg->substream;
+ bool capture_invalid = false;
/*
- * hw_ptr always indicates the buffer position of the first I/O message
- * in the virtqueue. Therefore, on each completion of an I/O message,
- * the hw_ptr value is unconditionally advanced.
+ * Completed requests can return captured data after STOP while the PCM
+ * buffer remains prepared. Once preparation ends, completions only retire
+ * queue ownership.
*/
guard(spinlock)(&vss->lock);
- /*
- * If the capture substream returned an incorrect status, then just
- * increase the hw_ptr by the message size.
- */
- if (vss->direction == SNDRV_PCM_STREAM_PLAYBACK ||
- written_bytes <= sizeof(msg->status))
- vss->hw_ptr += msg->length;
- else
- vss->hw_ptr += written_bytes - sizeof(msg->status);
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ (!vss->prepared || vss->xfer_xrun ||
+ (vss->stopped && written_bytes <= sizeof(msg->status)))) {
+ msg->length = 0;
+ vss->msg_count--;
+ if (!vss->msg_count)
+ wake_up_all(&vss->msg_empty);
+ return;
+ }
- if (vss->hw_ptr >= vss->buffer_bytes)
- vss->hw_ptr -= vss->buffer_bytes;
+ /* Validate the returned payload before advancing the capture pointer. */
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ (written_bytes < sizeof(msg->status) ||
+ le32_to_cpu(msg->status.status) != VIRTIO_SND_S_OK ||
+ written_bytes - sizeof(msg->status) > msg->length)) {
+ dev_err_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: invalid capture position, status or length\n",
+ vss->sid);
+ vss->xfer_xrun = true;
+ capture_invalid = true;
+ }
+#ifndef CONFIG_MMU
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->prepared && !capture_invalid &&
+ written_bytes > sizeof(msg->status)) {
+ struct snd_pcm_runtime *runtime =
+ vss->substream ? vss->substream->runtime : NULL;
+
+ if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
+ msg->data_offset > runtime->dma_bytes ||
+ written_bytes - sizeof(msg->status) > runtime->dma_bytes - msg->data_offset ||
+ msg->data_offset > vss->owned_dma_bytes ||
+ written_bytes - sizeof(msg->status) > 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);
+ vss->xfer_xrun = true;
+ capture_invalid = true;
+ } else {
+ memcpy(runtime->dma_area + msg->data_offset,
+ vss->owned_dma_area + msg->data_offset,
+ written_bytes - sizeof(msg->status));
+ }
+ }
+#endif
+ /* Publish pointer progress only for a complete TX or valid RX request. */
+ if (!capture_invalid) {
+ vss->hw_ptr += vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ written_bytes > sizeof(msg->status) ?
+ written_bytes - sizeof(msg->status) : msg->length;
+ if (vss->hw_ptr >= vss->buffer_bytes)
+ vss->hw_ptr -= vss->buffer_bytes;
+ }
msg->length = 0;
-
- vss->xfer_xrun = false;
vss->msg_count--;
if (vss->xfer_enabled) {
- struct snd_pcm_runtime *runtime = vss->substream->runtime;
-
- runtime->delay =
- bytes_to_frames(runtime,
- le32_to_cpu(msg->status.latency_bytes));
+ if (!capture_invalid) {
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ 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);
}
+ 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
index feb2a5bcee87..f40a9f3cd2a3 100644
--- 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:
*
@@ -262,11 +264,22 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
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;
virtsnd_pcm_close_ack(substream, false);
+ /* 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;
stopped = vss->stopped;
@@ -608,6 +621,96 @@ struct virtsnd_pcm_transfer {
int error;
};
+/* Caller holds @vss->lock and the PCM stream lock. */
+static int virtsnd_pcm_playback_ack_validate(struct virtio_pcm_substream *vss,
+ struct snd_pcm_runtime *runtime,
+ const struct snd_pcm_indirect *rec)
+{
+ snd_pcm_uframes_t appl_ptr;
+ snd_pcm_uframes_t appl_diff;
+ snd_pcm_uframes_t signed_max;
+ snd_pcm_uframes_t back;
+ u64 buffer_bits;
+ u64 appl_bits;
+ size_t appl_bytes;
+ s64 queued_bytes;
+ s64 sw_ready_total;
+ s64 total_bytes;
+ s64 int_max;
+ unsigned int hw_queue_size;
+
+ if (!runtime->control || !runtime->boundary ||
+ runtime->control->appl_ptr >= runtime->boundary ||
+ rec->appl_ptr >= runtime->boundary)
+ return -EPIPE;
+
+ /* Keep the signed frame conversion used by frames_to_bytes() in range. */
+ signed_max = (~(snd_pcm_uframes_t)0) >> 1;
+ if (runtime->boundary > signed_max ||
+ runtime->buffer_size > signed_max || !runtime->frame_bits ||
+ check_mul_overflow((u64)runtime->buffer_size,
+ (u64)runtime->frame_bits, &buffer_bits) ||
+ buffer_bits > signed_max || buffer_bits % 8 ||
+ buffer_bits / 8 != vss->buffer_bytes)
+ return -EPIPE;
+
+ /* Match the indirect helper's appl_ptr boundary-wrap calculation. */
+ appl_ptr = runtime->control->appl_ptr;
+ if (appl_ptr >= rec->appl_ptr) {
+ appl_diff = appl_ptr - rec->appl_ptr;
+ } else {
+ back = rec->appl_ptr - appl_ptr;
+ if (back <= runtime->boundary / 2)
+ return -EPIPE;
+ appl_diff = runtime->boundary - back;
+ }
+
+ if (appl_diff > runtime->buffer_size)
+ return -EPIPE;
+ /* The indirect helper advances appl_ptr even if frames_to_bytes truncates. */
+ if (!runtime->min_align || appl_diff % runtime->min_align)
+ return -EBADFD;
+ if (check_mul_overflow((u64)appl_diff,
+ (u64)runtime->frame_bits, &appl_bits) ||
+ appl_bits % 8)
+ return -EPIPE;
+
+ if (!rec->hw_buffer_size || !rec->sw_buffer_size ||
+ rec->hw_buffer_size != vss->buffer_bytes ||
+ rec->sw_buffer_size != vss->buffer_bytes || rec->hw_ready < 0 ||
+ rec->sw_ready < 0 || rec->hw_data >= rec->hw_buffer_size ||
+ rec->hw_io >= rec->hw_buffer_size ||
+ rec->sw_data >= rec->sw_buffer_size ||
+ rec->sw_io >= rec->sw_buffer_size ||
+ (unsigned int)rec->sw_ready > rec->sw_buffer_size)
+ return -EPIPE;
+
+ hw_queue_size = rec->hw_queue_size ? rec->hw_queue_size :
+ rec->hw_buffer_size;
+ if (!hw_queue_size || hw_queue_size > rec->hw_buffer_size ||
+ (unsigned int)rec->hw_ready > hw_queue_size)
+ return -EPIPE;
+
+ int_max = (int)(~0U >> 1);
+ if (hw_queue_size > int_max || appl_bits / 8 > int_max)
+ return -EPIPE;
+ appl_bytes = frames_to_bytes(runtime, (snd_pcm_sframes_t)appl_diff);
+ if (appl_bytes != (size_t)(appl_bits / 8) ||
+ check_add_overflow((s64)rec->sw_ready, (s64)appl_bytes,
+ &sw_ready_total) ||
+ sw_ready_total > int_max)
+ return -EPIPE;
+
+ /* hw_ready already excludes device completions accounted by the pointer. */
+ if (check_add_overflow((s64)rec->hw_ready, (s64)rec->sw_ready,
+ &queued_bytes) ||
+ check_add_overflow(queued_bytes, (s64)appl_bytes, &total_bytes) ||
+ total_bytes > (s64)vss->buffer_bytes)
+ return -EPIPE;
+
+ return 0;
+}
+
static void virtsnd_pcm_trans_copy(struct snd_pcm_substream *substream,
struct snd_pcm_indirect *rec, size_t bytes)
{
@@ -670,7 +773,15 @@ static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
rc = -EPIPE;
} else {
transfer.indirect = vss->pcm_indirect;
- rc = snd_pcm_indirect_playback_transfer(substream,
+ rc = virtsnd_pcm_playback_ack_validate(vss,
+ substream->runtime,
+ &transfer.indirect);
+ if (rc == -EBADFD ||
+ (rc && substream->runtime->state ==
+ SNDRV_PCM_STATE_PAUSED))
+ return -EBADFD;
+ if (!rc)
+ rc = snd_pcm_indirect_playback_transfer(substream,
&transfer.indirect,
virtsnd_pcm_trans_copy);
if (!rc)
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 09/20] ALSA: virtio: bound PCM transfers and retain DMA buffers
2026-11-13 6:41 ` [PATCH v32 09/20] ALSA: virtio: bound PCM transfers and retain DMA buffers Joonho Lee via B4 Relay
@ 2026-11-13 7:11 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:11 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> Validate ACK geometry, conversion bounds and queued-byte accounting
> before submitting transfers. Retain the backing DMA pages until message
> retirement. Check RX status and upper payload bounds while accepting
> bounded short responses, including at STOP and PAUSE. Preserve the
> legacy RX completion and ACK policy until the request-owned conversion,
> including the legacy handling of empty PAUSE returns. The fixed-offset
> layout still has short-response data gaps; request-owned capture and its
> queue/accounting contract arrive together in patch 12. Non-MMU copies
> use only the validated returned payload length.
>
> 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>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=9
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 10/20] ALSA: virtio: Keep control payloads alive through DMA completion
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (8 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 09/20] ALSA: virtio: bound PCM transfers and retain DMA buffers Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:50 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 11/20] ALSA: virtio: wait through signals for PCM teardown Joonho Lee via B4 Relay
` (9 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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>
---
sound/virtio/virtio_ctl_msg.c | 275 ++++++++++++++++++++++++++++++++----------
sound/virtio/virtio_ctl_msg.h | 24 ++--
sound/virtio/virtio_kctl.c | 109 +++++++++--------
sound/virtio/virtio_pcm_msg.c | 2 +-
4 files changed, 287 insertions(+), 123 deletions(-)
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
index 6093298cdf5a..07df43f923f5 100644
--- 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_ref(struct virtio_snd_msg *msg)
*/
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);
+ }
}
/**
@@ -73,10 +88,71 @@ 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 @@ void *virtsnd_ctl_msg_response(struct virtio_snd_msg *msg)
* 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;
+ 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)
+ 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;
+ 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) + request_size + response_size, gfp);
+ 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 @@ struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size,
* 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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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);
+ if (msg->response_data_size)
+ psgs[nouts + nins++] = &msg->sg_response_data;
- list_add_tail(&msg->list, &snd->ctl_msgs);
- if (notify)
- virtqueue_notify(queue->vqueue);
- }
+ 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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
* 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 @@ int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id,
{
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 @@ int virtsnd_ctl_query_info(struct virtio_snd *snd, int command, int start_id,
query->count = cpu_to_le32(count);
query->size = cpu_to_le32(size);
- sg_init_one(&sg, info, count * size);
+ 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 virtsnd_ctl_msg_send(snd, msg, NULL, &sg, false);
+ return rc;
}
/**
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
index d917435409d0..f867b9b9233d 100644
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -21,20 +21,28 @@ void *virtsnd_ctl_msg_request(struct virtio_snd_msg *msg);
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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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_sync(struct virtio_snd *snd,
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
index 45f7b6a5b308..2b6290c45e7a 100644
--- 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 <sound/control.h>
+#include <linux/limits.h>
+#include <linux/overflow.h>
#include <linux/virtio_config.h>
+#include <sound/control.h>
#include "virtio_card.h"
@@ -157,14 +159,16 @@ static int virtsnd_kctl_get(struct snd_kcontrol *kcontrol,
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 @@ static int virtsnd_kctl_get(struct snd_kcontrol *kcontrol,
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 @@ static int virtsnd_kctl_put(struct snd_kcontrol *kcontrol,
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,23 +292,28 @@ static int virtsnd_kctl_tlv_op(struct snd_kcontrol *kcontrol, int op_flag,
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 @@ static int virtsnd_kctl_tlv_op(struct snd_kcontrol *kcontrol, int op_flag,
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;
- }
+ /* 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);
- break;
+ 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 @@ static int virtsnd_kctl_tlv_op(struct snd_kcontrol *kcontrol, int op_flag,
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;
+ return virtsnd_ctl_msg_send(snd, msg, false);
}
- kfree(tlv);
- return rc;
-
-on_msg_unref:
- virtsnd_ctl_msg_unref(msg);
- kfree(tlv);
- return rc;
+ return -EINVAL;
}
/**
@@ -369,27 +369,36 @@ static int virtsnd_kctl_get_enum_items(struct virtio_snd *snd, unsigned int cid)
struct virtio_snd_msg *msg;
struct virtio_snd_ctl_hdr *hdr;
unsigned int n = le32_to_cpu(kinfo->value.enumerated.items);
- struct scatterlist sg;
+ size_t items_size;
+ int rc;
- msg = virtsnd_ctl_msg_alloc(sizeof(*hdr),
- sizeof(struct virtio_snd_hdr), GFP_KERNEL);
- if (!msg)
- return -ENOMEM;
+ 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);
+ if (!kctl->items)
return -ENOMEM;
- }
- sg_init_one(&sg, kctl->items, n * sizeof(*kctl->items));
+ 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
index f8d191952d41..a3e23fdbea14 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -688,7 +688,7 @@ virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss,
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);
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 10/20] ALSA: virtio: Keep control payloads alive through DMA completion
2026-11-13 6:41 ` [PATCH v32 10/20] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee via B4 Relay
@ 2026-11-13 6:50 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 6:50 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=10
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 11/20] ALSA: virtio: wait through signals for PCM teardown
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (9 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 10/20] ALSA: virtio: Keep control payloads alive through DMA completion Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:58 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 12/20] ALSA: virtio: use request-owned capture and serialize recovery Joonho Lee via B4 Relay
` (8 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
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.
Keep timeout and wait results in the types returned by the kernel wait
APIs.
Fixes: 9d45e514da88 ("ALSA: virtio: handling control messages")
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_ctl_msg.c | 19 ++++++++++++++++---
sound/virtio/virtio_ctl_msg.h | 3 +++
sound/virtio/virtio_pcm_ops.c | 10 +++++-----
3 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
index 07df43f923f5..7de20c3da9c2 100644
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -265,7 +265,7 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
{
struct virtio_device *vdev = snd->vdev;
struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
- unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ unsigned long js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg);
struct virtio_snd_hdr *response = virtsnd_ctl_msg_response(msg);
unsigned int nouts = 0;
@@ -273,7 +273,7 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
struct scatterlist *psgs[4];
bool notify = false;
unsigned long flags;
- int rc;
+ long rc;
virtsnd_ctl_msg_ref(msg);
/* This reference stays with the queue until completion or teardown. */
@@ -314,7 +314,20 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
*/
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
index f867b9b9233d..d84f0740fcb0 100644
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -47,6 +47,9 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
* 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.
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
index f40a9f3cd2a3..a5a682534acf 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -20,11 +20,11 @@
* (RELEASE command in the ops->sync_stop()) and the ops->hw_free() has been
* called.
*
- * When the substream stops, the ops->sync_stop() waits until the device has
- * completed all pending messages. This wait can be interrupted either by a
- * 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.
+ * When the substream stops, ops->sync_stop() stops and releases the device
+ * as required, then waits for pending messages. A failed control request or
+ * timed-out flush can leave the device able to access messages after
+ * 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.
*
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 12/20] ALSA: virtio: use request-owned capture and serialize recovery
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (10 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 11/20] ALSA: virtio: wait through signals for PCM teardown Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 7:10 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 13/20] ALSA: virtio: prioritize capture period work Joonho Lee via B4 Relay
` (7 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Receive capture into request-owned buffers and commit completed requests
in submission order. Append actual frame-aligned payloads contiguously
to the ALSA ring, preserve a valid STOP tail, and retire empty requests
without inventing data. Bound occupancy and report overflow through
XRUN.
Add explicit, default-off compatibility options for packed status
decoding and capture pause using RELEASE followed by PREPARE. The
default capture path uses request-owned buffers.
Serialize control transitions and preparation by generation, keep
payload and DMA ownership separate from the ALSA runtime, and stop
admission after uncertain control completion. Preserve linked prepare
and paused-error recovery semantics. Reject new ACK admissions after the
core buffer-access gate closes, including linked members that have not
entered their prepare callback. The PCM lock barriers wait for ACKs
admitted before gate closure. Internal RESET pointer observation remains
separately guarded.
Implement separate capture and playback RESET paths. A paused capture
RESET keeps the modulo position of the last committed sample instead of
rebasing the logical cursor independently of RX storage. This preserves
the ordinary capture reset/reopen sample sequence.
Capture DRAINING uses the progress helper for every pending
notification, including a tail crossing a period boundary and a latched
error. Running capture and playback retain their period-timer behavior.
Retain request buffers until device ownership ends. This adds temporary
memory and copies validated RX data while queue and stream locks are
held. Each period buffer is allocated separately, so page rounding can
make the allocation larger than the ALSA ring.
A separate narrow Linux 6.18.60 experiment provides evidence for the
RX-credit root cause, not a qualification run of this full patch. With
Scarlett 2i2 input 1 at actual 48 kHz, S16_LE mono, a 6,000-frame period
and 24,000-frame buffer, the stock trace began with 48,000 bytes of
request credit. Across 160 short completions it lost 36,192 bytes,
leaving 11,808 bytes, 192 below the 12,000-byte next-request threshold,
with no pending requests. ACK and period work still ran. Replay matched
452 state transitions and stored all 3,209 trace entries.
A separate opt-in Linux 6.18.60 candidate passed two 70-second captures
and ten 3-second reopens using the same kernel binary as stock; only the
virtio-snd module and options differed. This supports the narrow
ordinary-load RX-credit repair only. It does not test this full patch.
The narrow candidate omitted patches 1-5
policy/progress/reset/prepare/group-lock dependencies and patches 7-11
lifecycle/ownership coverage. Patch 13 work priority is not required for
the observed normal-load dead end, but remains a separate performance
change. Patches 15-16 cover playback separately.
RESET discards valid completed capture payloads already waiting for an
earlier request at the pointer-observation boundary. Keep their sequence
placeholders so later completions can advance without replaying the
discarded samples. Preserve transport-owned requests and data completed
after that boundary, and retain protocol-error reporting.
When a completion is rejected after an external XRUN or inactive
runtime, release the other returned buffered entries as well. PREPARE
continues to reset the complete generation after ownership is returned.
On failed native PAUSE, restore the previous capture-stopping flag and
quiet sequence before reopening transfer admission. Otherwise old
requests can retain the empty-tail exception while capture is running.
The restore is guarded by the substream lock and reset epoch.
ARM64 QEMU controlled tests reproduce the v27 failures and pass the
corrected ordering, RESET, XRUN and failed-PAUSE paths. The separately
built production module passes RW/MMAP capture, RESET, playback pause
and reopen regression tests with KASAN and lockdep.
Link: https://lore.kernel.org/linux-sound/sashiko-outbox-210240@kernel.org/
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_card.c | 58 +-
sound/virtio/virtio_ctl_msg.c | 58 +-
sound/virtio/virtio_ctl_msg.h | 9 +-
sound/virtio/virtio_pcm.c | 101 ++-
sound/virtio/virtio_pcm.h | 150 +++-
sound/virtio/virtio_pcm_msg.c | 598 ++++++++++++--
sound/virtio/virtio_pcm_ops.c | 1831 ++++++++++++++++++++++++++++++++++++-----
7 files changed, 2490 insertions(+), 315 deletions(-)
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
index fe31eb7805d8..336da9a5b8fe 100644
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -369,6 +369,23 @@ 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->preparing = 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. */
@@ -388,13 +405,36 @@ static void virtsnd_reset_vqs(struct virtio_snd *snd)
queue->vqueue = NULL;
spin_unlock_irqrestore(&queue->lock, flags);
}
-
if (snd->vqs_initialized) {
vdev->config->del_vqs(vdev);
snd->vqs_initialized = false;
}
}
+/**
+ * virtsnd_cancel_period_work() - Drain PCM work after control cleanup.
+ * @snd: VirtIO sound device.
+ *
+ * Must follow control message cleanup, which wakes work blocked on a PCM
+ * control request.
+ *
+ * Context: Any context that permits to sleep.
+ */
+static void virtsnd_cancel_period_work(struct virtio_snd *snd)
+{
+ unsigned int i;
+
+ 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;
+ }
+}
+
/**
* virtsnd_remove() - Remove VirtIO and ALSA devices.
* @vdev: VirtIO parent device.
@@ -414,12 +454,13 @@ static void virtsnd_remove(struct virtio_device *vdev)
virtsnd_ctl_msg_cancel_all(snd);
virtsnd_reset_vqs(snd);
+ /* Wake PCM workers blocked on control replies before draining them. */
virtsnd_ctl_msg_cleanup(snd);
+ virtsnd_cancel_period_work(snd);
for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
struct virtio_pcm_substream *vss = &snd->substreams[i];
- cancel_work_sync(&vss->elapsed_period);
virtsnd_pcm_msg_free(vss);
}
@@ -437,16 +478,14 @@ static void virtsnd_remove(struct virtio_device *vdev)
static int virtsnd_freeze(struct virtio_device *vdev)
{
struct virtio_snd *snd = vdev->priv;
- unsigned int i;
virtsnd_disable_event_vq(snd);
virtsnd_ctl_msg_cancel_all(snd);
virtsnd_reset_vqs(snd);
+ /* Wake PCM workers blocked on control replies before draining them. */
virtsnd_ctl_msg_cleanup(snd);
-
- for (i = 0; i < snd->nsubstreams; ++i)
- cancel_work_sync(&snd->substreams[i].elapsed_period);
+ virtsnd_cancel_period_work(snd);
kfree(snd->event_msgs);
snd->event_msgs = NULL;
@@ -465,6 +504,7 @@ static int virtsnd_restore(struct virtio_device *vdev)
{
struct virtio_snd *snd = vdev->priv;
int rc;
+ unsigned int i;
rc = virtsnd_find_vqs(snd);
if (rc) {
@@ -475,6 +515,12 @@ static int virtsnd_restore(struct virtio_device *vdev)
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
index 7de20c3da9c2..d7480253a5f5 100644
--- 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,8 +40,50 @@ struct virtio_snd_msg {
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.
* @msg: Control message.
@@ -293,9 +338,9 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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
index d84f0740fcb0..b92c41c0c1a6 100644
--- 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 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
* 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
index be3893de40a5..7b332c9476f1 100644
--- a/sound/virtio/virtio_pcm.c
+++ b/sound/virtio/virtio_pcm.c
@@ -310,8 +310,97 @@ static void virtsnd_pcm_period_elapsed(struct work_struct *work)
{
struct virtio_pcm_substream *vss =
container_of(work, struct virtio_pcm_substream, elapsed_period);
+ struct snd_pcm_substream *substream = vss->substream;
+ struct snd_pcm_runtime *runtime;
+ size_t period_bytes;
+ u64 period_floor;
+ u64 snapshot;
+ bool paused_xrun = false;
+ bool notify = false;
+ bool partial = false;
+ bool requeue = false;
+
+ /* Serialize the pointer snapshot with ALSA's pointer and ACK callbacks. */
+ guard(pcm_stream_lock_irqsave)(substream);
+ runtime = substream->runtime;
+ if (!runtime)
+ return;
+ if (!snd_pcm_running(substream) &&
+ runtime->state != SNDRV_PCM_STATE_DRAINING) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ paused_xrun = runtime &&
+ runtime->state == SNDRV_PCM_STATE_PAUSED &&
+ vss->xfer_xrun;
+ if (paused_xrun)
+ virtsnd_pcm_defer_paused_xrun(vss);
+ }
+ return;
+ }
+
+ if (vss->direction != SNDRV_PCM_STREAM_CAPTURE) {
+ snd_pcm_period_elapsed_under_stream_lock(substream);
+ return;
+ }
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ runtime = substream->runtime;
+ if (!runtime || !vss->runtime_active ||
+ (!vss->xfer_enabled && !vss->xfer_xrun &&
+ runtime->state != SNDRV_PCM_STATE_DRAINING))
+ 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;
+
+ /* Capture DRAINING needs pointer updates even at a period boundary. */
+ if (partial || runtime->state == SNDRV_PCM_STATE_DRAINING)
+ 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;
+ }
+ }
- snd_pcm_period_elapsed(vss->substream);
+ if (requeue)
+ virtsnd_pcm_schedule_period_elapsed(vss);
}
/**
@@ -351,8 +440,10 @@ int virtsnd_pcm_parse_cfg(struct virtio_snd *snd)
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 +577,7 @@ int virtsnd_pcm_build_devs(struct virtio_snd *snd)
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 +608,12 @@ void virtsnd_pcm_event(struct virtio_snd *snd, struct virtio_snd_event *event)
break;
case VIRTIO_SND_EVT_PCM_XRUN:
scoped_guard(spinlock, &vss->lock) {
- if (vss->xfer_enabled)
+ if (vss->xfer_enabled ||
+ (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
+ vss->prepared && !vss->stopped)) {
vss->xfer_xrun = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
}
break;
}
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index b75961c705cc..95881a3a2c57 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -7,7 +7,10 @@
#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>
@@ -15,6 +18,22 @@ 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.
* @snd: VirtIO sound device.
@@ -26,20 +45,48 @@ struct page;
* @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.
+ * @period_work_quiesced: Teardown owns a work disable, protected by @lifecycle_lock.
+ * @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_submit_sequence: Next sequence assigned to a capture RX request.
+ * @capture_commit_sequence: Next capture request sequence allowed to append.
+ * @capture_quiet_sequence: Requests below this sequence were queued before a
+ * STOP or PAUSE and may complete without audio.
+ * @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 capture runtime buffer may be accessed by completions.
+ * @capture_stopping: A STOP or PAUSE is draining requests already in flight.
+ * @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: True after device PREPARE and the core pointer reset succeed;
- * protected by @lock.
- * @preparing: ACK gate through teardown and prepare's core reset;
- * protected by @lock.
+ * @prepared: Device PREPARE succeeded; protected by @lock.
+ * @preparing: ACK gate during sleeping teardown and prepare's core reset; protected by
+ * the PCM stream lock and @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.
+ * @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.
* @msgs: Allocated I/O messages.
* @nmsgs: Number of allocated I/O messages.
* @msg_count: Number of pending I/O messages in the virtqueue.
@@ -60,16 +107,41 @@ struct virtio_pcm_substream {
struct snd_pcm_hardware hw;
struct work_struct elapsed_period;
spinlock_t lock;
+ struct mutex lifecycle_lock;
+ bool period_work_quiesced;
+ 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_submit_sequence;
+ u64 capture_commit_sequence;
+ u64 capture_quiet_sequence;
+ u64 capture_notified_periods;
+ u64 capture_callback_snapshot;
+ bool capture_callback_snapshot_active;
+ bool runtime_active;
+ bool capture_stopping;
+ bool capture_started;
+ bool capture_free_running;
+ unsigned int capture_empty;
bool xfer_enabled;
bool xfer_xrun;
bool stopped;
bool prepared;
bool preparing;
- /* Successful PREPARE still needs RELEASE; protected by lock. */
+ /* Native control state, protected by lock. */
+ bool device_running;
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;
unsigned int nmsgs;
unsigned int msg_count;
@@ -82,6 +154,63 @@ struct virtio_pcm_substream {
wait_queue_head_t msg_empty;
};
+/* Caller holds the PCM stream lock and vss->lock. */
+static inline void
+virtsnd_pcm_defer_paused_xrun(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;
+
+ /* Publish the XRUN after resume, when ALSA can stop the whole group. */
+ vss->xfer_enabled = false;
+ runtime->stop_operating = true;
+}
+
+/* Give capture accounting priority over PCM callbacks that can block readers. */
+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.
@@ -112,6 +241,8 @@ struct virtio_pcm {
extern const struct snd_pcm_ops virtsnd_pcm_ops[];
+extern bool virtsnd_capture_packed_status;
+
int virtsnd_pcm_validate(struct virtio_device *vdev);
int virtsnd_pcm_parse_cfg(struct virtio_snd *snd);
@@ -130,11 +261,18 @@ struct virtio_pcm *virtsnd_pcm_find_or_create(struct virtio_snd *snd, u32 nid);
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);
+
+void virtsnd_pcm_capture_discard_completed(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
index a3e23fdbea14..3dfc45d90f08 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -3,9 +3,12 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/compiler.h>
+#include <linux/gcd.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,15 +19,29 @@
* @xfer: Request header payload.
* @status: Response header payload.
* @length: Data length in bytes.
+ * @data: Request-owned payload for every capture request.
* @data_offset: Offset of the fixed PCM ring slice for ordinary I/O.
+ * @capture_sequence: Submission order for capture stream data.
+ * @capture_audio_bytes: Validated audio bytes returned by the device.
+ * @capture_latency_bytes: Latency returned with the capture completion.
+ * @capture_complete: Completion waits for preceding request sequences.
+ * @capture_valid: Response length and status passed protocol validation.
+ * @capture_discard: RESET observed this completed payload before its commit.
* @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;
size_t data_offset;
+ u64 capture_sequence;
+ size_t capture_audio_bytes;
+ u32 capture_latency_bytes;
+ bool capture_complete;
+ bool capture_valid;
+ bool capture_discard;
struct scatterlist sgs[];
};
@@ -212,6 +229,7 @@ int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
struct snd_pcm_runtime *runtime;
size_t buffer_bytes;
unsigned int i;
+ bool request_owned;
int rc;
if (vss->msgs || vss->nmsgs || vss->dma_pages || vss->ndma_pages)
@@ -222,10 +240,13 @@ int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
#endif
if (!periods || !period_bytes)
return -EINVAL;
+ request_owned = vss->direction == SNDRV_PCM_STREAM_CAPTURE;
if (check_mul_overflow((size_t)periods, (size_t)period_bytes,
&buffer_bytes))
return -EOVERFLOW;
+ if (request_owned && periods < 2)
+ return -EINVAL;
if (!vss->substream || !vss->substream->runtime)
return -EINVAL;
@@ -234,14 +255,19 @@ int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
return -EINVAL;
#ifdef CONFIG_MMU
- rc = virtsnd_pcm_dma_pages_get(vss, runtime->dma_area, buffer_bytes);
- if (rc)
- return rc;
+ if (!request_owned) {
+ 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 (!request_owned) {
+ 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);
@@ -253,32 +279,54 @@ int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
vss->nmsgs = periods;
for (i = 0; i < periods; ++i) {
- int sg_num;
struct virtio_pcm_msg *msg;
u8 *data;
+ int sg_num;
+
+ if (request_owned) {
+ 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 (request_owned)
+ vfree(data);
rc = sg_num;
goto err_free_msgs;
}
msg = kzalloc_flex(*msg, sgs, sg_num + 2);
if (!msg) {
+ if (request_owned)
+ vfree(data);
rc = -ENOMEM;
goto err_free_msgs;
}
msg->substream = vss;
+ msg->data = request_owned ? 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));
msg->data_offset = (size_t)period_bytes * i;
virtsnd_pcm_sg_from(&msg->sgs[PCM_MSG_SG_DATA], sg_num, data,
period_bytes);
@@ -312,8 +360,14 @@ void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss)
{
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);
@@ -335,15 +389,38 @@ void virtsnd_pcm_msg_free(struct virtio_pcm_substream *vss)
* 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;
+ for (i = 0; vss->msgs && i < vss->nmsgs; ++i) {
+ vss->msgs[i]->capture_sequence = 0;
+ vss->msgs[i]->capture_audio_bytes = 0;
+ vss->msgs[i]->capture_latency_bytes = 0;
+ vss->msgs[i]->capture_complete = false;
+ vss->msgs[i]->capture_valid = false;
+ vss->msgs[i]->capture_discard = false;
+ }
+ vss->capture_ready = 0;
+ vss->capture_copied_total = 0;
+ vss->capture_acked_total = 0;
+ vss->capture_pointer_seen_total = 0;
+ vss->capture_submit_sequence = 0;
+ vss->capture_commit_sequence = 0;
+ vss->capture_quiet_sequence = 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;
+ vss->capture_stopping = false;
}
/**
@@ -373,6 +450,8 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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 ||
@@ -398,8 +477,8 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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) {
@@ -473,6 +552,393 @@ int virtsnd_pcm_msg_send(struct virtio_pcm_substream *vss, unsigned long offset,
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->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->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 || msg->capture_complete)
+ continue;
+ if (vss->capture_submit_sequence == U64_MAX)
+ return -EOVERFLOW;
+
+ 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;
+ msg->capture_sequence = vss->capture_submit_sequence++;
+ msg->capture_audio_bytes = 0;
+ msg->capture_latency_bytes = 0;
+ msg->capture_valid = false;
+ msg->capture_discard = false;
+ 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);
+ }
+}
+
+/* Keep ordered placeholders for completed payloads observed by RESET. */
+void virtsnd_pcm_capture_discard_completed(struct virtio_pcm_substream *vss)
+{
+ unsigned int i;
+
+ lockdep_assert_held(&vss->lock);
+ for (i = 0; i < vss->nmsgs; i++) {
+ struct virtio_pcm_msg *msg = vss->msgs[i];
+
+ if (msg->capture_complete && msg->capture_valid)
+ msg->capture_discard = true;
+ }
+}
+
+/* Discard returned buffers only; transport-owned requests keep their length. */
+static void virtsnd_pcm_capture_drop_completed(struct virtio_pcm_substream *vss)
+{
+ unsigned int i;
+
+ lockdep_assert_held(&vss->lock);
+ for (i = 0; i < vss->nmsgs; i++) {
+ struct virtio_pcm_msg *msg = vss->msgs[i];
+
+ if (msg->capture_complete) {
+ msg->capture_complete = false;
+ msg->capture_discard = false;
+ msg->length = 0;
+ }
+ }
+}
+
+/* Caller holds vss->lock; append only once every older request is complete. */
+static bool virtsnd_pcm_capture_commit_one_locked(
+ struct virtio_pcm_substream *vss, struct virtio_pcm_msg *msg)
+{
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ size_t audio_bytes = msg->capture_audio_bytes;
+ size_t next_ready = 0;
+ u64 next_copied_total;
+ bool free_running;
+ const char *invalid_reason = NULL;
+ size_t ring_ptr, first;
+ unsigned int limit;
+
+ if (!msg->capture_valid)
+ invalid_reason = "status";
+ else 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 (check_add_overflow(vss->capture_copied_total,
+ (u64)audio_bytes, &next_copied_total))
+ invalid_reason = "total_overflow";
+ else if (!audio_bytes)
+ goto empty;
+ 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)
+ goto invalid;
+
+ ring_ptr = virtsnd_pcm_ring_offset(vss->capture_copied_total,
+ vss->buffer_bytes);
+ 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,
+ msg->capture_latency_bytes);
+ return true;
+
+empty:
+ if (msg->capture_sequence >= vss->capture_quiet_sequence) {
+ limit = min_t(unsigned int, vss->nmsgs, UINT_MAX / 2) * 2;
+ if (vss->capture_started || vss->capture_empty >= limit)
+ goto invalid;
+ vss->capture_empty++;
+ }
+ runtime->delay = bytes_to_frames(runtime, msg->capture_latency_bytes);
+ return false;
+
+invalid:
+ virtsnd_pcm_capture_xrun_locked(vss);
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: capture completion rejected (%s)\n",
+ vss->sid, invalid_reason ?: "empty_limit");
+ return false;
+}
+
+/* Caller holds vss->lock. */
+static bool virtsnd_pcm_capture_commit_ready_locked(
+ struct virtio_pcm_substream *vss)
+{
+ bool got_audio = false;
+ unsigned int i;
+
+ while (!vss->xfer_xrun) {
+ struct virtio_pcm_msg *next = NULL;
+
+ for (i = 0; i < vss->nmsgs; i++) {
+ struct virtio_pcm_msg *msg = vss->msgs[i];
+
+ if (msg->capture_complete &&
+ msg->capture_sequence == vss->capture_commit_sequence) {
+ next = msg;
+ break;
+ }
+ }
+ if (!next)
+ break;
+ if (!next->capture_discard)
+ got_audio |= virtsnd_pcm_capture_commit_one_locked(vss, next);
+ if (vss->xfer_xrun)
+ break;
+ next->capture_complete = false;
+ next->capture_discard = false;
+ next->length = 0;
+ vss->capture_commit_sequence++;
+ }
+
+ if (vss->xfer_xrun)
+ virtsnd_pcm_capture_drop_completed(vss);
+
+ return got_audio;
+}
+
+static void virtsnd_pcm_capture_complete(struct virtio_pcm_msg *msg,
+ size_t written_bytes)
+{
+ struct virtio_pcm_substream *vss = msg->substream;
+ struct snd_pcm_runtime *runtime;
+ size_t audio_bytes = 0;
+ u32 latency_bytes = 0;
+ size_t frame_bytes;
+ bool got_audio = false;
+ bool accept_completion;
+
+ guard(spinlock)(&vss->lock);
+ vss->msg_count--;
+ accept_completion = vss->runtime_active && vss->prepared &&
+ (!vss->stopped || vss->capture_stopping) && !vss->xfer_xrun;
+ if (accept_completion) {
+ runtime = vss->substream->runtime;
+ if (runtime && runtime->dma_area) {
+ frame_bytes = runtime->frame_bits /
+ gcd(runtime->frame_bits, 8);
+ msg->capture_valid = !virtsnd_pcm_capture_status(
+ msg, written_bytes, frame_bytes, &audio_bytes,
+ &latency_bytes);
+ msg->capture_audio_bytes = audio_bytes;
+ msg->capture_latency_bytes = latency_bytes;
+ msg->capture_complete = true;
+ got_audio = virtsnd_pcm_capture_commit_ready_locked(vss);
+ } else {
+ msg->length = 0;
+ msg->capture_complete = false;
+ virtsnd_pcm_capture_drop_completed(vss);
+ }
+ } else {
+ msg->length = 0;
+ msg->capture_complete = false;
+ virtsnd_pcm_capture_drop_completed(vss);
+ }
+
+ if (vss->runtime_active && vss->xfer_enabled) {
+ if (!vss->xfer_xrun && virtsnd_pcm_capture_send(vss)) {
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: failed to refill capture queue\n",
+ vss->sid);
+ virtsnd_pcm_capture_xrun_locked(vss);
+ }
+ }
+ if (!vss->msg_count) {
+ wake_up_all(&vss->msg_empty);
+ }
+ if (got_audio && !vss->xfer_xrun)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+}
+
/**
* virtsnd_pcm_msg_pending_num() - Returns the number of pending I/O messages.
* @vss: VirtIO substream.
@@ -524,9 +990,9 @@ void virtsnd_pcm_msg_cancel(struct virtio_pcm_msg *msg)
* 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 playback substream, @written_bytes is equal to sizeof(*msg->status).
*
- * For the capture 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.
@@ -535,80 +1001,32 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
size_t written_bytes)
{
struct virtio_pcm_substream *vss = msg->substream;
- bool capture_invalid = false;
+ size_t completed_bytes;
+ u32 latency_bytes;
- /*
- * Completed requests can return captured data after STOP while the PCM
- * buffer remains prepared. Once preparation ends, completions only retire
- * queue ownership.
- */
- guard(spinlock)(&vss->lock);
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- (!vss->prepared || vss->xfer_xrun ||
- (vss->stopped && written_bytes <= sizeof(msg->status)))) {
- msg->length = 0;
- vss->msg_count--;
- if (!vss->msg_count)
- wake_up_all(&vss->msg_empty);
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ virtsnd_pcm_capture_complete(msg, written_bytes);
return;
}
- /* Validate the returned payload before advancing the capture pointer. */
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- (written_bytes < sizeof(msg->status) ||
- le32_to_cpu(msg->status.status) != VIRTIO_SND_S_OK ||
- written_bytes - sizeof(msg->status) > msg->length)) {
- dev_err_ratelimited(&vss->snd->vdev->dev,
- "SID %u: invalid capture position, status or length\n",
- vss->sid);
- vss->xfer_xrun = true;
- capture_invalid = true;
- }
-#ifndef CONFIG_MMU
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- vss->prepared && !capture_invalid &&
- written_bytes > sizeof(msg->status)) {
- struct snd_pcm_runtime *runtime =
- vss->substream ? vss->substream->runtime : NULL;
-
- if (!runtime || !runtime->dma_area || !vss->owned_dma_area ||
- msg->data_offset > runtime->dma_bytes ||
- written_bytes - sizeof(msg->status) > runtime->dma_bytes - msg->data_offset ||
- msg->data_offset > vss->owned_dma_bytes ||
- written_bytes - sizeof(msg->status) > 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);
- vss->xfer_xrun = true;
- capture_invalid = true;
- } else {
- memcpy(runtime->dma_area + msg->data_offset,
- vss->owned_dma_area + msg->data_offset,
- written_bytes - sizeof(msg->status));
- }
- }
-#endif
- /* Publish pointer progress only for a complete TX or valid RX request. */
- if (!capture_invalid) {
- vss->hw_ptr += vss->direction == SNDRV_PCM_STREAM_CAPTURE &&
- written_bytes > sizeof(msg->status) ?
- written_bytes - sizeof(msg->status) : msg->length;
- if (vss->hw_ptr >= vss->buffer_bytes)
- vss->hw_ptr -= vss->buffer_bytes;
- }
+ /* Playback requests point directly at the ALSA ring. */
+ guard(spinlock)(&vss->lock);
+ completed_bytes = msg->length;
+ latency_bytes = le32_to_cpu(msg->status->latency_bytes);
+ vss->hw_ptr += completed_bytes;
+ if (vss->hw_ptr >= vss->buffer_bytes)
+ vss->hw_ptr -= vss->buffer_bytes;
msg->length = 0;
+
+ /* Keep latched errors until prepare establishes a fresh epoch. */
vss->msg_count--;
if (vss->xfer_enabled) {
- if (!capture_invalid) {
- struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
- runtime->delay =
- bytes_to_frames(runtime,
- le32_to_cpu(msg->status.latency_bytes));
- }
- schedule_work(&vss->elapsed_period);
+ runtime->delay = bytes_to_frames(runtime, latency_bytes);
+ virtsnd_pcm_schedule_period_elapsed(vss);
}
if (!vss->msg_count)
wake_up_all(&vss->msg_empty);
@@ -668,6 +1086,7 @@ void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue)
* 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.
*
@@ -676,7 +1095,7 @@ void virtsnd_pcm_rx_notify_cb(struct virtqueue *vqueue)
*/
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);
@@ -694,6 +1113,7 @@ virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss,
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
index a5a682534acf..442169a51667 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -3,10 +3,27 @@
* virtio-snd: Virtio sound device
* Copyright (C) 2021 OpenSynergy GmbH
*/
+#include <linux/moduleparam.h>
+#include <linux/overflow.h>
+#include <linux/spinlock.h>
#include <sound/pcm_params.h>
#include "virtio_card.h"
+static const struct snd_pcm_ops virtsnd_pcm_capture_ops;
+
+bool virtsnd_capture_packed_status;
+module_param_named(capture_packed_status, virtsnd_capture_packed_status,
+ bool, 0444);
+MODULE_PARM_DESC(capture_packed_status,
+ "Read an explicitly opted-in packed PCM status trailer");
+
+static bool virtsnd_capture_pause_restart;
+module_param_named(capture_pause_restart, virtsnd_capture_pause_restart,
+ bool, 0444);
+MODULE_PARM_DESC(capture_pause_restart,
+ "Use release/reprepare pause compatibility instead of native pause");
+
/*
* I/O messages lifetime
* ---------------------
@@ -97,48 +114,265 @@ static const struct virtsnd_a2v_rate g_a2v_rate_map[] = {
};
static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream);
+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);
-/**
- * virtsnd_pcm_close_ack() - Update PCM prepare admission state.
- * @substream: Kernel ALSA substream.
- * @preparing: True while driver prepare and the core pointer reset are active.
- *
- * Context: Process context.
- */
-static void virtsnd_pcm_close_ack(struct snd_pcm_substream *substream,
- bool preparing)
+static void virtsnd_pcm_trigger_failed(struct virtio_pcm_substream *vss,
+ u64 epoch)
{
- struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ bool notify;
- scoped_guard(pcm_stream_lock_irqsave, substream) {
- guard(spinlock_irqsave)(&vss->lock);
- vss->prepared = false;
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return;
vss->xfer_enabled = false;
- vss->preparing = preparing;
+ vss->stopped = vss->release_required || vss->msg_count;
+ vss->xfer_xrun = true;
+ notify = vss->direction != SNDRV_PCM_STREAM_CAPTURE ||
+ vss->runtime_active;
+ }
+ 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->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_start_failed(struct virtio_pcm_substream *vss)
+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;
- schedule_work(&vss->elapsed_period);
}
-/* Keep completed RX data valid while RELEASE drains the device queue. */
-static void virtsnd_pcm_block_ack(struct snd_pcm_substream *substream)
+static int virtsnd_pcm_dev_prepare(struct virtio_pcm_substream *vss, u64 epoch)
{
- struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct virtio_snd_msg *msg;
- scoped_guard(pcm_stream_lock_irqsave, substream) {
- guard(spinlock_irqsave)(&vss->lock);
- vss->preparing = true;
- vss->xfer_enabled = false;
+ 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 long 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->release_required = 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);
+}
+
+/* The PCM stream lock is held; defer a paused XRUN until resume commits. */
+static bool virtsnd_pcm_defer_resume_xrun(struct virtio_pcm_substream *vss,
+ u64 epoch)
+{
+ struct snd_pcm_runtime *runtime = vss->substream->runtime;
+ bool defer = false;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (!vss->resetting && vss->reset_epoch == epoch && runtime &&
+ runtime->state == SNDRV_PCM_STATE_PAUSED &&
+ vss->xfer_xrun) {
+ vss->xfer_enabled = false;
+ runtime->stop_operating = true;
+ defer = true;
+ }
}
+ if (defer)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+
+ return defer;
}
/**
@@ -153,23 +387,36 @@ static int virtsnd_pcm_open(struct snd_pcm_substream *substream)
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) {
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ vss->runtime_active = false;
+ vss->xfer_enabled = false;
+ }
+ }
+
+ /* All capture streams use request-owned receive and accounting paths. */
+ substream->ops = vss->direction == SNDRV_PCM_STREAM_CAPTURE ?
+ &virtsnd_pcm_capture_ops : &virtsnd_pcm_ops[substream->stream];
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;
}
/**
@@ -181,6 +428,15 @@ static int virtsnd_pcm_open(struct snd_pcm_substream *substream)
*/
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;
}
@@ -193,6 +449,7 @@ static int virtsnd_pcm_close(struct snd_pcm_substream *substream)
* @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.
@@ -202,7 +459,7 @@ static int virtsnd_pcm_dev_set_params(struct virtio_pcm_substream *vss,
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;
@@ -228,7 +485,7 @@ static int virtsnd_pcm_dev_set_params(struct virtio_pcm_substream *vss,
return -EINVAL;
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_SET_PARAMS,
- GFP_KERNEL);
+ GFP_KERNEL, epoch);
if (!msg)
return -ENOMEM;
@@ -247,7 +504,14 @@ static int virtsnd_pcm_dev_set_params(struct virtio_pcm_substream *vss,
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);
}
/**
@@ -266,10 +530,10 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
unsigned int msg_count;
size_t period_buffer_bytes;
bool stopped;
+ u64 epoch;
int rc;
- virtsnd_pcm_close_ack(substream, false);
-
+ 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),
@@ -279,6 +543,25 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
(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->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ 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;
@@ -292,16 +575,20 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
if (stopped) {
dev_err(&vdev->dev, "SID %u: stream release failed\n",
vss->sid);
- scoped_guard(pcm_stream_lock_irqsave, substream)
- substream->runtime->stop_operating = true;
+ substream->runtime->stop_operating = true;
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;
@@ -311,8 +598,18 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
*/
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) {
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ vss->suspended = false;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ vss->runtime_active = true;
+ }
+
+ return rc;
}
/**
@@ -325,182 +622,846 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
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);
- virtsnd_pcm_close_ack(substream, false);
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ 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_device() - Prepare the PCM substream on the device.
+ * 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_device(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 stopped;
+ bool pause_restart = virtsnd_pcm_pause_restart_enabled(vss);
+ bool free_running;
int rc;
+ bool stopped;
+ bool suspended;
+ u64 epoch;
+
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ stopped = vss->stopped || vss->release_required || vss->msg_count;
+ vss->prepared = false;
+ vss->preparing = true;
+ }
+ if (stopped) {
+ rc = virtsnd_pcm_sync_stop_locked(substream);
+ if (rc)
+ return rc;
+ }
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ 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;
if (stopped) {
dev_err(&vdev->dev, "SID %u: stream release failed\n",
vss->sid);
- scoped_guard(pcm_stream_lock_irqsave, substream)
- substream->runtime->stop_operating = true;
+ substream->runtime->stop_operating = true;
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);
rc = virtsnd_pcm_dev_set_params(vss, buffer_bytes, period_bytes,
- runtime->channels,
- runtime->format, runtime->rate);
- if (rc)
+ runtime->channels,
+ runtime->format, runtime->rate, epoch);
+ if (rc) {
+ if (pause_restart && rc != -ECANCELED)
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
return rc;
+ }
}
- 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);
- if (!msg)
- return -ENOMEM;
-
- rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
- if (rc)
+ rc = virtsnd_pcm_dev_prepare(vss, epoch);
+ if (rc) {
+ if (pause_restart && rc != -ECANCELED)
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->reset_epoch == epoch && !vss->resetting) {
+ vss->xfer_xrun = true;
+ vss->stopped = true;
+ }
+ }
return rc;
+ }
- scoped_guard(spinlock_irqsave, &vss->lock) {
- vss->xfer_xrun = false;
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ if (epoch != vss->reset_epoch || vss->resetting)
+ return -ECANCELED;
vss->release_required = true;
+ vss->buffer_bytes = snd_pcm_lib_buffer_bytes(substream);
+ vss->hw_ptr = 0;
+ 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->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ vss->capture_boundary = substream->runtime->boundary;
+ rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
+ &free_running);
+ if (rc) {
+ vss->xfer_xrun = true;
+ vss->stopped = true;
+ return rc;
+ }
+ }
+ vss->prepared = true;
+ vss->stopped = false;
+ vss->xfer_xrun = false;
}
- return 0;
+
+ return rc;
}
-/* Keep ACK closed through the core's locked pointer reset. */
static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
{
- virtsnd_pcm_close_ack(substream, true);
- return virtsnd_pcm_prepare_device(substream);
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+
+ int rc;
+
+ guard(mutex)(&vss->lifecycle_lock);
+ rc = virtsnd_pcm_prepare_locked(substream);
+ if (!rc && vss->period_work_quiesced) {
+ enable_work(&vss->elapsed_period);
+ vss->period_work_quiesced = false;
+ }
+ return rc;
}
-/* Complete admission only after the linked core prepare action ends. */
-static void virtsnd_pcm_prepare_complete(
- struct snd_pcm_substream *substream, bool success)
+/* The core calls this after every linked member's prepare action completes. */
+static void virtsnd_pcm_prepare_complete(struct snd_pcm_substream *substream,
+ bool success)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ bool teardown_pending;
- scoped_guard(spinlock_irqsave, &vss->lock) {
- vss->prepared = success;
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting) {
+ vss->prepared = false;
+ vss->xfer_enabled = false;
vss->preparing = false;
+ return;
}
+ if (!success) {
+ teardown_pending = vss->prepared || vss->release_required ||
+ vss->msg_count || vss->device_running || vss->stopped;
+ vss->prepared = false;
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ if (teardown_pending)
+ substream->runtime->stop_operating = true;
+ }
+ vss->preparing = false;
}
-/**
- * virtsnd_pcm_trigger() - Process command for the PCM substream.
- * @substream: Kernel ALSA substream.
- * @command: Substream command (SNDRV_PCM_TRIGGER_XXX).
- *
- * Context: Any context. Takes and releases the VirtIO substream spinlock.
- * May take and release the tx/rx queue spinlock.
- * Return: 0 on success, -errno on failure.
- */
-static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
+static int virtsnd_pcm_pause_restart_start(struct virtio_pcm_substream *vss,
+ bool resume)
{
- struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
- struct virtio_snd *snd = vss->snd;
- struct virtio_snd_queue *queue;
+ 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;
+ bool xrun;
int rc = 0;
+ u64 epoch;
- switch (command) {
- case SNDRV_PCM_TRIGGER_START:
- case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
- queue = virtsnd_pcm_queue(vss);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ if (vss->preparing)
+ return -EBADFD;
+ epoch = vss->reset_epoch;
+ state = vss->pause_restart_state;
+ xrun = vss->xfer_xrun;
+ vss->capture_stopping = false;
+ reset_required =
+ state == VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ virtsnd_pcm_pause_restart_transitioning(state);
+ invalid_state =
+ (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 (xrun) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -EPIPE;
+ }
+ if (invalid_state) {
+ virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ return -EBADFD;
+ }
- scoped_guard(spinlock_irqsave, &queue->lock) {
- guard(spinlock)(&vss->lock);
- if (vss->preparing || !vss->prepared)
- return -EBADFD;
- if (vss->stopped) {
- substream->runtime->stop_operating = true;
- 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 {
+ vss->release_required = true;
+ if (vss->pause_restart_state !=
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED ||
+ vss->msg_count || !vss->runtime_active) {
+ rc = -EBADFD;
+ } else {
+ /* RX may complete before START responds to this PREPARE. */
+ vss->prepared = true;
+ vss->stopped = false;
+ vss->capture_started = false;
+ vss->capture_empty = 0;
+ }
}
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
- rc = virtsnd_pcm_msg_send(vss, 0, vss->buffer_bytes);
- if (!rc)
- vss->xfer_enabled = true;
}
if (rc) {
- virtsnd_pcm_start_failed(vss);
+ 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->xfer_xrun) {
+ rc = -EPIPE;
+ } else if (vss->pause_restart_state !=
+ VIRTSND_PCM_PAUSE_RESTART_PREPARED ||
+ vss->msg_count || !vss->runtime_active) {
+ 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;
+ bool xrun;
+ int rc;
+ u64 epoch;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ return -ECANCELED;
+ epoch = vss->reset_epoch;
+ state = vss->pause_restart_state;
+ xrun = vss->xfer_xrun;
+ if (state != VIRTSND_PCM_PAUSE_RESTART_RELEASED) {
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ vss->capture_stopping = true;
+ vss->capture_quiet_sequence =
+ vss->capture_submit_sequence;
+ }
+ }
+ if (state == VIRTSND_PCM_PAUSE_RESTART_RELEASED) {
+ if (xrun) {
+ virtsnd_pcm_pause_restart_failed(vss, epoch);
+ return -EPIPE;
+ }
+ return 0;
+ }
+
+ 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 (suspend)
+ vss->suspended = true;
+ if (!vss->prepared && !vss->msg_count)
+ return 0;
+ vss->xfer_enabled = false;
+ vss->stopped = true;
+ vss->capture_stopping = true;
+ vss->capture_quiet_sequence = vss->capture_submit_sequence;
+ 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_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->preparing)
+ return -EBADFD;
+ 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) {
+ rc = vss->runtime_active ? virtsnd_pcm_capture_send(vss) : -EBADFD;
+ }
+ if (!rc)
+ vss->xfer_enabled = true;
+ return rc;
+}
+
+/**
+ * virtsnd_pcm_trigger_inner() - Process command for the PCM substream.
+ * @substream: Kernel ALSA substream.
+ * @command: Substream command (SNDRV_PCM_TRIGGER_XXX).
+ *
+ * Context: Any context. Takes and releases the VirtIO substream spinlock.
+ * May take and release the tx/rx queue spinlock.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_pcm_trigger_inner(struct snd_pcm_substream *substream,
+ int command)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct virtio_snd_msg *msg;
+ int rc = 0;
+ bool running;
+ bool capture_stopping = false;
+ u64 capture_quiet_sequence = 0;
+
+ 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:
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ running = vss->device_running;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ vss->capture_stopping = false;
+ }
+ /* Preserve queued requests and unread RX data across native pause. */
+ rc = virtsnd_pcm_start_queue(vss, epoch);
+ if (rc) {
+ virtsnd_pcm_trigger_failed(vss, epoch);
+ return rc;
+ }
+
+ if (running) {
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return 0;
+ }
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_trigger_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_trigger_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->direction == SNDRV_PCM_STREAM_CAPTURE))
+ 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;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ capture_stopping = vss->capture_stopping;
+ capture_quiet_sequence = vss->capture_quiet_sequence;
+ vss->capture_stopping = true;
+ vss->capture_quiet_sequence =
+ vss->capture_submit_sequence;
+ }
vss->xfer_enabled = false;
+ if (!vss->device_running)
+ return 0;
}
- msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_STOP,
- GFP_KERNEL);
- if (!msg)
- return -ENOMEM;
+ rc = virtsnd_pcm_stop_device(vss, epoch);
+ if (rc && command != SNDRV_PCM_TRIGGER_PAUSE_PUSH) {
+ guard(spinlock_irqsave)(&vss->lock);
- return virtsnd_ctl_msg_send_sync(snd, msg);
+ if (vss->reset_epoch == epoch && !vss->resetting) {
+ vss->xfer_xrun = true;
+ substream->runtime->stop_operating = true;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+ }
+ if (rc && command == SNDRV_PCM_TRIGGER_PAUSE_PUSH) {
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->reset_epoch == epoch && !vss->resetting &&
+ vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ vss->capture_stopping = capture_stopping;
+ vss->capture_quiet_sequence = capture_quiet_sequence;
+ }
+ }
+ if (substream->runtime->state != SNDRV_PCM_STATE_RUNNING) {
+ /* Failed resume rollback must leave a paused queue closed. */
+ virtsnd_pcm_trigger_failed(vss, epoch);
+ } else if (virtsnd_pcm_start_queue(vss, epoch)) {
+ virtsnd_pcm_trigger_failed(vss, epoch);
+ } else if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+ }
+ return rc;
default:
return -EINVAL;
}
}
+static int virtsnd_pcm_start_admission(struct virtio_pcm_substream *vss)
+{
+ int rc = 0;
+
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting)
+ rc = -ECANCELED;
+ else if (vss->preparing)
+ rc = -EBADFD;
+ }
+
+ return rc;
+}
+
+/**
+ * virtsnd_pcm_trigger() - Defer a paused XRUN until linked resume completes.
+ * @substream: Kernel ALSA substream.
+ * @command: Substream command (SNDRV_PCM_TRIGGER_XXX).
+ *
+ * Context: Called with the PCM stream and linked-group locks held.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ u64 epoch;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ epoch = vss->reset_epoch;
+ if (command == SNDRV_PCM_TRIGGER_START ||
+ command == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) {
+ rc = virtsnd_pcm_start_admission(vss);
+ if (rc) {
+ return rc;
+ }
+ }
+
+ if (command == SNDRV_PCM_TRIGGER_PAUSE_RELEASE &&
+ virtsnd_pcm_defer_resume_xrun(vss, epoch)) {
+ return 0;
+ }
+
+ rc = virtsnd_pcm_trigger_inner(substream, command);
+ if (!rc && command == SNDRV_PCM_TRIGGER_PAUSE_PUSH)
+ substream->runtime->stop_operating = true;
+ if (command == SNDRV_PCM_TRIGGER_PAUSE_RELEASE &&
+ (rc == 0 || rc == -EPIPE) &&
+ virtsnd_pcm_defer_resume_xrun(vss, epoch))
+ rc = 0;
+
+ return rc;
+}
+
+static int
+virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ unsigned long 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;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ vss->capture_stopping = true;
+ vss->capture_quiet_sequence =
+ vss->capture_submit_sequence;
+ }
+ 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->release_required = 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_release_locked() - Retire the closed PCM generation.
* @substream: Kernel ALSA substream.
*
* The function can be called both from the upper level or from the driver
@@ -509,43 +1470,68 @@ static int virtsnd_pcm_trigger(struct snd_pcm_substream *substream, int command)
* 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_release_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct virtio_snd *snd = vss->snd;
struct virtio_snd_msg *msg;
- unsigned int js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
+ unsigned long js = msecs_to_jiffies(virtsnd_msg_timeout_ms);
bool need_release;
+ long waited;
int rc;
+ u64 epoch;
- virtsnd_pcm_block_ack(substream);
+ lockdep_assert_held(&vss->lifecycle_lock);
+ /* Keep late RX completions from requeuing work after cancellation. */
+ if (!vss->period_work_quiesced) {
+ disable_work_sync(&vss->elapsed_period);
+ vss->period_work_quiesced = true;
+ }
+ 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;
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ vss->capture_stopping = true;
+ vss->capture_quiet_sequence =
+ vss->capture_submit_sequence;
+ }
+ }
+ /* 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;
}
- cancel_work_sync(&vss->elapsed_period);
if (need_release) {
msg = virtsnd_pcm_ctl_msg_alloc(vss, VIRTIO_SND_R_PCM_RELEASE,
- GFP_KERNEL);
+ GFP_KERNEL, epoch);
if (!msg) {
- scoped_guard(pcm_stream_lock_irqsave, substream)
- substream->runtime->stop_operating = true;
+ substream->runtime->stop_operating = true;
return -ENOMEM;
}
- rc = virtsnd_ctl_msg_send_sync(snd, msg);
+ rc = virtsnd_pcm_send_sync(vss, msg);
if (rc) {
- scoped_guard(pcm_stream_lock_irqsave, substream)
- substream->runtime->stop_operating = true;
+ 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;
+ }
}
/*
@@ -554,24 +1540,58 @@ static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
* 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,
- !virtsnd_pcm_msg_pending_num(vss),
- js);
- if (rc <= 0) {
+ waited = wait_event_timeout(vss->msg_empty,
+ !virtsnd_pcm_msg_pending_num(vss),
+ js);
+ if (waited <= 0) {
dev_warn(&snd->vdev->dev, "SID %u: failed to flush I/O queue\n",
vss->sid);
- scoped_guard(pcm_stream_lock_irqsave, substream)
- substream->runtime->stop_operating = true;
+ substream->runtime->stop_operating = true;
- return !rc ? -ETIMEDOUT : rc;
+ return waited ? (int)waited : -ETIMEDOUT;
}
- 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;
}
+/* Close ACK admission before any sleep; prepare owns the gate through RESET. */
+static int virtsnd_pcm_sync_stop_locked(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ bool preparing;
+ int rc;
+
+ lockdep_assert_held(&vss->lifecycle_lock);
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ preparing = vss->preparing;
+ vss->preparing = true;
+ vss->prepared = false;
+ vss->xfer_enabled = false;
+ }
+ rc = virtsnd_pcm_release_locked(substream);
+ scoped_guard(pcm_stream_lock_irqsave, substream) {
+ guard(spinlock_irqsave)(&vss->lock);
+ vss->preparing = preparing;
+ }
+ return rc;
+}
+
+static snd_pcm_uframes_t
+virtsnd_pcm_cached_pointer(struct snd_pcm_runtime *runtime)
+{
+ if (!runtime->buffer_size)
+ return 0;
+ return runtime->status->hw_ptr % runtime->buffer_size;
+}
+
/**
* virtsnd_pcm_pb_pointer() - Get the current hardware position for the PCM
* substream for playback.
@@ -584,14 +1604,51 @@ static snd_pcm_uframes_t
virtsnd_pcm_pb_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ size_t hw_ptr;
+ bool paused_pointer = false;
+ bool defer_work = false;
- scoped_guard(spinlock_irqsave, &vss->lock)
- if (vss->xfer_xrun)
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (runtime->state == SNDRV_PCM_STATE_PAUSED) {
+ if (vss->xfer_xrun) {
+ virtsnd_pcm_defer_paused_xrun(vss);
+ defer_work = true;
+ }
+ paused_pointer = !vss->prepared || vss->preparing ||
+ vss->xfer_xrun;
+ } else if (!vss->prepared || vss->preparing || vss->xfer_xrun) {
return SNDRV_PCM_POS_XRUN;
+ }
+ if (!paused_pointer)
+ hw_ptr = vss->hw_ptr;
+ }
+ if (paused_pointer) {
+ if (defer_work)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return virtsnd_pcm_cached_pointer(runtime);
+ }
return snd_pcm_indirect_playback_pointer(substream,
- &vss->pcm_indirect,
- vss->hw_ptr);
+ &vss->pcm_indirect, hw_ptr);
+}
+
+/* Caller holds vss->lock. */
+static void virtsnd_pcm_capture_reset_pointer_locked(
+ struct virtio_pcm_substream *vss, struct snd_pcm_substream *substream,
+ u64 total)
+{
+ size_t period;
+
+ if (!vss->capture_reset_observing)
+ return;
+
+ virtsnd_pcm_capture_discard_completed(vss);
+
+ 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;
}
/**
@@ -606,14 +1663,78 @@ static snd_pcm_uframes_t
virtsnd_pcm_cp_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ size_t hw_ptr = 0;
+ u64 total;
+ bool free_running;
+ bool xrun = false;
+ bool paused = runtime->state == SNDRV_PCM_STATE_PAUSED;
+ bool paused_pointer = false;
+ bool defer_work = false;
- scoped_guard(spinlock_irqsave, &vss->lock)
- if (vss->xfer_xrun)
- return SNDRV_PCM_POS_XRUN;
-
- return snd_pcm_indirect_capture_pointer(substream,
- &vss->pcm_indirect,
- vss->hw_ptr);
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (paused) {
+ if (vss->xfer_xrun) {
+ virtsnd_pcm_defer_paused_xrun(vss);
+ defer_work = true;
+ }
+ paused_pointer = !vss->runtime_active || vss->preparing ||
+ vss->suspended ||
+ (!vss->prepared &&
+ !(virtsnd_pcm_pause_restart_enabled(vss) &&
+ vss->pause_restart_state ==
+ VIRTSND_PCM_PAUSE_RESTART_RELEASED)) ||
+ vss->xfer_xrun;
+ } else {
+ if (!vss->runtime_active || vss->preparing || vss->suspended ||
+ (!vss->prepared &&
+ !(virtsnd_pcm_pause_restart_enabled(vss) &&
+ vss->pause_restart_state ==
+ VIRTSND_PCM_PAUSE_RESTART_RELEASED)))
+ return SNDRV_PCM_POS_XRUN;
+ }
+ if (!paused_pointer) {
+ 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;
+ } else {
+ vss->capture_pointer_seen_total = total;
+ hw_ptr = virtsnd_pcm_ring_offset(
+ total, vss->buffer_bytes);
+ virtsnd_pcm_capture_reset_pointer_locked(
+ vss, substream, total);
+ }
+ }
+ if (xrun) {
+ vss->xfer_xrun = true;
+ if (paused) {
+ virtsnd_pcm_defer_paused_xrun(vss);
+ paused_pointer = true;
+ defer_work = true;
+ } else {
+ return SNDRV_PCM_POS_XRUN;
+ }
+ }
+ }
+ }
+ if (paused_pointer) {
+ if (defer_work)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return virtsnd_pcm_cached_pointer(runtime);
+ }
+ return bytes_to_frames(runtime, hw_ptr);
}
struct virtsnd_pcm_transfer {
@@ -733,12 +1854,84 @@ static void virtsnd_pcm_transfer_failed(struct virtio_pcm_substream *vss)
vss->substream->runtime->stop_operating = true;
}
+/* Caller holds vss->lock; a paused ACK must not trigger linked XRUN. */
+static int virtsnd_pcm_ack_xrun_locked(struct virtio_pcm_substream *vss,
+ struct snd_pcm_runtime *runtime)
+{
+ vss->xfer_xrun = true;
+ if (runtime->state == SNDRV_PCM_STATE_PAUSED) {
+ vss->xfer_enabled = false;
+ runtime->stop_operating = true;
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return -EBADFD;
+ }
+ return -EPIPE;
+}
+
+static int virtsnd_pcm_pb_ack_locked(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 = {};
+ bool paused;
+ int rc;
+
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ paused = runtime->state == SNDRV_PCM_STATE_PAUSED;
+ if (vss->preparing)
+ return -EBADFD;
+ /* A latched XRUN must not re-arm sync_stop once teardown began. */
+ if (vss->stopped && vss->xfer_xrun)
+ return -EBADFD;
+ if (vss->xfer_xrun) {
+ rc = virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ } else {
+ if (!vss->prepared || vss->stopped)
+ return -EBADFD;
+ transfer.indirect = vss->pcm_indirect;
+ rc = virtsnd_pcm_playback_ack_validate(vss, runtime,
+ &transfer.indirect);
+ if (rc == -EBADFD)
+ return -EBADFD;
+ if (!rc)
+ rc = snd_pcm_indirect_playback_transfer(substream,
+ &transfer.indirect,
+ virtsnd_pcm_trans_copy);
+ }
+ if (!rc)
+ rc = transfer.error;
+ if (rc) {
+ if (paused)
+ rc = virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ else
+ 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 paused ? -EBADFD : -EPIPE;
+ }
+
+ return 0;
+}
+
/* The caller holds the PCM lock, also used by DROP and state transitions. */
static int virtsnd_pcm_ack_state(struct snd_pcm_substream *substream)
{
switch (substream->runtime->state) {
case SNDRV_PCM_STATE_XRUN:
- /* The member is already XRUN; avoid stopping its linked group twice. */
+ /*
+ * pcm_lib_apply_appl_ptr() interprets -EPIPE as a new XRUN
+ * and calls __snd_pcm_xrun(), which stops the whole linked
+ * group. The stream is already XRUN; do not repeat that action.
+ */
return -EBADFD;
case SNDRV_PCM_STATE_PREPARED:
case SNDRV_PCM_STATE_RUNNING:
@@ -752,97 +1945,310 @@ static int virtsnd_pcm_ack_state(struct snd_pcm_substream *substream)
}
}
+/* The PCM lock serializes ACK with prepare's accounting reset. */
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);
- struct virtsnd_pcm_transfer transfer = {};
int rc;
+ if (vss->indirect_reset_observing)
+ return 0;
rc = virtsnd_pcm_ack_state(substream);
if (rc)
return rc;
+ return virtsnd_pcm_pb_ack_locked(substream);
+}
- scoped_guard(spinlock_irqsave, &queue->lock) {
- guard(spinlock)(&vss->lock);
- if (vss->preparing || !vss->prepared)
+static int virtsnd_pcm_capture_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_uframes_t appl_diff;
+ snd_pcm_sframes_t diff;
+ size_t bytes;
+ u64 outstanding;
+ u64 next_acked_total;
+ bool free_running;
+
+ if (!vss->runtime_active || vss->preparing || !runtime || !runtime->control ||
+ !runtime->dma_area || vss->suspended)
return -EBADFD;
- if (vss->stopped)
+ /* Preserve DRAINING tails only when no transfer XRUN is latched. */
+ if (vss->stopped && vss->xfer_xrun)
+ return -EBADFD;
+ if (vss->xfer_xrun)
+ return virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ if (!vss->prepared &&
+ !(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->stopped && 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;
+ 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)
+ return virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ appl_diff = diff;
+
+ if (!runtime->min_align) {
return -EBADFD;
- if (vss->xfer_xrun) {
- rc = -EPIPE;
- } else {
- transfer.indirect = vss->pcm_indirect;
- rc = virtsnd_pcm_playback_ack_validate(vss,
- substream->runtime,
- &transfer.indirect);
- if (rc == -EBADFD ||
- (rc && substream->runtime->state ==
- SNDRV_PCM_STATE_PAUSED))
- return -EBADFD;
- if (!rc)
- 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 (appl_diff > runtime->buffer_size)
+ return virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ if (appl_diff % runtime->min_align) {
+ return -EBADFD;
+ }
+ rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
+ &free_running);
+ if (rc || !virtsnd_pcm_capture_counters_valid(vss))
+ return virtsnd_pcm_ack_xrun_locked(vss, runtime);
+
+ /* ACK includes any subperiod tail already copied to the ring. */
+ bytes = frames_to_bytes(runtime, appl_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) {
+ return virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ }
+
+ vss->capture_acked_total = next_acked_total;
+ if (!free_running)
+ vss->capture_ready -= bytes;
+ vss->pcm_indirect.appl_ptr += appl_diff;
+ if (vss->pcm_indirect.appl_ptr >= runtime->boundary)
+ vss->pcm_indirect.appl_ptr -= runtime->boundary;
}
- if (rc) {
- dev_err(&vss->snd->vdev->dev,
- "SID %u: playback ACK transfer failed: %d\n",
- vss->sid, rc);
- return -EPIPE;
+
+ /* 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;
+ if (runtime->state == SNDRV_PCM_STATE_PAUSED)
+ virtsnd_pcm_ack_xrun_locked(vss, runtime);
+ 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 void virtsnd_pcm_legacy_capture_copy(struct snd_pcm_substream *substream,
- struct snd_pcm_indirect *rec, size_t bytes)
+static int virtsnd_pcm_cp_ack_locked(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ int rc = virtsnd_pcm_capture_ack(vss, substream->runtime);
- virtsnd_pcm_msg_send(vss, rec->sw_data, bytes);
+ if (rc == -EPIPE) {
+ guard(spinlock_irqsave)(&vss->lock);
+ virtsnd_pcm_transfer_failed(vss);
+ }
+ return rc;
}
+/* Every linked member's prepare closes the ACK gate under its PCM lock. */
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);
int rc;
+ /* RESET observes the pointer before returning discarded RX space. */
+ if (vss->indirect_reset_observing)
+ return 0;
rc = virtsnd_pcm_ack_state(substream);
if (rc)
return rc;
- guard(spinlock_irqsave)(&queue->lock);
- guard(spinlock)(&vss->lock);
- if (vss->preparing || !vss->prepared)
- return -EBADFD;
- if (vss->xfer_xrun)
- return -EPIPE;
- if (vss->stopped &&
- substream->runtime->state != SNDRV_PCM_STATE_DRAINING)
- return -EBADFD;
+ return virtsnd_pcm_cp_ack_locked(substream);
+}
- return snd_pcm_indirect_capture_transfer(substream, &vss->pcm_indirect,
- virtsnd_pcm_legacy_capture_copy);
+static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
+{
+ struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ bool teardown_pending;
+
+ 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.
+ */
+ guard(spinlock_irqsave)(&vss->lock);
+ teardown_pending = vss->prepared || vss->release_required ||
+ vss->msg_count || vss->device_running || vss->stopped;
+ 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)) {
+ /* Core may consume this marker before a later inactive teardown. */
+ if (teardown_pending)
+ substream->runtime->stop_operating = true;
+ return 0;
+ }
+ if (vss->resetting) {
+ substream->runtime->stop_operating = true;
+ return -ECANCELED;
+ }
+ }
+ return virtsnd_pcm_sync_stop_locked(substream);
}
-/**
- * virtsnd_pcm_reset() - Reset PCM pointers.
- * @substream: Kernel ALSA substream.
- *
- * Context: PCM stream lock held.
- * Return: 0 on success, -errno on failure.
- */
+static void virtsnd_pcm_capture_reset_error(struct virtio_pcm_substream *vss,
+ struct snd_pcm_runtime *runtime)
+{
+ size_t position = frames_to_bytes(runtime, runtime->status->hw_ptr);
+ size_t period = snd_pcm_lib_period_bytes(vss->substream);
+
+ vss->pcm_indirect.appl_ptr = runtime->status->hw_ptr;
+ vss->pcm_indirect.sw_ready = 0;
+ vss->hw_ptr = position;
+ 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_active = false;
+ if (vss->xfer_xrun) {
+ virtsnd_pcm_transfer_failed(vss);
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ }
+}
+
+/* The core holds the stream lock through the matching appl_ptr reset. */
static int virtsnd_pcm_reset(struct snd_pcm_substream *substream)
{
- return snd_pcm_lib_reset(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)
+ return rc;
+
+ guard(spinlock_irqsave)(&queue->lock);
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting || vss->xfer_xrun) {
+ virtsnd_pcm_capture_reset_error(vss, runtime);
+ 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 if (runtime->state == SNDRV_PCM_STATE_PAUSED) {
+ virtsnd_pcm_capture_discard_completed(vss);
+ /* Rebase paused RESET at the last sample actually committed. */
+ position = virtsnd_pcm_ring_offset(vss->capture_copied_total,
+ vss->buffer_bytes);
+ runtime->status->hw_ptr = bytes_to_frames(runtime, position);
+ vss->hw_ptr = position;
+ vss->capture_acked_total = vss->capture_copied_total;
+ vss->capture_pointer_seen_total = vss->capture_copied_total;
+ vss->capture_ready = 0;
+ period = snd_pcm_lib_period_bytes(substream);
+ vss->capture_notified_periods = period ?
+ div64_ul(vss->capture_copied_total, period) : 0;
+ } else {
+ virtsnd_pcm_capture_discard_completed(vss);
+ 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;
+}
+
+/* RESET can discard partial fills only after the transport returns ownership. */
+static int virtsnd_pcm_playback_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;
+ int rc;
+
+ /* The PCM lock excludes new ACK submissions through the entire reset. */
+ scoped_guard(spinlock_irqsave, &queue->lock) {
+ guard(spinlock)(&vss->lock);
+ if (vss->msg_count)
+ return -EBUSY;
+ }
+ vss->indirect_reset_observing = true;
+ rc = snd_pcm_lib_reset(substream);
+ vss->indirect_reset_observing = false;
+ if (rc)
+ return rc;
+ guard(spinlock_irqsave)(&queue->lock);
+ guard(spinlock)(&vss->lock);
+ position = frames_to_bytes(runtime, runtime->status->hw_ptr) %
+ vss->buffer_bytes;
+ vss->hw_ptr = position;
+ vss->pcm_indirect.hw_io = position;
+ vss->pcm_indirect.hw_data = position;
+ vss->pcm_indirect.sw_io = position;
+ vss->pcm_indirect.sw_data = position;
+ vss->pcm_indirect.hw_ready = 0;
+ vss->pcm_indirect.sw_ready = 0;
+ vss->pcm_indirect.appl_ptr = runtime->status->hw_ptr;
+ virtsnd_pcm_msg_reset(vss);
+ return 0;
}
/* PCM substream operators map. */
@@ -855,11 +2261,11 @@ const struct snd_pcm_ops virtsnd_pcm_ops[] = {
.hw_free = virtsnd_pcm_hw_free,
.prepare = virtsnd_pcm_prepare,
.prepare_complete = virtsnd_pcm_prepare_complete,
- .reset = virtsnd_pcm_reset,
.trigger = virtsnd_pcm_trigger,
.sync_stop = virtsnd_pcm_sync_stop,
.pointer = virtsnd_pcm_pb_pointer,
.ack = virtsnd_pcm_pb_ack,
+ .reset = virtsnd_pcm_playback_reset,
},
{
.open = virtsnd_pcm_open,
@@ -869,10 +2275,25 @@ const struct snd_pcm_ops virtsnd_pcm_ops[] = {
.hw_free = virtsnd_pcm_hw_free,
.prepare = virtsnd_pcm_prepare,
.prepare_complete = virtsnd_pcm_prepare_complete,
- .reset = virtsnd_pcm_reset,
.trigger = virtsnd_pcm_trigger,
.sync_stop = virtsnd_pcm_sync_stop,
.pointer = virtsnd_pcm_cp_pointer,
.ack = virtsnd_pcm_cp_ack,
},
};
+
+/* All capture access uses validated request-owned payloads. */
+static const struct snd_pcm_ops virtsnd_pcm_capture_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,
+ .prepare_complete = virtsnd_pcm_prepare_complete,
+ .trigger = virtsnd_pcm_trigger,
+ .sync_stop = virtsnd_pcm_sync_stop,
+ .pointer = virtsnd_pcm_cp_pointer,
+ .ack = virtsnd_pcm_cp_ack,
+};
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 12/20] ALSA: virtio: use request-owned capture and serialize recovery
2026-11-13 6:41 ` [PATCH v32 12/20] ALSA: virtio: use request-owned capture and serialize recovery Joonho Lee via B4 Relay
@ 2026-11-13 7:10 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:10 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 to consider:
- [Medium] ALSA: virtio: premature capture drain teardown in
virtsnd_pcm_sync_stop()
--
commit 9ad9852068461024197debdd2bd36f84e60f028e
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: use request-owned capture and serialize recovery
This commit switches VirtIO ALSA capture to request-owned buffers with
in-order completion commits and serializes PCM control transitions, reset
handling, and error recovery across substream generations.
Link: https://lore.kernel.org/linux-sound/sashiko-outbox-210240@kernel.org/
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index a5a682534acf7..442169a516676 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
[ ... ]
> +static int virtsnd_pcm_sync_stop(struct snd_pcm_substream *substream)
> +{
> + struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
> + bool teardown_pending;
> +
> + 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.
> + */
> + guard(spinlock_irqsave)(&vss->lock);
> + teardown_pending = vss->prepared || vss->release_required ||
> + vss->msg_count || vss->device_running || vss->stopped;
> + 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)) {
> + /* Core may consume this marker before a later inactive teardown. */
> + if (teardown_pending)
> + substream->runtime->stop_operating = true;
> + return 0;
> + }
[Severity: Medium]
Can a delayed virtsnd_pcm_sync_stop() tear down a replacement capture
generation while it is in SNDRV_PCM_STATE_DRAINING?
snd_pcm_drop() unlocks the PCM stream lock before calling
snd_pcm_sync_stop(). If another thread prepares, starts, and drains a
capture substream with unread frames before the first thread acquires
vss->lifecycle_lock, snd_pcm_drain() leaves the substream in
SNDRV_PCM_STATE_DRAINING with vss->prepared set so userspace can read the
remaining buffered frames and in-flight STOP tail completions:
CPU0 CPU1
snd_pcm_drop()
runtime->stop_operating = true
snd_pcm_stream_unlock_irq()
snd_pcm_sync_stop()
runtime->stop_operating = false
snd_pcm_prepare()
virtsnd_pcm_prepare_locked()
virtsnd_pcm_sync_stop_locked()
...
vss->prepared = true
snd_pcm_start()
snd_pcm_drain()
/* state = SNDRV_PCM_STATE_DRAINING */
virtsnd_pcm_sync_stop()
/* DRAINING capture not skipped */
virtsnd_pcm_sync_stop_locked()
vss->prepared = false <-- aborts active capture drain
Because the active-generation check only exempts SNDRV_PCM_STATE_DRAINING
for playback, virtsnd_pcm_sync_stop_locked() sends VIRTIO_SND_R_PCM_RELEASE,
disables vss->elapsed_period, and clears vss->prepared. Subsequent reads
and pointer updates during capture drain then see !vss->prepared in
virtsnd_pcm_capture_ack() and virtsnd_pcm_cp_pointer(), failing with -EBADFD
or SNDRV_PCM_POS_XRUN and discarding the unread capture data.
Allowing SNDRV_PCM_STATE_DRAINING for capture in the active-generation check
as well would keep the draining generation intact until its final teardown.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=12
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 13/20] ALSA: virtio: prioritize capture period work
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (11 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 12/20] ALSA: virtio: use request-owned capture and serialize recovery Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:50 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 14/20] docs: describe virtio-sound capture options Joonho Lee via B4 Relay
` (6 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Use system_highpri_wq for capture progress notifications. Capture now
always uses request-owned storage. Playback keeps the ordinary
workqueue. This preserves the priority proposal separately from recovery
and accounting. It provides no hard real-time guarantee and cannot
prevent reader overruns.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_pcm.h | 3 +++
1 file changed, 3 insertions(+)
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 95881a3a2c57..987105145205 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -173,6 +173,9 @@ virtsnd_pcm_defer_paused_xrun(struct virtio_pcm_substream *vss)
static inline bool
virtsnd_pcm_schedule_period_elapsed(struct virtio_pcm_substream *vss)
{
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ return queue_work(system_highpri_wq, &vss->elapsed_period);
+
return schedule_work(&vss->elapsed_period);
}
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 14/20] docs: describe virtio-sound capture options
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (12 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 13/20] ALSA: virtio: prioritize capture period work Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:52 ` sashiko-bot
2026-11-13 6:41 ` [PATCH v32 15/20] ALSA: virtio: reject invalid playback completions Joonho Lee via B4 Relay
` (5 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Document request-owned capture in the ALSA configuration guide. Describe
`capture_packed_status` as default-off, reading a packed trailer after
returned data while the fixed status scatterlist remains the default.
Describe default-off `capture_pause_restart`: pause uses STOP and
RELEASE, then resume uses PREPARE and START; disabled mode uses STOP and
START while retaining the prepared stream.
Note that on MMU systems request buffers are vmalloc-backed and
page-rounded individually. They remain allocated from hw_params through
hw_free, and capture data is copied into the ALSA ring.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
Documentation/admin-guide/kernel-parameters.txt | 14 ++++++++++++++
Documentation/sound/alsa-configuration.rst | 25 +++++++++++++++++++++++++
2 files changed, 39 insertions(+)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index e75344f4e0cd..ca3fd768b439 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -8344,6 +8344,20 @@ Kernel parameters
Can be used multiple times for multiple devices.
+ virtio_snd.capture_packed_status= [HW,ALSA]
+ Read an explicitly enabled packed status trailer after
+ the returned capture data. The fixed status scatterlist is
+ used by default.
+ Format: <bool>
+ Default: false.
+
+ virtio_snd.capture_pause_restart= [HW,ALSA]
+ Stop and release the stream when pausing capture. On
+ resume, send PREPARE and START. If disabled, pause uses
+ STOP and START while retaining the prepared stream.
+ Format: <bool>
+ Default: false.
+
vga= [BOOT,X86-32] Select a particular video mode
See Documentation/arch/x86/boot.rst and
Documentation/admin-guide/svga.rst.
diff --git a/Documentation/sound/alsa-configuration.rst b/Documentation/sound/alsa-configuration.rst
index 689eccf6a592..0531a3e3129c 100644
--- a/Documentation/sound/alsa-configuration.rst
+++ b/Documentation/sound/alsa-configuration.rst
@@ -2537,6 +2537,31 @@ midi_devs
This module supports multiple cards.
+Module virtio-snd
+-----------------
+
+Virtio-sound capture receives data into a separate request buffer for each
+PCM period. The driver validates the responses and appends frame-aligned audio
+to the ALSA ring in request order. Valid short responses are retained. Empty
+responses are accepted only during bounded startup or for requests already in
+flight at STOP or PAUSE.
+
+capture_packed_status
+ Read an explicitly enabled packed status trailer after returned capture
+ data. The fixed status scatterlist is used by default.
+ (default = false)
+
+capture_pause_restart
+ Stop and release capture on pause. Resume with PREPARE and START. When
+ disabled, pause uses STOP and START while retaining the prepared stream.
+ (default = false)
+
+On MMU systems, the request buffers are vmalloc-backed and page-rounded
+individually. Allocator rounding can make total additional memory exceed the
+ALSA ring size, especially with small periods. The request buffers remain
+allocated from hw_params until hw_free.
+Captured audio is copied from each request buffer into the ALSA ring.
+
Module snd-virtuoso
-------------------
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 15/20] ALSA: virtio: reject invalid playback completions
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (13 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 14/20] docs: describe virtio-sound capture options Joonho Lee via B4 Relay
@ 2026-11-13 6:41 ` Joonho Lee via B4 Relay
2026-11-13 6:59 ` sashiko-bot
2026-11-13 6:42 ` [PATCH v32 16/20] ALSA: pcm: add an explicit application pointer to indirect playback transfer Joonho Lee via B4 Relay
` (4 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:41 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
A TX completion currently advances the hardware position even when the
used length is malformed or the device reports an error. Validate the
fixed status response before publishing progress. Retire the request
once, latch XRUN and schedule error delivery while preparation is
active. Keep malformed or failed transfers out of successful playback
byte accounting.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_pcm_msg.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
index 3dfc45d90f08..9c67a8bf15f5 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -1011,6 +1011,17 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
/* Playback requests point directly at the ALSA ring. */
guard(spinlock)(&vss->lock);
+ if (written_bytes != sizeof(*msg->status) ||
+ le32_to_cpu(msg->status->status) != VIRTIO_SND_S_OK) {
+ vss->xfer_xrun = true;
+ msg->length = 0;
+ vss->msg_count--;
+ if (!vss->msg_count)
+ wake_up_all(&vss->msg_empty);
+ if (vss->prepared && !vss->preparing)
+ virtsnd_pcm_schedule_period_elapsed(vss);
+ return;
+ }
completed_bytes = msg->length;
latency_bytes = le32_to_cpu(msg->status->latency_bytes);
vss->hw_ptr += completed_bytes;
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 16/20] ALSA: pcm: add an explicit application pointer to indirect playback transfer
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (14 preceding siblings ...)
2026-11-13 6:41 ` [PATCH v32 15/20] ALSA: virtio: reject invalid playback completions Joonho Lee via B4 Relay
@ 2026-11-13 6:42 ` Joonho Lee via B4 Relay
2026-11-13 6:54 ` sashiko-bot
2026-11-13 6:42 ` [PATCH v32 17/20] ALSA: virtio: preserve playback progress across pause and buffer access Joonho Lee via B4 Relay
` (3 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:42 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
The indirect playback transfer helper samples runtime->control->appl_ptr
internally. A driver that needs to refill already committed data from a
pointer callback cannot pass the application-pointer snapshot used for
ACK validation.
Add snd_pcm_indirect_playback_transfer_appl_ptr() so callers can supply
that snapshot. Keep snd_pcm_indirect_playback_transfer() as a wrapper
that samples the runtime pointer as before, preserving existing callers.
The explicit-pointer form lets virtio refill accepted data without
accepting new application progress while the ALSA buffer-access gate is
closed.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
include/sound/pcm-indirect.h | 24 +++++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/include/sound/pcm-indirect.h b/include/sound/pcm-indirect.h
index 98e06ea73b2b..ed73bf8ef423 100644
--- a/include/sound/pcm-indirect.h
+++ b/include/sound/pcm-indirect.h
@@ -28,15 +28,17 @@ typedef void (*snd_pcm_indirect_copy_t)(struct snd_pcm_substream *substream,
struct snd_pcm_indirect *rec, size_t bytes);
/*
- * helper function for playback ack callback
+ * Playback transfer using a caller-supplied application pointer snapshot.
+ * Passing rec->appl_ptr refills previously accepted data without accepting
+ * additional application progress.
*/
static inline int
-snd_pcm_indirect_playback_transfer(struct snd_pcm_substream *substream,
- struct snd_pcm_indirect *rec,
- snd_pcm_indirect_copy_t copy)
+snd_pcm_indirect_playback_transfer_appl_ptr(struct snd_pcm_substream *substream,
+ struct snd_pcm_indirect *rec,
+ snd_pcm_indirect_copy_t copy,
+ snd_pcm_uframes_t appl_ptr)
{
struct snd_pcm_runtime *runtime = substream->runtime;
- snd_pcm_uframes_t appl_ptr = runtime->control->appl_ptr;
snd_pcm_sframes_t diff = appl_ptr - rec->appl_ptr;
int qsize;
@@ -74,6 +76,18 @@ snd_pcm_indirect_playback_transfer(struct snd_pcm_substream *substream,
return 0;
}
+/* Preserve the application pointer sampling of the ordinary ACK helper. */
+static inline int
+snd_pcm_indirect_playback_transfer(struct snd_pcm_substream *substream,
+ struct snd_pcm_indirect *rec,
+ snd_pcm_indirect_copy_t copy)
+{
+ snd_pcm_uframes_t appl_ptr = substream->runtime->control->appl_ptr;
+
+ return snd_pcm_indirect_playback_transfer_appl_ptr(substream, rec, copy,
+ appl_ptr);
+}
+
/*
* helper function for playback pointer callback
* ptr = current byte pointer
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 17/20] ALSA: virtio: preserve playback progress across pause and buffer access
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (15 preceding siblings ...)
2026-11-13 6:42 ` [PATCH v32 16/20] ALSA: pcm: add an explicit application pointer to indirect playback transfer Joonho Lee via B4 Relay
@ 2026-11-13 6:42 ` Joonho Lee via B4 Relay
2026-11-13 7:08 ` sashiko-bot
2026-11-13 6:42 ` [PATCH v32 18/20] ALSA: virtio: settle PCM control state from late replies Joonho Lee via B4 Relay
` (2 subsequent siblings)
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:42 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
TX completions can arrive after PAUSE closes transfer admission.
Preserve that progress and schedule catch-up when the stream resumes or
a rejected pause restores RUNNING. Cache device latency in stream-owned
state so a late completion does not need to dereference the ALSA
runtime. Clear both the cache and runtime delay on prepare and before
RESET observes the hardware pointer.
A playback pointer callback can also invoke ACK while the core has
closed buffer-access admission, after the pointer helper has already
accounted for completed hardware bytes. Track that callback context and
refill only previously committed data using pcm_indirect.appl_ptr. Keep
rejecting other ACKs while the gate is closed, and use the
snd_pcm_indirect_playback_transfer_appl_ptr() for the refill. If
runtime->control is absent, route the condition through the common
playback validator and XRUN error path rather than returning before
transfer failure handling.
Use the existing indirect hw_queue_size to keep outstanding TX below one
physical ring: buffer_bytes - period_bytes. Equal modulo cursors then
mean no unobserved completion, and a non-interrupt HWSYNC cannot consume
a hidden full-ring wrap before deferred period work. Continue using the
ordinary ALSA pointer update for silence, drain, stop-threshold and
wakeup policy.
Check for pending progress or a latched transfer error again in the
period worker under the PCM stream lock. A queued notification can
outlive a paused RESET or an earlier pointer observation. Calling the
period helper without new progress after resume can turn equal cursors
into a false wrap when the core has backdated its pointer timestamp.
This reduces queued buffering by one period. With two periods, only one
period can be queued.
The final candidate was tested on ARM64 QEMU with KASAN and lockdep.
RW/MMAP boundary pause, release, HWSYNC/poll, drop and reopen passed
with two- and eight-period rings. RW/MMAP also passed accepted RESET
from PREPARED followed by DRAIN and reopen, and accepted RESET while
RUNNING after the full 2,048-frame buffer became available.
A separate paired RW/MMAP gate fixture reproduced the baseline failure.
The generic ALSA indirect helper invokes ACK for each pointer callback
but masks its internal -EBADFD. Once hw_ptr == hw_io with no completion
outstanding, virtio period-work progress suppresses further callbacks,
so there is no ACK to retry the refill. Baseline stopped after 3,072
completed bytes with 1,024 bytes in sw_ready. The candidate drained
sw_ready and completed 4,096 bytes without any further client ACK. A
test-only hook changed appl_ptr to 2,560 while only 2,048 frames were
committed; the candidate refilled only committed data. The hostile
mutation used a test-only kernel hook because ARM64 control-page mmap is
unsupported. KASAN and lockdep reported no diagnostics.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
sound/virtio/virtio_pcm.c | 3 ++
sound/virtio/virtio_pcm.h | 13 +++++++++
sound/virtio/virtio_pcm_msg.c | 8 ++----
sound/virtio/virtio_pcm_ops.c | 67 ++++++++++++++++++++++++++++++++-----------
4 files changed, 69 insertions(+), 22 deletions(-)
diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c
index 7b332c9476f1..38795c4acb01 100644
--- a/sound/virtio/virtio_pcm.c
+++ b/sound/virtio/virtio_pcm.c
@@ -338,6 +338,9 @@ static void virtsnd_pcm_period_elapsed(struct work_struct *work)
}
if (vss->direction != SNDRV_PCM_STREAM_CAPTURE) {
+ /* Ignore stale work after RESET rebases the playback cursors. */
+ if (!virtsnd_pcm_playback_progress_pending(vss))
+ return;
snd_pcm_period_elapsed_under_stream_lock(substream);
return;
}
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 987105145205..0c625df1f4e4 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -53,6 +53,7 @@ enum virtio_pcm_pause_restart_state {
* substream operators, including @stopped.
* @buffer_bytes: Current buffer size in bytes.
* @hw_ptr: Substream hardware pointer value in bytes [0 ... buffer_bytes).
+ * @playback_latency_bytes: Device delay cached independently of the ALSA runtime.
* @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.
@@ -84,6 +85,8 @@ enum virtio_pcm_pause_restart_state {
* @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.
+ * @playback_pointer_active: Internal playback ACK context; 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.
@@ -113,6 +116,7 @@ struct virtio_pcm_substream {
bool resetting;
size_t buffer_bytes;
size_t hw_ptr;
+ u32 playback_latency_bytes;
size_t capture_ready;
snd_pcm_uframes_t capture_boundary;
u64 capture_copied_total;
@@ -140,6 +144,7 @@ struct virtio_pcm_substream {
bool control_uncertain;
bool capture_reset_observing;
bool indirect_reset_observing;
+ bool playback_pointer_active;
bool suspended;
enum virtio_pcm_pause_restart_state pause_restart_state;
struct virtio_pcm_msg **msgs;
@@ -179,6 +184,14 @@ virtsnd_pcm_schedule_period_elapsed(struct virtio_pcm_substream *vss)
return schedule_work(&vss->elapsed_period);
}
+/* Caller holds the PCM stream lock so RESET cannot rebase these cursors. */
+static inline bool
+virtsnd_pcm_playback_progress_pending(struct virtio_pcm_substream *vss)
+{
+ guard(spinlock_irqsave)(&vss->lock);
+ return vss->xfer_xrun || vss->hw_ptr != vss->pcm_indirect.hw_io;
+}
+
/*
* ALSA may observe a subperiod tail before its period callback. Keep actual
* unread occupancy separate from pointer observations and notification state;
diff --git a/sound/virtio/virtio_pcm_msg.c b/sound/virtio/virtio_pcm_msg.c
index 9c67a8bf15f5..44761dff3e0d 100644
--- a/sound/virtio/virtio_pcm_msg.c
+++ b/sound/virtio/virtio_pcm_msg.c
@@ -1033,12 +1033,10 @@ static void virtsnd_pcm_msg_complete(struct virtio_pcm_msg *msg,
/* Keep latched errors until prepare establishes a fresh epoch. */
vss->msg_count--;
- if (vss->xfer_enabled) {
- struct snd_pcm_runtime *runtime = vss->substream->runtime;
-
- runtime->delay = bytes_to_frames(runtime, latency_bytes);
+ /* A queued request can outlive the ALSA runtime after hw_free. */
+ vss->playback_latency_bytes = latency_bytes;
+ if (vss->prepared && !vss->preparing)
virtsnd_pcm_schedule_period_elapsed(vss);
- }
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
index 442169a51667..4a10cd3cc57c 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -770,6 +770,13 @@ static int virtsnd_pcm_prepare_locked(struct snd_pcm_substream *substream)
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->direction == SNDRV_PCM_STREAM_PLAYBACK) {
+ /* Keep unobserved completions below one physical ring. */
+ vss->pcm_indirect.hw_queue_size = vss->buffer_bytes -
+ snd_pcm_lib_period_bytes(substream);
+ vss->playback_latency_bytes = 0;
+ substream->runtime->delay = 0;
+ }
if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
vss->capture_boundary = substream->runtime->boundary;
rc = virtsnd_pcm_capture_refresh_mode_locked(vss,
@@ -1200,7 +1207,8 @@ static int virtsnd_pcm_trigger_inner(struct snd_pcm_substream *substream,
}
if (running) {
- if (vss->direction == SNDRV_PCM_STREAM_CAPTURE)
+ if (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss))
virtsnd_pcm_schedule_period_elapsed(vss);
return 0;
}
@@ -1222,7 +1230,8 @@ static int virtsnd_pcm_trigger_inner(struct snd_pcm_substream *substream,
return -ECANCELED;
vss->device_running = true;
}
- if (!rc && (vss->direction == SNDRV_PCM_STREAM_CAPTURE))
+ if (!rc && (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss)))
virtsnd_pcm_schedule_period_elapsed(vss);
return rc;
case SNDRV_PCM_TRIGGER_SUSPEND:
@@ -1272,7 +1281,8 @@ static int virtsnd_pcm_trigger_inner(struct snd_pcm_substream *substream,
virtsnd_pcm_trigger_failed(vss, epoch);
} else if (virtsnd_pcm_start_queue(vss, epoch)) {
virtsnd_pcm_trigger_failed(vss, epoch);
- } else if (vss->direction == SNDRV_PCM_STREAM_CAPTURE) {
+ } else if (vss->direction == SNDRV_PCM_STREAM_CAPTURE ||
+ virtsnd_pcm_playback_progress_pending(vss)) {
virtsnd_pcm_schedule_period_elapsed(vss);
}
}
@@ -1605,6 +1615,7 @@ virtsnd_pcm_pb_pointer(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
+ snd_pcm_uframes_t position;
size_t hw_ptr;
bool paused_pointer = false;
bool defer_work = false;
@@ -1622,6 +1633,7 @@ virtsnd_pcm_pb_pointer(struct snd_pcm_substream *substream)
}
if (!paused_pointer)
hw_ptr = vss->hw_ptr;
+ runtime->delay = bytes_to_frames(runtime, vss->playback_latency_bytes);
}
if (paused_pointer) {
if (defer_work)
@@ -1629,8 +1641,11 @@ virtsnd_pcm_pb_pointer(struct snd_pcm_substream *substream)
return virtsnd_pcm_cached_pointer(runtime);
}
- return snd_pcm_indirect_playback_pointer(substream,
- &vss->pcm_indirect, hw_ptr);
+ vss->playback_pointer_active = true;
+ position = snd_pcm_indirect_playback_pointer(substream,
+ &vss->pcm_indirect, hw_ptr);
+ vss->playback_pointer_active = false;
+ return position;
}
/* Caller holds vss->lock. */
@@ -1745,9 +1760,9 @@ struct virtsnd_pcm_transfer {
/* Caller holds @vss->lock and the PCM stream lock. */
static int virtsnd_pcm_playback_ack_validate(struct virtio_pcm_substream *vss,
struct snd_pcm_runtime *runtime,
- const struct snd_pcm_indirect *rec)
+ const struct snd_pcm_indirect *rec,
+ snd_pcm_uframes_t appl_ptr)
{
- snd_pcm_uframes_t appl_ptr;
snd_pcm_uframes_t appl_diff;
snd_pcm_uframes_t signed_max;
snd_pcm_uframes_t back;
@@ -1761,7 +1776,7 @@ static int virtsnd_pcm_playback_ack_validate(struct virtio_pcm_substream *vss,
unsigned int hw_queue_size;
if (!runtime->control || !runtime->boundary ||
- runtime->control->appl_ptr >= runtime->boundary ||
+ appl_ptr >= runtime->boundary ||
rec->appl_ptr >= runtime->boundary)
return -EPIPE;
@@ -1776,7 +1791,6 @@ static int virtsnd_pcm_playback_ack_validate(struct virtio_pcm_substream *vss,
return -EPIPE;
/* Match the indirect helper's appl_ptr boundary-wrap calculation. */
- appl_ptr = runtime->control->appl_ptr;
if (appl_ptr >= rec->appl_ptr) {
appl_diff = appl_ptr - rec->appl_ptr;
} else {
@@ -1868,7 +1882,8 @@ static int virtsnd_pcm_ack_xrun_locked(struct virtio_pcm_substream *vss,
return -EPIPE;
}
-static int virtsnd_pcm_pb_ack_locked(struct snd_pcm_substream *substream)
+static int virtsnd_pcm_pb_ack_locked(struct snd_pcm_substream *substream,
+ snd_pcm_uframes_t appl_ptr)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
@@ -1892,13 +1907,13 @@ static int virtsnd_pcm_pb_ack_locked(struct snd_pcm_substream *substream)
return -EBADFD;
transfer.indirect = vss->pcm_indirect;
rc = virtsnd_pcm_playback_ack_validate(vss, runtime,
- &transfer.indirect);
+ &transfer.indirect, appl_ptr);
if (rc == -EBADFD)
return -EBADFD;
if (!rc)
- rc = snd_pcm_indirect_playback_transfer(substream,
- &transfer.indirect,
- virtsnd_pcm_trans_copy);
+ rc = snd_pcm_indirect_playback_transfer_appl_ptr(
+ substream, &transfer.indirect,
+ virtsnd_pcm_trans_copy, appl_ptr);
}
if (!rc)
rc = transfer.error;
@@ -1937,8 +1952,6 @@ static int virtsnd_pcm_ack_state(struct snd_pcm_substream *substream)
case SNDRV_PCM_STATE_RUNNING:
case SNDRV_PCM_STATE_PAUSED:
case SNDRV_PCM_STATE_DRAINING:
- if (atomic_read(&substream->runtime->buffer_accessing) < 0)
- return -EBADFD;
return 0;
default:
return -EBADFD;
@@ -1949,6 +1962,8 @@ static int virtsnd_pcm_ack_state(struct snd_pcm_substream *substream)
static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
{
struct virtio_pcm_substream *vss = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *runtime = substream->runtime;
+ snd_pcm_uframes_t appl_ptr;
int rc;
if (vss->indirect_reset_observing)
@@ -1956,7 +1971,21 @@ static int virtsnd_pcm_pb_ack(struct snd_pcm_substream *substream)
rc = virtsnd_pcm_ack_state(substream);
if (rc)
return rc;
- return virtsnd_pcm_pb_ack_locked(substream);
+ if (atomic_read(&runtime->buffer_accessing) < 0) {
+ if (!vss->playback_pointer_active)
+ return -EBADFD;
+ /*
+ * Pointer accounting already consumed completed hardware bytes.
+ * Refill committed data without admitting a new application
+ * pointer while a buffer operation has closed ACK admission.
+ */
+ appl_ptr = vss->pcm_indirect.appl_ptr;
+ } else if (runtime->control) {
+ appl_ptr = READ_ONCE(runtime->control->appl_ptr);
+ } else {
+ appl_ptr = 0;
+ }
+ return virtsnd_pcm_pb_ack_locked(substream, appl_ptr);
}
static int virtsnd_pcm_capture_ack(struct virtio_pcm_substream *vss,
@@ -2087,6 +2116,8 @@ static int virtsnd_pcm_cp_ack(struct snd_pcm_substream *substream)
rc = virtsnd_pcm_ack_state(substream);
if (rc)
return rc;
+ if (atomic_read(&substream->runtime->buffer_accessing) < 0)
+ return -EBADFD;
return virtsnd_pcm_cp_ack_locked(substream);
}
@@ -2229,6 +2260,8 @@ static int virtsnd_pcm_playback_reset(struct snd_pcm_substream *substream)
guard(spinlock)(&vss->lock);
if (vss->msg_count)
return -EBUSY;
+ vss->playback_latency_bytes = 0;
+ runtime->delay = 0;
}
vss->indirect_reset_observing = true;
rc = snd_pcm_lib_reset(substream);
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 17/20] ALSA: virtio: preserve playback progress across pause and buffer access
2026-11-13 6:42 ` [PATCH v32 17/20] ALSA: virtio: preserve playback progress across pause and buffer access Joonho Lee via B4 Relay
@ 2026-11-13 7:08 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:08 UTC (permalink / raw)
To: Joonho Lee; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
> From: Joonho Lee <mynameisjune111@gmail.com>
>
> TX completions can arrive after PAUSE closes transfer admission.
> Preserve that progress and schedule catch-up when the stream resumes or
> a rejected pause restores RUNNING. Cache device latency in stream-owned
> state so a late completion does not need to dereference the ALSA
> runtime. Clear both the cache and runtime delay on prepare and before
> RESET observes the hardware pointer.
>
> A playback pointer callback can also invoke ACK while the core has
> closed buffer-access admission, after the pointer helper has already
> accounted for completed hardware bytes. Track that callback context and
> refill only previously committed data using pcm_indirect.appl_ptr. Keep
> rejecting other ACKs while the gate is closed, and use the
> snd_pcm_indirect_playback_transfer_appl_ptr() for the refill. If
> [ ... ]
> mutation used a test-only kernel hook because ARM64 control-page mmap is
> unsupported. KASAN and lockdep reported no diagnostics.
>
> 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/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=17
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 18/20] ALSA: virtio: settle PCM control state from late replies
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (16 preceding siblings ...)
2026-11-13 6:42 ` [PATCH v32 17/20] ALSA: virtio: preserve playback progress across pause and buffer access Joonho Lee via B4 Relay
@ 2026-11-13 6:42 ` Joonho Lee via B4 Relay
2026-11-13 7:26 ` sashiko-bot
2026-11-13 6:42 ` [PATCH v32 19/20] ALSA: pcm: allow stream suspension without power management Joonho Lee via B4 Relay
2026-11-13 6:42 ` [PATCH v32 20/20] ALSA: virtio: reset the device when PCM state stays unknown Joonho Lee via B4 Relay
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:42 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
A native PCM control request that times out remains owned by the device.
The driver therefore stops sending PCM control requests for the stream
until the device state is known. Only a device reset cleared that state,
and a reset happens only on remove or freeze. One reply arriving after
msg_timeout_ms left open, hw_params, prepare and trigger returning -EIO
until the driver was rebound or the guest resumed from suspend.
Keep the ordering guarantee, but let the reply resolve it. After a
synchronous wait times out, decide under the control queue lock whether
the reply has already completed. If not, mark the request late and count
it on the substream. When the device completes the request, apply its
result to the native device state and clear the unknown state once no
late request remains:
- PREPARE: success requires a later RELEASE
- START: success leaves the device running
- STOP: success stops the device
- RELEASE: success releases the stream
- an error reply leaves the state unchanged
Trigger still fails while the state is unknown, so it does not hold the
PCM stream lock any longer. hw_params, hw_free, prepare and open wait,
killably, for up to four message timeouts before tearing down or
preparing the stream, then continue from the reported state. A device
reset still clears the state. The opt-in capture_pause_restart mode keeps
its reset-required state.
Log when a reply is late, when it settles, and when it remains
outstanding after the wait.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
Documentation/sound/alsa-configuration.rst | 6 ++
sound/virtio/virtio_card.c | 3 +
sound/virtio/virtio_ctl_msg.c | 87 ++++++++++++++----
sound/virtio/virtio_ctl_msg.h | 2 +
sound/virtio/virtio_pcm.c | 1 +
sound/virtio/virtio_pcm.h | 13 ++-
sound/virtio/virtio_pcm_ops.c | 137 ++++++++++++++++++++++++++++-
7 files changed, 229 insertions(+), 20 deletions(-)
diff --git a/Documentation/sound/alsa-configuration.rst b/Documentation/sound/alsa-configuration.rst
index 0531a3e3129c..df9c048fcb9b 100644
--- a/Documentation/sound/alsa-configuration.rst
+++ b/Documentation/sound/alsa-configuration.rst
@@ -2562,6 +2562,12 @@ ALSA ring size, especially with small periods. The request buffers remain
allocated from hw_params until hw_free.
Captured audio is copied from each request buffer into the ALSA ring.
+A PCM control request that is not answered within ``msg_timeout_ms`` remains
+owned by the device. The driver sends no further PCM control request for that
+stream until the reply arrives. Trigger operations fail meanwhile; hw_params,
+hw_free and prepare wait up to four timeouts for the reply and continue from
+the device state it reports.
+
Module snd-virtuoso
-------------------
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
index 336da9a5b8fe..3f2242f27440 100644
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -382,9 +382,12 @@ static void virtsnd_reset_vqs(struct virtio_snd *snd)
vss->device_running = false;
vss->release_required = false;
vss->control_uncertain = false;
+ vss->control_late = 0;
vss->capture_reset_observing = false;
vss->xfer_enabled = false;
vss->suspended = true;
+ /* Waiters observe resetting and return instead of timing out. */
+ wake_up_all(&vss->control_wait);
}
virtio_reset_device(vdev);
diff --git a/sound/virtio/virtio_ctl_msg.c b/sound/virtio/virtio_ctl_msg.c
index d7480253a5f5..97c5f27e5cbc 100644
--- a/sound/virtio/virtio_ctl_msg.c
+++ b/sound/virtio/virtio_ctl_msg.c
@@ -27,6 +27,8 @@
* @pcm_epoch: Operation generation captured before request allocation.
* @submitted: Request was handed to the device.
* @cancelled: Request canceled during device teardown.
+ * @late: The sender stopped waiting while the device still owned the request;
+ * completion settles the PCM state. Protected by the control queue lock.
*/
struct virtio_snd_msg {
struct scatterlist sg_request;
@@ -41,6 +43,7 @@ struct virtio_snd_msg {
refcount_t ref_count;
bool cancelled;
bool submitted;
+ bool late;
struct virtio_pcm_substream *pcm;
u64 pcm_epoch;
};
@@ -294,6 +297,32 @@ struct virtio_snd_msg *virtsnd_ctl_msg_alloc(size_t request_size,
return msg;
}
+/**
+ * virtsnd_ctl_msg_status() - Convert a completed response to an errno value.
+ * @msg: Completed control message.
+ *
+ * Context: Any context.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_ctl_msg_status(struct virtio_snd_msg *msg)
+{
+ struct virtio_snd_hdr *response = virtsnd_ctl_msg_response(msg);
+
+ if (READ_ONCE(msg->cancelled))
+ return -ENODEV;
+
+ switch (le32_to_cpu(response->code)) {
+ case VIRTIO_SND_S_OK:
+ return 0;
+ case VIRTIO_SND_S_NOT_SUPP:
+ return -EOPNOTSUPP;
+ case VIRTIO_SND_S_IO_ERR:
+ return -EIO;
+ default:
+ return -EINVAL;
+ }
+}
+
/**
* virtsnd_ctl_msg_send() - Send a control message.
* @snd: VirtIO sound device.
@@ -382,24 +411,7 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
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;
- }
- }
+ rc = virtsnd_ctl_msg_status(msg);
}
}
@@ -419,11 +431,50 @@ int virtsnd_ctl_msg_send(struct virtio_snd *snd, struct virtio_snd_msg *msg,
void virtsnd_ctl_msg_complete(struct virtio_snd_msg *msg)
{
list_del(&msg->list);
+ if (msg->late) {
+ struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg);
+
+ msg->late = false;
+ virtsnd_pcm_ctl_settle_locked(msg->pcm, msg->pcm_epoch,
+ le32_to_cpu(request->code),
+ virtsnd_ctl_msg_status(msg));
+ }
complete(&msg->notify);
virtsnd_ctl_msg_unref(msg);
}
+/**
+ * virtsnd_ctl_msg_defer() - Hand a timed-out PCM request to its late reply.
+ * @snd: VirtIO sound device.
+ * @msg: PCM control message whose synchronous wait timed out.
+ *
+ * The device still owns a request after its sender times out. The control
+ * queue lock orders this decision against completion: either the reply has
+ * already arrived and its status is returned, or the eventual completion
+ * settles the PCM state. Until then, the substream does not send another
+ * PCM control request.
+ *
+ * Context: Any context. The caller holds a message reference.
+ * Return: The reply status if it already completed, -ENODEV if the request
+ * was canceled, -ECANCELED if the PCM generation was reset, or
+ * -ETIMEDOUT if the reply is still pending.
+ */
+int virtsnd_ctl_msg_defer(struct virtio_snd *snd, struct virtio_snd_msg *msg)
+{
+ struct virtio_snd_queue *queue = virtsnd_control_queue(snd);
+
+ guard(spinlock_irqsave)(&queue->lock);
+ if (completion_done(&msg->notify) || READ_ONCE(msg->cancelled))
+ return virtsnd_ctl_msg_status(msg);
+ if (!msg->pcm)
+ return -ETIMEDOUT;
+ if (!virtsnd_pcm_ctl_defer_locked(msg->pcm, msg->pcm_epoch))
+ return -ECANCELED;
+ msg->late = true;
+ return -ETIMEDOUT;
+}
+
/**
* virtsnd_ctl_msg_cancel_all() - Cancel all pending control messages.
* @snd: VirtIO sound device.
diff --git a/sound/virtio/virtio_ctl_msg.h b/sound/virtio/virtio_ctl_msg.h
index b92c41c0c1a6..9cf8461736bb 100644
--- a/sound/virtio/virtio_ctl_msg.h
+++ b/sound/virtio/virtio_ctl_msg.h
@@ -84,6 +84,8 @@ static inline int virtsnd_ctl_msg_send_async(struct virtio_snd *snd,
return virtsnd_ctl_msg_send(snd, msg, true);
}
+int virtsnd_ctl_msg_defer(struct virtio_snd *snd, struct virtio_snd_msg *msg);
+
void virtsnd_ctl_msg_cancel_all(struct virtio_snd *snd);
void virtsnd_ctl_msg_cleanup(struct virtio_snd *snd);
diff --git a/sound/virtio/virtio_pcm.c b/sound/virtio/virtio_pcm.c
index 38795c4acb01..8db0cdde3ca6 100644
--- a/sound/virtio/virtio_pcm.c
+++ b/sound/virtio/virtio_pcm.c
@@ -445,6 +445,7 @@ int virtsnd_pcm_parse_cfg(struct virtio_snd *snd)
INIT_WORK(&vss->elapsed_period, virtsnd_pcm_period_elapsed);
disable_work(&vss->elapsed_period);
init_waitqueue_head(&vss->msg_empty);
+ init_waitqueue_head(&vss->control_wait);
spin_lock_init(&vss->lock);
mutex_init(&vss->lifecycle_lock);
}
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 0c625df1f4e4..4ead0d94af63 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -81,7 +81,10 @@ enum virtio_pcm_pause_restart_state {
* the PCM stream lock and @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.
+ * @control_uncertain: A native control result is not yet known; no further
+ * PCM control request is sent until it is.
+ * @control_late: Timed-out native control requests still owned by the device.
+ * Their replies settle the device state.
* @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.
@@ -98,6 +101,7 @@ enum virtio_pcm_pause_restart_state {
* @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.
+ * @control_wait: Notify when native control state becomes known.
*/
struct virtio_pcm_substream {
struct virtio_snd *snd;
@@ -142,6 +146,7 @@ struct virtio_pcm_substream {
bool device_running;
bool release_required;
bool control_uncertain;
+ unsigned int control_late;
bool capture_reset_observing;
bool indirect_reset_observing;
bool playback_pointer_active;
@@ -157,6 +162,7 @@ struct virtio_pcm_substream {
size_t owned_dma_bytes;
#endif
wait_queue_head_t msg_empty;
+ wait_queue_head_t control_wait;
};
/* Caller holds the PCM stream lock and vss->lock. */
@@ -279,6 +285,11 @@ struct virtio_snd_msg *
virtsnd_pcm_ctl_msg_alloc(struct virtio_pcm_substream *vss,
unsigned int command, gfp_t gfp, u64 epoch);
+bool virtsnd_pcm_ctl_defer_locked(struct virtio_pcm_substream *vss, u64 epoch);
+
+void virtsnd_pcm_ctl_settle_locked(struct virtio_pcm_substream *vss, u64 epoch,
+ u32 command, int status);
+
int virtsnd_pcm_msg_alloc(struct virtio_pcm_substream *vss,
unsigned int periods, unsigned int period_bytes);
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
index 4a10cd3cc57c..15d4473f4c0a 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -157,10 +157,79 @@ static bool virtsnd_pcm_pause_restart_transitioning(enum virtio_pcm_pause_restar
}
}
+/**
+ * virtsnd_pcm_ctl_defer_locked() - Record a native control request whose
+ * sender timed out.
+ * @vss: VirtIO PCM substream.
+ * @epoch: Generation of the request.
+ *
+ * Context: Control queue lock held with interrupts disabled.
+ * Return: false if the generation was reset, true otherwise.
+ */
+bool virtsnd_pcm_ctl_defer_locked(struct virtio_pcm_substream *vss, u64 epoch)
+{
+ guard(spinlock)(&vss->lock);
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return false;
+ vss->control_late++;
+ vss->control_uncertain = true;
+ return true;
+}
+
+/**
+ * virtsnd_pcm_ctl_settle_locked() - Apply a late native control reply.
+ * @vss: VirtIO PCM substream.
+ * @epoch: Generation of the request.
+ * @command: Control request code (VIRTIO_SND_R_PCM_XXX).
+ * @status: Reply status converted to an errno value.
+ *
+ * No other native control request is sent while one is outstanding, so the
+ * reply determines the device state. A failed request leaves it unchanged.
+ *
+ * Context: Control queue lock held with interrupts disabled.
+ */
+void virtsnd_pcm_ctl_settle_locked(struct virtio_pcm_substream *vss, u64 epoch,
+ u32 command, int status)
+{
+ scoped_guard(spinlock, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch ||
+ !vss->control_late)
+ return;
+ if (!status) {
+ switch (command) {
+ case VIRTIO_SND_R_PCM_PREPARE:
+ vss->release_required = true;
+ break;
+ case VIRTIO_SND_R_PCM_START:
+ vss->device_running = true;
+ break;
+ case VIRTIO_SND_R_PCM_STOP:
+ vss->device_running = false;
+ break;
+ case VIRTIO_SND_R_PCM_RELEASE:
+ vss->release_required = false;
+ vss->device_running = false;
+ break;
+ }
+ }
+ if (--vss->control_late)
+ return;
+ vss->control_uncertain = false;
+ wake_up_all(&vss->control_wait);
+ }
+
+ dev_info_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: late PCM control 0x%08x reply settled (%d)\n",
+ vss->sid, command, status);
+}
+
static int virtsnd_pcm_send_sync(struct virtio_pcm_substream *vss,
struct virtio_snd_msg *msg)
{
+ struct virtio_snd_hdr *request = virtsnd_ctl_msg_request(msg);
u64 epoch = virtsnd_ctl_msg_pcm_epoch(msg);
+ u32 command = le32_to_cpu(request->code);
+ bool deferred = false;
bool uncertain;
int rc;
@@ -174,7 +243,12 @@ static int virtsnd_pcm_send_sync(struct virtio_pcm_substream *vss,
}
virtsnd_ctl_msg_ref(msg);
rc = virtsnd_ctl_msg_send_sync(vss->snd, msg);
- uncertain = virtsnd_ctl_msg_uncertain(msg, rc);
+ if (rc == -ETIMEDOUT) {
+ /* The device still owns the request; its reply settles the state. */
+ rc = virtsnd_ctl_msg_defer(vss->snd, msg);
+ deferred = rc == -ETIMEDOUT;
+ }
+ uncertain = !deferred && virtsnd_ctl_msg_uncertain(msg, rc);
virtsnd_ctl_msg_unref(msg);
scoped_guard(spinlock_irqsave, &vss->lock) {
if (epoch != vss->reset_epoch || vss->resetting)
@@ -182,9 +256,63 @@ static int virtsnd_pcm_send_sync(struct virtio_pcm_substream *vss,
if (rc && uncertain)
vss->control_uncertain = true;
}
+ if (deferred)
+ dev_warn_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: PCM control 0x%08x reply is late; holding PCM commands\n",
+ vss->sid, command);
return rc;
}
+/* Sleepable operations wait this many message timeouts for a late reply. */
+#define VIRTSND_PCM_SETTLE_TIMEOUTS 4
+
+static unsigned long virtsnd_pcm_settle_jiffies(void)
+{
+ u64 ms = (u64)virtsnd_msg_timeout_ms * VIRTSND_PCM_SETTLE_TIMEOUTS;
+
+ return msecs_to_jiffies(min_t(u64, ms, UINT_MAX));
+}
+
+static bool virtsnd_pcm_control_settled(struct virtio_pcm_substream *vss)
+{
+ guard(spinlock_irqsave)(&vss->lock);
+ return vss->resetting || !vss->control_uncertain;
+}
+
+/**
+ * virtsnd_pcm_control_settle() - Wait for a late native control reply.
+ * @vss: VirtIO PCM substream.
+ *
+ * Trigger fails while a native control result is unknown. Sleepable
+ * operations wait a bounded time for the reply, then continue from the
+ * device state it reports.
+ *
+ * Context: Process context with @vss->lifecycle_lock held.
+ * Return: 0 if the state is known or a device reset is in progress, -EIO if
+ * the reply is still outstanding, or -ERESTARTSYS if killed.
+ */
+static int virtsnd_pcm_control_settle(struct virtio_pcm_substream *vss)
+{
+ long rc;
+
+ lockdep_assert_held(&vss->lifecycle_lock);
+ if (virtsnd_pcm_control_settled(vss))
+ return 0;
+
+ rc = wait_event_killable_timeout(vss->control_wait,
+ virtsnd_pcm_control_settled(vss),
+ virtsnd_pcm_settle_jiffies());
+ if (rc < 0)
+ return rc;
+ if (virtsnd_pcm_control_settled(vss))
+ return 0;
+
+ dev_err_ratelimited(&vss->snd->vdev->dev,
+ "SID %u: PCM control reply is still outstanding\n",
+ vss->sid);
+ return -EIO;
+}
+
static int
virtsnd_pcm_pause_restart_send(struct virtio_pcm_substream *vss,
struct virtio_snd_msg *msg,
@@ -680,6 +808,10 @@ static int virtsnd_pcm_prepare_locked(struct snd_pcm_substream *substream)
bool suspended;
u64 epoch;
+ /* Teardown decisions below depend on the settled device state. */
+ rc = virtsnd_pcm_control_settle(vss);
+ if (rc)
+ return rc;
scoped_guard(pcm_stream_lock_irqsave, substream) {
guard(spinlock_irqsave)(&vss->lock);
if (vss->resetting)
@@ -1579,6 +1711,9 @@ static int virtsnd_pcm_sync_stop_locked(struct snd_pcm_substream *substream)
int rc;
lockdep_assert_held(&vss->lifecycle_lock);
+ rc = virtsnd_pcm_control_settle(vss);
+ if (rc)
+ return rc;
scoped_guard(pcm_stream_lock_irqsave, substream) {
guard(spinlock_irqsave)(&vss->lock);
preparing = vss->preparing;
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 18/20] ALSA: virtio: settle PCM control state from late replies
2026-11-13 6:42 ` [PATCH v32 18/20] ALSA: virtio: settle PCM control state from late replies Joonho Lee via B4 Relay
@ 2026-11-13 7:26 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:26 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
issues to consider:
- [Medium] ALSA: virtio: premature settle wait in
virtsnd_pcm_sync_stop_locked()
- [Medium] ALSA: virtio: late STOP race in virtsnd_pcm_trigger_inner()
pause rollback
- [Medium] ALSA: virtio: repeated virtsnd_pcm_control_settle() waits in
single PCM ops
--
commit dfc3a2dc4d2a8149c7f6f4fd9c5c643964f6b264
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: settle PCM control state from late replies
This commit tracks timed-out VirtIO PCM control requests and applies their
late replies to settle the substream's native device state so sleepable
PCM operations can wait for completion instead of requiring a device reset.
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index 4a10cd3cc57ce..15d4473f4c0a0 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
> @@ -157,10 +157,79 @@ static bool virtsnd_pcm_pause_restart_transitioning(enum virtio_pcm_pause_restar
> }
> }
>
> +/**
> + * virtsnd_pcm_ctl_defer_locked() - Record a native control request whose
> + * sender timed out.
> + * @vss: VirtIO PCM substream.
> + * @epoch: Generation of the request.
> + *
> + * Context: Control queue lock held with interrupts disabled.
> + * Return: false if the generation was reset, true otherwise.
> + */
> +bool virtsnd_pcm_ctl_defer_locked(struct virtio_pcm_substream *vss, u64 epoch)
> +{
> + guard(spinlock)(&vss->lock);
> + if (vss->resetting || vss->reset_epoch != epoch)
> + return false;
> + vss->control_late++;
> + vss->control_uncertain = true;
> + return true;
> +}
> +
> +/**
> + * virtsnd_pcm_ctl_settle_locked() - Apply a late native control reply.
> + * @vss: VirtIO PCM substream.
> + * @epoch: Generation of the request.
> + * @command: Control request code (VIRTIO_SND_R_PCM_XXX).
> + * @status: Reply status converted to an errno value.
> + *
> + * No other native control request is sent while one is outstanding, so the
> + * reply determines the device state. A failed request leaves it unchanged.
> + *
> + * Context: Control queue lock held with interrupts disabled.
> + */
> +void virtsnd_pcm_ctl_settle_locked(struct virtio_pcm_substream *vss, u64 epoch,
> + u32 command, int status)
> +{
> + scoped_guard(spinlock, &vss->lock) {
> + if (vss->resetting || vss->reset_epoch != epoch ||
> + !vss->control_late)
> + return;
> + if (!status) {
> + switch (command) {
> + case VIRTIO_SND_R_PCM_PREPARE:
> + vss->release_required = true;
> + break;
> + case VIRTIO_SND_R_PCM_START:
> + vss->device_running = true;
> + break;
> + case VIRTIO_SND_R_PCM_STOP:
> + vss->device_running = false;
> + break;
> + case VIRTIO_SND_R_PCM_RELEASE:
> + vss->release_required = false;
> + vss->device_running = false;
> + break;
> + }
> + }
> + if (--vss->control_late)
> + return;
> + vss->control_uncertain = false;
> + wake_up_all(&vss->control_wait);
> + }
[Severity: Medium]
Can a late successful VIRTIO_SND_R_PCM_STOP reply race with the
SNDRV_PCM_TRIGGER_PAUSE_PUSH rollback in virtsnd_pcm_trigger_inner() and
re-enable vss->xfer_enabled while the device is stopped?
CPU0 CPU1 (<hardirq>)
virtsnd_pcm_trigger_inner(..., PAUSE_PUSH)
virtsnd_pcm_stop_device()
virtsnd_pcm_send_sync()
virtsnd_ctl_msg_send_sync()
-> -ETIMEDOUT
virtsnd_ctl_msg_defer()
vss->control_late = 1
vss->control_uncertain = true
virtsnd_ctl_notify_cb()
virtsnd_ctl_msg_complete()
virtsnd_pcm_ctl_settle_locked()
vss->device_running = false
vss->control_late = 0
vss->control_uncertain = false
if (rc && command == SNDRV_PCM_TRIGGER_PAUSE_PUSH)
virtsnd_pcm_start_queue()
vss->xfer_enabled = true <-- queue reopened with device stopped
sound/virtio/virtio_pcm_ops.c:virtsnd_pcm_start_queue() {
...
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) {
rc = vss->runtime_active ? virtsnd_pcm_capture_send(vss) : -EBADFD;
}
if (!rc)
vss->xfer_enabled = true;
...
}
virtsnd_pcm_start_queue() checks vss->control_uncertain, which CPU1 just
cleared, but does not check vss->device_running. In a linked substream
group, ALSA only rolls back streams preceding the failed one, leaving this
substream in SNDRV_PCM_STATE_RUNNING with vss->xfer_enabled set while the
device is stopped; on an unlinked capture stream, rolling back
vss->capture_quiet_sequence before snd_pcm_undo_pause() resumes the stream
causes the zero-length RX buffers drained by STOP to trigger a false
capture XRUN.
> @@ -680,6 +808,10 @@ static int virtsnd_pcm_prepare_locked(struct snd_pcm_substream *substream)
> bool suspended;
> u64 epoch;
>
> + /* Teardown decisions below depend on the settled device state. */
> + rc = virtsnd_pcm_control_settle(vss);
> + if (rc)
> + return rc;
> scoped_guard(pcm_stream_lock_irqsave, substream) {
> guard(spinlock_irqsave)(&vss->lock);
> if (vss->resetting)
[Severity: Medium]
Does calling virtsnd_pcm_control_settle() in both
virtsnd_pcm_sync_stop_locked() and its callers make a single prepare,
hw_free, or hw_params operation wait 8 to 12 timeouts instead of the
documented 4?
When a prior control request times out and sets
substream->runtime->stop_operating, snd_pcm_do_prepare(), do_hw_free(), and
snd_pcm_hw_params() first call snd_pcm_sync_stop(), which invokes
virtsnd_pcm_sync_stop_locked() -> virtsnd_pcm_control_settle() (4 timeouts)
and ignores the -EIO return value. ALSA core then immediately calls the
driver's prepare, hw_free, or hw_params callback, which calls
virtsnd_pcm_control_settle() a second time (8 timeouts total), and when
virtsnd_pcm_hw_params() fails, snd_pcm_hw_params() unwinds through
virtsnd_pcm_hw_free() -> virtsnd_pcm_sync_stop_locked() for a third wait
(12 timeouts total, or 5 timeouts when VIRTIO_SND_R_PCM_SET_PARAMS itself
times out).
> @@ -1579,6 +1711,9 @@ static int virtsnd_pcm_sync_stop_locked(struct snd_pcm_substream *substream)
> int rc;
>
> lockdep_assert_held(&vss->lifecycle_lock);
> + rc = virtsnd_pcm_control_settle(vss);
> + if (rc)
> + return rc;
> scoped_guard(pcm_stream_lock_irqsave, substream) {
> guard(spinlock_irqsave)(&vss->lock);
> preparing = vss->preparing;
[Severity: Medium]
Does returning early when virtsnd_pcm_control_settle() fails here lose
substream->runtime->stop_operating and skip disabling vss->elapsed_period?
snd_pcm_sync_stop() clears substream->runtime->stop_operating before
calling ->sync_stop() and ignores the return value, relying on the driver
to re-arm stop_operating = true on failure as virtsnd_pcm_release_locked()
does. If virtsnd_pcm_control_settle() returns -ERESTARTSYS or -EIO and the
late reply later settles, the stream stays in SNDRV_PCM_STATE_SETUP with
stop_operating == false and vss->prepared == true, so subsequent
snd_pcm_sync_stop() calls skip releasing the device stream; on the
virtsnd_pcm_hw_free() path, returning before virtsnd_pcm_release_locked()
also skips disable_work_sync(&vss->elapsed_period) before the DMA buffer is
freed.
Sleeping in virtsnd_pcm_control_settle() before the pcm_stream_lock_irqsave
block in both virtsnd_pcm_sync_stop_locked() and
virtsnd_pcm_prepare_locked() also leaves vss->preparing == false and
vss->prepared == true open to I/O completions, pointer/ACK callbacks, and
triggers during the wait.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=18
^ permalink raw reply [flat|nested] 41+ messages in thread
* [PATCH v32 19/20] ALSA: pcm: allow stream suspension without power management
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (17 preceding siblings ...)
2026-11-13 6:42 ` [PATCH v32 18/20] ALSA: virtio: settle PCM control state from late replies Joonho Lee via B4 Relay
@ 2026-11-13 6:42 ` Joonho Lee via B4 Relay
2026-11-13 6:58 ` sashiko-bot
2026-11-13 6:42 ` [PATCH v32 20/20] ALSA: virtio: reset the device when PCM state stays unknown Joonho Lee via B4 Relay
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:42 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Device recovery needs to suspend PCM streams even when CONFIG_PM is disabled. The no-op snd_pcm_suspend_all() stub leaves active streams running while virtio-snd resets their queues, so an application sees an I/O failure instead of the suspended state needed for prepare and recovery.
Build the logical stream suspension operation independently of power management. Keep system resume support conditional on CONFIG_PM.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
include/sound/pcm.h | 7 -------
sound/core/pcm_native.c | 2 +-
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/include/sound/pcm.h b/include/sound/pcm.h
index 7150f8356dc8..cdbde0524dd4 100644
--- a/include/sound/pcm.h
+++ b/include/sound/pcm.h
@@ -628,14 +628,7 @@ int snd_pcm_start(struct snd_pcm_substream *substream);
int snd_pcm_stop(struct snd_pcm_substream *substream, snd_pcm_state_t status);
int snd_pcm_drain_done(struct snd_pcm_substream *substream);
int snd_pcm_stop_xrun(struct snd_pcm_substream *substream);
-#ifdef CONFIG_PM
int snd_pcm_suspend_all(struct snd_pcm *pcm);
-#else
-static inline int snd_pcm_suspend_all(struct snd_pcm *pcm)
-{
- return 0;
-}
-#endif
int snd_pcm_kernel_ioctl(struct snd_pcm_substream *substream, unsigned int cmd, void *arg);
int snd_pcm_open_substream(struct snd_pcm *pcm, int stream, struct file *file,
struct snd_pcm_substream **rsubstream);
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index 8131a7be5dfd..a1c13647a14c 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1828,7 +1828,6 @@ static int snd_pcm_pause_lock_irq(struct snd_pcm_substream *substream,
(snd_pcm_state_t)push);
}
-#ifdef CONFIG_PM
/* suspend callback: state argument ignored */
static int snd_pcm_pre_suspend(struct snd_pcm_substream *substream,
@@ -1939,6 +1938,7 @@ int snd_pcm_suspend_all(struct snd_pcm *pcm)
}
EXPORT_SYMBOL(snd_pcm_suspend_all);
+#ifdef CONFIG_PM
/* resume callbacks: state argument ignored */
static int snd_pcm_pre_resume(struct snd_pcm_substream *substream,
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* [PATCH v32 20/20] ALSA: virtio: reset the device when PCM state stays unknown
2026-11-13 6:41 [PATCH v32 00/20] ALSA: virtio: preserve PCM progress Joonho Lee via B4 Relay
` (18 preceding siblings ...)
2026-11-13 6:42 ` [PATCH v32 19/20] ALSA: pcm: allow stream suspension without power management Joonho Lee via B4 Relay
@ 2026-11-13 6:42 ` Joonho Lee via B4 Relay
2026-11-13 7:30 ` sashiko-bot
19 siblings, 1 reply; 41+ messages in thread
From: Joonho Lee via B4 Relay @ 2026-11-13 6:42 UTC (permalink / raw)
To: linux-sound
Cc: tiwai, perex, mst, virtualization, linux-kernel, corbet, skhan,
rdunlap, linux-doc, Joonho Lee
From: Joonho Lee <mynameisjune111@gmail.com>
Some failures leave the driver unable to learn or reclaim a stream's
device state:
- a native control reply still missing after the settle wait
- an uncertain control result in the opt-in capture_pause_restart mode,
which marks its stream reset-required
- released I/O requests that the device does not return
The stream then fails until the guest suspends and resumes or the driver
is rebound.
Recover with a device reset. A delayed work item checks whether any
stream is still in such a state. If one is, it moves the open streams to
SUSPENDED with snd_pcm_suspend_all(), so applications see -ESTRPIPE and
prepare again, then runs virtio_device_reset_prepare() and
virtio_device_reset_done(). The driver implements reset_prepare and
reset_done with its freeze and restore sequences, which also serves
transport-initiated resets.
A late control reply or an unreturned I/O flush schedules the check five
message timeouts later, one beyond the wait in sleepable operations. A
failed settle wait or an uncertain pause-restart result requests it
immediately. Sleepable operations stop waiting for late replies while
recovery suspends the streams. Applications prepare again as soon as
their stream is suspended, so hw_params and prepare wait until the reset
has finished instead of failing with -ECANCELED. open and hw_free do not
wait: they run under the PCM open mutex, which recovery takes to suspend
the streams. Resets of one device are at least 30 seconds apart. Remove
and freeze disable the work before tearing the device down.
If the transport reset fails, disconnect the ALSA card rather than leave
streams permanently resetting. Configuration waiters receive -ENODEV.
Publish that result and wake waiters before disconnecting, since ALSA
disconnect waits for their power references. Rebinding the driver is
required after this terminal failure.
With CONFIG_PM disabled, the common PCM suspend operation still changes
active streams to SUSPENDED before their queues are reset.
Assisted-by: LLM
Signed-off-by: Joonho Lee <mynameisjune111@gmail.com>
---
Documentation/sound/alsa-configuration.rst | 12 ++
sound/virtio/virtio_card.c | 222 ++++++++++++++++++++++++++---
sound/virtio/virtio_card.h | 16 +++
sound/virtio/virtio_pcm.h | 2 +
sound/virtio/virtio_pcm_ops.c | 113 +++++++++++----
5 files changed, 320 insertions(+), 45 deletions(-)
diff --git a/Documentation/sound/alsa-configuration.rst b/Documentation/sound/alsa-configuration.rst
index df9c048fcb9b..8c765bb0e7e5 100644
--- a/Documentation/sound/alsa-configuration.rst
+++ b/Documentation/sound/alsa-configuration.rst
@@ -2568,6 +2568,18 @@ stream until the reply arrives. Trigger operations fail meanwhile; hw_params,
hw_free and prepare wait up to four timeouts for the reply and continue from
the device state it reports.
+If that reply has not arrived after five timeouts, or released I/O requests
+are not returned, the driver suspends the open streams and resets the device.
+Applications observe ``-ESTRPIPE`` and prepare their streams again; hw_params
+and prepare wait until the reset has finished. The driver resets a device at
+most once every 30 seconds.
+
+Stream suspension for recovery is available even without power-management
+support. If the reset fails, the ALSA card is disconnected. Existing PCM
+handles report ``-ENODEV`` and the sound devices are removed. Rebinding the
+driver is required to use the device again; the driver does not repeatedly
+reset a failed device.
+
Module snd-virtuoso
-------------------
diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
index 3f2242f27440..dd110fd7ad16 100644
--- a/sound/virtio/virtio_card.c
+++ b/sound/virtio/virtio_card.c
@@ -16,6 +16,7 @@ module_param_named(msg_timeout_ms, virtsnd_msg_timeout_ms, uint, 0644);
MODULE_PARM_DESC(msg_timeout_ms, "Message completion timeout in milliseconds");
static void virtsnd_remove(struct virtio_device *vdev);
+static void virtsnd_recover_work(struct work_struct *work);
/**
* virtsnd_event_send() - Add an event to the event queue.
@@ -333,6 +334,11 @@ static int virtsnd_probe(struct virtio_device *vdev)
snd->vdev = vdev;
INIT_LIST_HEAD(&snd->ctl_msgs);
INIT_LIST_HEAD(&snd->pcm_list);
+ INIT_DELAYED_WORK(&snd->recover_work, virtsnd_recover_work);
+ init_waitqueue_head(&snd->recover_wait);
+ /* Recovery needs the complete device; probe enables it last. */
+ disable_delayed_work(&snd->recover_work);
+ snd->recover_next = jiffies;
vdev->priv = snd;
@@ -350,6 +356,7 @@ static int virtsnd_probe(struct virtio_device *vdev)
goto on_exit;
virtsnd_enable_event_vq(snd);
+ enable_delayed_work(&snd->recover_work);
on_exit:
if (rc)
@@ -383,6 +390,7 @@ static void virtsnd_reset_vqs(struct virtio_snd *snd)
vss->release_required = false;
vss->control_uncertain = false;
vss->control_late = 0;
+ vss->io_flush_failed = false;
vss->capture_reset_observing = false;
vss->xfer_enabled = false;
vss->suspended = true;
@@ -449,6 +457,7 @@ static void virtsnd_remove(struct virtio_device *vdev)
struct virtio_snd *snd = vdev->priv;
unsigned int i;
+ disable_delayed_work_sync(&snd->recover_work);
virtsnd_disable_event_vq(snd);
if (snd->card)
@@ -470,18 +479,14 @@ static void virtsnd_remove(struct virtio_device *vdev)
kfree(snd->event_msgs);
}
-#ifdef CONFIG_PM_SLEEP
/**
- * virtsnd_freeze() - Suspend device.
- * @vdev: VirtIO parent device.
+ * virtsnd_quiesce() - Stop device access and retire outstanding requests.
+ * @snd: VirtIO sound device.
*
- * Context: Any context.
- * Return: 0 on success, -errno on failure.
+ * Context: Any context that permits to sleep.
*/
-static int virtsnd_freeze(struct virtio_device *vdev)
+static void virtsnd_quiesce(struct virtio_snd *snd)
{
- struct virtio_snd *snd = vdev->priv;
-
virtsnd_disable_event_vq(snd);
virtsnd_ctl_msg_cancel_all(snd);
@@ -492,22 +497,19 @@ static int virtsnd_freeze(struct virtio_device *vdev)
kfree(snd->event_msgs);
snd->event_msgs = NULL;
-
- return 0;
}
/**
- * virtsnd_restore() - Resume device.
- * @vdev: VirtIO parent device.
+ * virtsnd_restart() - Reinitialize virtqueues after a device reset.
+ * @snd: VirtIO sound device.
*
- * Context: Any context.
+ * Context: Any context that permits to sleep.
* Return: 0 on success, -errno on failure.
*/
-static int virtsnd_restore(struct virtio_device *vdev)
+static int virtsnd_restart(struct virtio_snd *snd)
{
- struct virtio_snd *snd = vdev->priv;
- int rc;
unsigned int i;
+ int rc;
rc = virtsnd_find_vqs(snd);
if (rc) {
@@ -515,7 +517,7 @@ static int virtsnd_restore(struct virtio_device *vdev)
return rc;
}
- virtio_device_ready(vdev);
+ virtio_device_ready(snd->vdev);
virtsnd_ctl_msg_enable(snd);
for (i = 0; i < snd->nsubstreams; ++i) {
@@ -528,6 +530,190 @@ static int virtsnd_restore(struct virtio_device *vdev)
return 0;
}
+
+/* Minimum time between recovery resets of one device. */
+#define VIRTSND_RECOVER_INTERVAL_MS 30000
+
+static bool virtsnd_recover_needed(struct virtio_snd *snd)
+{
+ unsigned int i;
+
+ for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
+ struct virtio_pcm_substream *vss = &snd->substreams[i];
+
+ guard(spinlock_irqsave)(&vss->lock);
+ if (vss->resetting)
+ continue;
+ if (vss->control_uncertain ||
+ vss->pause_restart_state ==
+ VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
+ (vss->io_flush_failed && vss->msg_count))
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * virtsnd_recover_work() - Reset the device when a PCM state stays unknown.
+ * @work: Recovery work of the sound device.
+ *
+ * A control reply or released I/O request that never returns leaves its PCM
+ * stream unusable. Suspend the open streams so applications prepare them
+ * again, then reset the device with the transport reset sequence.
+ *
+ * Context: Process context.
+ */
+static void virtsnd_recover_work(struct work_struct *work)
+{
+ struct virtio_snd *snd = container_of(to_delayed_work(work),
+ struct virtio_snd, recover_work);
+ struct virtio_device *vdev = snd->vdev;
+ struct virtio_pcm *vpcm;
+ unsigned int i;
+ int rc;
+
+ if (!virtsnd_recover_needed(snd))
+ return;
+
+ if (time_before(jiffies, snd->recover_next)) {
+ queue_delayed_work(system_dfl_wq, &snd->recover_work,
+ snd->recover_next - jiffies);
+ return;
+ }
+ snd->recover_next = jiffies +
+ msecs_to_jiffies(VIRTSND_RECOVER_INTERVAL_MS);
+
+ dev_warn(&vdev->dev, "PCM device state is unknown; resetting device\n");
+
+ WRITE_ONCE(snd->recovering, true);
+ for (i = 0; i < snd->nsubstreams; ++i)
+ wake_up_all(&snd->substreams[i].control_wait);
+
+ /* Applications observe -ESTRPIPE and prepare their streams again. */
+ list_for_each_entry(vpcm, &snd->pcm_list, list)
+ snd_pcm_suspend_all(vpcm->pcm);
+
+ rc = virtio_device_reset_prepare(vdev);
+ if (!rc)
+ rc = virtio_device_reset_done(vdev);
+ WRITE_ONCE(snd->recover_failed, !!rc);
+ /* Publish the result before releasing configuration waiters. */
+ smp_store_release(&snd->recovering, false);
+ wake_up_all(&snd->recover_wait);
+
+ if (rc) {
+ /*
+ * Wake configuration waiters before disconnect, which waits for
+ * their ALSA power references to be released.
+ */
+ dev_err(&vdev->dev, "device recovery reset failed: %d; disconnecting\n",
+ rc);
+ snd_card_disconnect(snd->card);
+ }
+}
+
+/**
+ * virtsnd_recover_wait() - Wait until a recovery reset has finished.
+ * @snd: VirtIO sound device.
+ *
+ * Applications prepare their streams again as soon as recovery suspends
+ * them. Configuring a stream must wait until the reset has completed.
+ *
+ * Context: Process context. The caller must not hold the PCM open mutex or
+ * a substream lifecycle lock, which recovery takes.
+ * Return: 0, -ENODEV if recovery failed, or -ERESTARTSYS if killed.
+ */
+int virtsnd_recover_wait(struct virtio_snd *snd)
+{
+ int rc;
+
+ rc = wait_event_killable(snd->recover_wait,
+ /* Pair with recovery's result publication. */
+ !smp_load_acquire(&snd->recovering));
+ if (rc)
+ return rc;
+ return READ_ONCE(snd->recover_failed) ? -ENODEV : 0;
+}
+
+/**
+ * virtsnd_recover_schedule() - Request a recovery check.
+ * @snd: VirtIO sound device.
+ * @delay: Time before the check, in jiffies. Zero brings a pending check
+ * forward.
+ *
+ * Context: Any context.
+ */
+void virtsnd_recover_schedule(struct virtio_snd *snd, unsigned long delay)
+{
+ if (delay)
+ queue_delayed_work(system_dfl_wq, &snd->recover_work, delay);
+ else
+ mod_delayed_work(system_dfl_wq, &snd->recover_work, 0);
+}
+
+/*
+ * Transport resets and driver recovery use the freeze and restore sequence.
+ * Recovery calls these callbacks from its own work item.
+ */
+static int virtsnd_reset_prepare(struct virtio_device *vdev)
+{
+ struct virtio_snd *snd = vdev->priv;
+
+ if (current_work() != &snd->recover_work.work)
+ disable_delayed_work_sync(&snd->recover_work);
+ virtsnd_quiesce(snd);
+
+ return 0;
+}
+
+static int virtsnd_reset_done(struct virtio_device *vdev)
+{
+ struct virtio_snd *snd = vdev->priv;
+ int rc;
+
+ rc = virtsnd_restart(snd);
+ if (current_work() != &snd->recover_work.work)
+ enable_delayed_work(&snd->recover_work);
+
+ return rc;
+}
+
+#ifdef CONFIG_PM_SLEEP
+/**
+ * virtsnd_freeze() - Suspend device.
+ * @vdev: VirtIO parent device.
+ *
+ * Context: Any context.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_freeze(struct virtio_device *vdev)
+{
+ struct virtio_snd *snd = vdev->priv;
+
+ disable_delayed_work_sync(&snd->recover_work);
+ virtsnd_quiesce(snd);
+
+ return 0;
+}
+
+/**
+ * virtsnd_restore() - Resume device.
+ * @vdev: VirtIO parent device.
+ *
+ * Context: Any context.
+ * Return: 0 on success, -errno on failure.
+ */
+static int virtsnd_restore(struct virtio_device *vdev)
+{
+ struct virtio_snd *snd = vdev->priv;
+ int rc;
+
+ rc = virtsnd_restart(snd);
+ enable_delayed_work(&snd->recover_work);
+
+ return rc;
+}
#endif /* CONFIG_PM_SLEEP */
static const struct virtio_device_id id_table[] = {
@@ -547,6 +733,8 @@ static struct virtio_driver virtsnd_driver = {
.validate = virtsnd_validate,
.probe = virtsnd_probe,
.remove = virtsnd_remove,
+ .reset_prepare = virtsnd_reset_prepare,
+ .reset_done = virtsnd_reset_done,
#ifdef CONFIG_PM_SLEEP
.freeze = virtsnd_freeze,
.restore = virtsnd_restore,
diff --git a/sound/virtio/virtio_card.h b/sound/virtio/virtio_card.h
index a733fa684c86..7b17e6c8a594 100644
--- a/sound/virtio/virtio_card.h
+++ b/sound/virtio/virtio_card.h
@@ -8,6 +8,7 @@
#include <linux/slab.h>
#include <linux/virtio.h>
+#include <linux/workqueue.h>
#include <sound/core.h>
#include <uapi/linux/virtio_snd.h>
@@ -61,6 +62,12 @@ struct virtio_kctl {
* @kctl_infos: VirtIO control element information.
* @kctls: VirtIO control elements.
* @nkctls: Number of control elements.
+ * @recover_work: Resets the device when a PCM stream state stays unknown.
+ * @recover_next: Earliest time of the next recovery reset, in jiffies.
+ * @recovering: Recovery is suspending streams; sleepable PCM operations do
+ * not wait for late control replies meanwhile.
+ * @recover_wait: Notify when recovery finishes.
+ * @recover_failed: Recovery failed; PCM configuration must return -ENODEV.
*/
struct virtio_snd {
struct virtio_device *vdev;
@@ -81,11 +88,20 @@ struct virtio_snd {
struct virtio_snd_ctl_info *kctl_infos;
struct virtio_kctl *kctls;
u32 nkctls;
+ struct delayed_work recover_work;
+ unsigned long recover_next;
+ bool recovering;
+ bool recover_failed;
+ wait_queue_head_t recover_wait;
};
/* Message completion timeout in milliseconds (module parameter). */
extern u32 virtsnd_msg_timeout_ms;
+void virtsnd_recover_schedule(struct virtio_snd *snd, unsigned long delay);
+
+int virtsnd_recover_wait(struct virtio_snd *snd);
+
static inline struct virtio_snd_queue *
virtsnd_control_queue(struct virtio_snd *snd)
{
diff --git a/sound/virtio/virtio_pcm.h b/sound/virtio/virtio_pcm.h
index 4ead0d94af63..33d44fab08fa 100644
--- a/sound/virtio/virtio_pcm.h
+++ b/sound/virtio/virtio_pcm.h
@@ -85,6 +85,7 @@ enum virtio_pcm_pause_restart_state {
* PCM control request is sent until it is.
* @control_late: Timed-out native control requests still owned by the device.
* Their replies settle the device state.
+ * @io_flush_failed: Released I/O requests were not returned in time.
* @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.
@@ -147,6 +148,7 @@ struct virtio_pcm_substream {
bool release_required;
bool control_uncertain;
unsigned int control_late;
+ bool io_flush_failed;
bool capture_reset_observing;
bool indirect_reset_observing;
bool playback_pointer_active;
diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
index 15d4473f4c0a..dcce428d704c 100644
--- a/sound/virtio/virtio_pcm_ops.c
+++ b/sound/virtio/virtio_pcm_ops.c
@@ -157,6 +157,35 @@ static bool virtsnd_pcm_pause_restart_transitioning(enum virtio_pcm_pause_restar
}
}
+/* Sleepable operations wait this many message timeouts for a late reply. */
+#define VIRTSND_PCM_SETTLE_TIMEOUTS 4
+
+static unsigned long virtsnd_pcm_timeouts_jiffies(unsigned int count)
+{
+ u64 ms = (u64)virtsnd_msg_timeout_ms * count;
+
+ return msecs_to_jiffies(min_t(u64, ms, UINT_MAX));
+}
+
+static unsigned long virtsnd_pcm_settle_jiffies(void)
+{
+ return virtsnd_pcm_timeouts_jiffies(VIRTSND_PCM_SETTLE_TIMEOUTS);
+}
+
+/* Recovery starts after a waiting operation would have given up. */
+static unsigned long virtsnd_pcm_recover_jiffies(void)
+{
+ return virtsnd_pcm_timeouts_jiffies(VIRTSND_PCM_SETTLE_TIMEOUTS + 1);
+}
+
+/* A released I/O request that is not returned keeps its DMA buffer busy. */
+static void virtsnd_pcm_flush_failed(struct virtio_pcm_substream *vss)
+{
+ scoped_guard(spinlock_irqsave, &vss->lock)
+ vss->io_flush_failed = true;
+ virtsnd_recover_schedule(vss->snd, virtsnd_pcm_recover_jiffies());
+}
+
/**
* virtsnd_pcm_ctl_defer_locked() - Record a native control request whose
* sender timed out.
@@ -256,36 +285,37 @@ static int virtsnd_pcm_send_sync(struct virtio_pcm_substream *vss,
if (rc && uncertain)
vss->control_uncertain = true;
}
- if (deferred)
+ if (deferred) {
dev_warn_ratelimited(&vss->snd->vdev->dev,
"SID %u: PCM control 0x%08x reply is late; holding PCM commands\n",
vss->sid, command);
+ /* Reset the device if the reply has not arrived by then. */
+ virtsnd_recover_schedule(vss->snd, virtsnd_pcm_recover_jiffies());
+ }
return rc;
}
-/* Sleepable operations wait this many message timeouts for a late reply. */
-#define VIRTSND_PCM_SETTLE_TIMEOUTS 4
-
-static unsigned long virtsnd_pcm_settle_jiffies(void)
-{
- u64 ms = (u64)virtsnd_msg_timeout_ms * VIRTSND_PCM_SETTLE_TIMEOUTS;
-
- return msecs_to_jiffies(min_t(u64, ms, UINT_MAX));
-}
-
static bool virtsnd_pcm_control_settled(struct virtio_pcm_substream *vss)
{
guard(spinlock_irqsave)(&vss->lock);
return vss->resetting || !vss->control_uncertain;
}
+static bool virtsnd_pcm_control_wait_done(struct virtio_pcm_substream *vss)
+{
+ return READ_ONCE(vss->snd->recovering) ||
+ virtsnd_pcm_control_settled(vss);
+}
+
/**
* virtsnd_pcm_control_settle() - Wait for a late native control reply.
* @vss: VirtIO PCM substream.
*
* Trigger fails while a native control result is unknown. Sleepable
* operations wait a bounded time for the reply, then continue from the
- * device state it reports.
+ * device state it reports. If the reply does not arrive, device recovery
+ * resets the stream state. Recovery itself suspends the streams, so these
+ * operations do not wait while it runs.
*
* Context: Process context with @vss->lifecycle_lock held.
* Return: 0 if the state is known or a device reset is in progress, -EIO if
@@ -298,18 +328,23 @@ static int virtsnd_pcm_control_settle(struct virtio_pcm_substream *vss)
lockdep_assert_held(&vss->lifecycle_lock);
if (virtsnd_pcm_control_settled(vss))
return 0;
+ if (READ_ONCE(vss->snd->recovering))
+ return -EIO;
rc = wait_event_killable_timeout(vss->control_wait,
- virtsnd_pcm_control_settled(vss),
+ virtsnd_pcm_control_wait_done(vss),
virtsnd_pcm_settle_jiffies());
if (rc < 0)
return rc;
if (virtsnd_pcm_control_settled(vss))
return 0;
+ if (READ_ONCE(vss->snd->recovering))
+ return -EIO;
dev_err_ratelimited(&vss->snd->vdev->dev,
- "SID %u: PCM control reply is still outstanding\n",
+ "SID %u: PCM control reply is still outstanding; resetting device\n",
vss->sid);
+ virtsnd_recover_schedule(vss->snd, 0);
return -EIO;
}
@@ -370,10 +405,12 @@ virtsnd_pcm_pause_restart_send(struct virtio_pcm_substream *vss,
}
}
- if (rc && uncertain)
+ if (rc && uncertain) {
dev_err(&vss->snd->vdev->dev,
- "SID %u: PCM control state is unknown; device reprobe required\n",
+ "SID %u: PCM control state is unknown; resetting device\n",
vss->sid);
+ virtsnd_recover_schedule(vss->snd, 0);
+ }
return rc;
}
@@ -395,6 +432,8 @@ virtsnd_pcm_pause_restart_fail(struct virtio_pcm_substream *vss,
vss->pause_restart_state =
VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED;
}
+ if (reset_required)
+ virtsnd_recover_schedule(vss->snd, 0);
if (notify_xrun)
virtsnd_pcm_schedule_period_elapsed(vss);
vss->substream->runtime->stop_operating = true;
@@ -440,16 +479,22 @@ static int virtsnd_pcm_release_wait(struct virtio_pcm_substream *vss, u64 epoch)
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->release_required = false;
- vss->pause_restart_state = VIRTSND_PCM_PAUSE_RESTART_RELEASED;
- return 0;
+ scoped_guard(spinlock_irqsave, &vss->lock) {
+ if (vss->resetting || vss->reset_epoch != epoch)
+ return -ECANCELED;
+ if (vss->pause_restart_state !=
+ VIRTSND_PCM_PAUSE_RESTART_RELEASE_SENT)
+ return -ETIMEDOUT;
+ if (waited && !vss->msg_count) {
+ vss->prepared = false;
+ vss->release_required = false;
+ vss->pause_restart_state =
+ VIRTSND_PCM_PAUSE_RESTART_RELEASED;
+ return 0;
+ }
+ }
+ virtsnd_pcm_flush_failed(vss);
+ return -ETIMEDOUT;
}
static int
@@ -661,6 +706,9 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
u64 epoch;
int rc;
+ rc = virtsnd_recover_wait(vss->snd);
+ if (rc)
+ return 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),
@@ -922,6 +970,7 @@ static int virtsnd_pcm_prepare_locked(struct snd_pcm_substream *substream)
vss->prepared = true;
vss->stopped = false;
vss->xfer_xrun = false;
+ vss->io_flush_failed = false;
}
return rc;
@@ -933,6 +982,10 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
int rc;
+ /* Applications prepare again as soon as recovery suspends the stream. */
+ rc = virtsnd_recover_wait(vss->snd);
+ if (rc)
+ return rc;
guard(mutex)(&vss->lifecycle_lock);
rc = virtsnd_pcm_prepare_locked(substream);
if (!rc && vss->period_work_quiesced) {
@@ -1508,7 +1561,7 @@ virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream)
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",
+ "SID %u: PCM state is unknown; resetting device\n",
vss->sid);
return -EIO;
}
@@ -1554,6 +1607,7 @@ virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream)
!virtsnd_pcm_msg_pending_num(vss), js);
if (waited <= 0) {
rc = waited ? (int)waited : -ETIMEDOUT;
+ virtsnd_pcm_flush_failed(vss);
virtsnd_pcm_pause_restart_fail(vss, false, epoch);
return rc;
}
@@ -1571,8 +1625,10 @@ virtsnd_pcm_pause_restart_sync_stop(struct snd_pcm_substream *substream)
rc = 0;
}
}
- if (rc)
+ if (rc) {
+ virtsnd_pcm_flush_failed(vss);
virtsnd_pcm_pause_restart_fail(vss, false, epoch);
+ }
return rc;
}
@@ -1689,6 +1745,7 @@ static int virtsnd_pcm_release_locked(struct snd_pcm_substream *substream)
dev_warn(&snd->vdev->dev, "SID %u: failed to flush I/O queue\n",
vss->sid);
substream->runtime->stop_operating = true;
+ virtsnd_pcm_flush_failed(vss);
return waited ? (int)waited : -ETIMEDOUT;
}
--
Git-157)
^ permalink raw reply related [flat|nested] 41+ messages in thread* Re: [PATCH v32 20/20] ALSA: virtio: reset the device when PCM state stays unknown
2026-11-13 6:42 ` [PATCH v32 20/20] ALSA: virtio: reset the device when PCM state stays unknown Joonho Lee via B4 Relay
@ 2026-11-13 7:30 ` sashiko-bot
0 siblings, 0 replies; 41+ messages in thread
From: sashiko-bot @ 2026-11-13 7:30 UTC (permalink / raw)
To: Joonho Lee, linux-sound; +Cc: virtualization, Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 8 potential
issues to consider:
- [High] ALSA: virtio: deadlock between virtsnd_pcm_prepare() and
virtsnd_recover_work()
- [High] ALSA: virtio: missing recovery wait in open, hw_free, and
virtsnd_pcm_sync_stop()
- [Medium] ALSA: virtio: pending recover_work timer triggers premature
device reset
- [Medium] ALSA: virtio: virtsnd_recover_work() races with freeze/reset
and restore
- [Medium] ALSA: virtio: virtsnd_reset_prepare() does not suspend PCM
streams on reset
- [Medium] ALSA: virtio: snd_pcm_suspend_all() in recovery sends
blocking requests
- [Medium] ALSA: virtio: recovery ordering and races break
virtsnd_pcm_prepare() and virtsnd_pcm_hw_params()
- [Low] ALSA: virtio: double volatile read of jiffies in
virtsnd_recover_work()
--
commit ab36d8f3f343747049667c96b153b488074305f1
Author: Joonho Lee <mynameisjune111@gmail.com>
ALSA: virtio: reset the device when PCM state stays unknown
This commit adds a delayed recovery work item that suspends open PCM
streams and resets the VirtIO sound device when a PCM substream's device
state remains unknown or an I/O queue flush times out, disconnecting the
ALSA card if the transport reset fails.
> diff --git a/sound/virtio/virtio_card.c b/sound/virtio/virtio_card.c
> index 3f2242f274404..dd110fd7ad165 100644
> --- a/sound/virtio/virtio_card.c
> +++ b/sound/virtio/virtio_card.c
[ ... ]
> @@ -528,6 +530,190 @@ static int virtsnd_restore(struct virtio_device *vdev)
>
> return 0;
> }
> +
> +/* Minimum time between recovery resets of one device. */
> +#define VIRTSND_RECOVER_INTERVAL_MS 30000
> +
> +static bool virtsnd_recover_needed(struct virtio_snd *snd)
> +{
> + unsigned int i;
> +
> + for (i = 0; snd->substreams && i < snd->nsubstreams; ++i) {
> + struct virtio_pcm_substream *vss = &snd->substreams[i];
> +
> + guard(spinlock_irqsave)(&vss->lock);
> + if (vss->resetting)
> + continue;
> + if (vss->control_uncertain ||
> + vss->pause_restart_state ==
> + VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED ||
> + (vss->io_flush_failed && vss->msg_count))
> + return true;
> + }
> +
> + return false;
> +}
> +
> +/**
> + * virtsnd_recover_work() - Reset the device when a PCM state stays unknown.
> + * @work: Recovery work of the sound device.
> + *
> + * A control reply or released I/O request that never returns leaves its PCM
> + * stream unusable. Suspend the open streams so applications prepare them
> + * again, then reset the device with the transport reset sequence.
> + *
> + * Context: Process context.
> + */
> +static void virtsnd_recover_work(struct work_struct *work)
> +{
> + struct virtio_snd *snd = container_of(to_delayed_work(work),
> + struct virtio_snd, recover_work);
> + struct virtio_device *vdev = snd->vdev;
> + struct virtio_pcm *vpcm;
> + unsigned int i;
> + int rc;
> +
> + if (!virtsnd_recover_needed(snd))
> + return;
> +
> + if (time_before(jiffies, snd->recover_next)) {
> + queue_delayed_work(system_dfl_wq, &snd->recover_work,
> + snd->recover_next - jiffies);
> + return;
> + }
[Severity: Low]
Can snd->recover_next - jiffies underflow here? jiffies is volatile and
read twice, so if it advances to or past snd->recover_next between
time_before() and the subtraction, the delay wraps around near ULONG_MAX
and prevents later queue_delayed_work() calls from scheduling recovery.
> + snd->recover_next = jiffies +
> + msecs_to_jiffies(VIRTSND_RECOVER_INTERVAL_MS);
> +
> + dev_warn(&vdev->dev, "PCM device state is unknown; resetting device\n");
> +
> + WRITE_ONCE(snd->recovering, true);
> + for (i = 0; i < snd->nsubstreams; ++i)
> + wake_up_all(&snd->substreams[i].control_wait);
> +
> + /* Applications observe -ESTRPIPE and prepare their streams again. */
> + list_for_each_entry(vpcm, &snd->pcm_list, list)
> + snd_pcm_suspend_all(vpcm->pcm);
> +
> + rc = virtio_device_reset_prepare(vdev);
[Severity: Medium]
Does calling snd_pcm_suspend_all() before virtio_device_reset_prepare()
cause recovery to send new synchronous control requests and repeat flush
waits against the unresponsive device?
At this point vss->resetting is still false and control messages are not
blocked. In virtsnd_pcm_control_settle(), virtsnd_pcm_control_settled()
is checked before READ_ONCE(vss->snd->recovering), so it returns 0 for any
substream where vss->control_uncertain is false (including streams being
recovered for vss->io_flush_failed or
VIRTSND_PCM_PAUSE_RESTART_RESET_REQUIRED, as well as any other active
substream on the card). During snd_pcm_suspend_all():
- snd_pcm_suspend() -> virtsnd_pcm_trigger(SNDRV_PCM_TRIGGER_SUSPEND)
sends a synchronous VIRTIO_SND_R_PCM_STOP request and blocks for
virtsnd_msg_timeout_ms.
- snd_pcm_sync_stop() -> virtsnd_pcm_release_locked() waits another
virtsnd_msg_timeout_ms on vss->msg_empty, logs a duplicate warning, sets
substream->runtime->stop_operating = true again, and calls
virtsnd_pcm_flush_failed() to re-schedule snd->recover_work for another
reset (or virtsnd_pcm_pause_restart_sync_stop() logs a duplicate error
and calls virtsnd_recover_schedule(vss->snd, 0)).
> + if (!rc)
> + rc = virtio_device_reset_done(vdev);
> + WRITE_ONCE(snd->recover_failed, !!rc);
> + /* Publish the result before releasing configuration waiters. */
> + smp_store_release(&snd->recovering, false);
> + wake_up_all(&snd->recover_wait);
> +
> + if (rc) {
> + /*
> + * Wake configuration waiters before disconnect, which waits for
> + * their ALSA power references to be released.
> + */
> + dev_err(&vdev->dev, "device recovery reset failed: %d; disconnecting\n",
> + rc);
> + snd_card_disconnect(snd->card);
> + }
> +}
> +
> +/**
> + * virtsnd_recover_wait() - Wait until a recovery reset has finished.
> + * @snd: VirtIO sound device.
> + *
> + * Applications prepare their streams again as soon as recovery suspends
> + * them. Configuring a stream must wait until the reset has completed.
> + *
> + * Context: Process context. The caller must not hold the PCM open mutex or
> + * a substream lifecycle lock, which recovery takes.
> + * Return: 0, -ENODEV if recovery failed, or -ERESTARTSYS if killed.
> + */
> +int virtsnd_recover_wait(struct virtio_snd *snd)
> +{
> + int rc;
> +
> + rc = wait_event_killable(snd->recover_wait,
> + /* Pair with recovery's result publication. */
> + !smp_load_acquire(&snd->recovering));
> + if (rc)
> + return rc;
> + return READ_ONCE(snd->recover_failed) ? -ENODEV : 0;
> +}
[Severity: High]
Can virtsnd_recover_wait() called from virtsnd_pcm_prepare() deadlock with
virtsnd_recover_work() and snd_pcm_release()?
snd_pcm_prepare() -> snd_pcm_action_nonatomic() holds
down_read(&snd_pcm_link_rwsem) when it invokes virtsnd_pcm_prepare() ->
virtsnd_recover_wait(). Meanwhile, snd_pcm_release() holds
pcm->open_mutex and calls snd_pcm_release_substream() -> snd_pcm_unlink(),
which unconditionally takes down_write(&snd_pcm_link_rwsem) before
checking whether the substream is linked. Because virtsnd_recover_work()
sets snd->recovering = true before taking pcm->open_mutex in
snd_pcm_suspend_all(), the three contexts can lock up:
CPU0 CPU1 kworker
---- ---- -------
virtsnd_recover_work()
snd->recovering = true
snd_pcm_prepare()
down_read(&snd_pcm_link_rwsem)
virtsnd_pcm_prepare()
virtsnd_recover_wait()
snd_pcm_release()
mutex_lock(&pcm->open_mutex)
snd_pcm_unlink()
down_write(&snd_pcm_link_rwsem)
snd_pcm_suspend_all()
mutex_lock(&pcm->open_mutex)
*** DEADLOCK ***
This wedges system_dfl_wq and the global snd_pcm_link_rwsem across all
sound cards.
> +
> +/**
> + * virtsnd_recover_schedule() - Request a recovery check.
> + * @snd: VirtIO sound device.
> + * @delay: Time before the check, in jiffies. Zero brings a pending check
> + * forward.
> + *
> + * Context: Any context.
> + */
> +void virtsnd_recover_schedule(struct virtio_snd *snd, unsigned long delay)
> +{
> + if (delay)
> + queue_delayed_work(system_dfl_wq, &snd->recover_work, delay);
> + else
> + mod_delayed_work(system_dfl_wq, &snd->recover_work, 0);
> +}
[Severity: Medium]
Can an already-pending snd->recover_work from an earlier event trigger a
premature device reset on a later timeout?
When a late control reply or I/O flush failure occurs at T1,
queue_delayed_work() schedules snd->recover_work for T1 + 5 timeouts. If
that reply arrives and settles the substream (or the I/O queue drains),
snd->recover_work remains queued for T1 + 5 timeouts. If another control
reply is late or an I/O flush fails at T2 < T1 + 5 timeouts,
queue_delayed_work() leaves the timer at T1 + 5 timeouts, and when
virtsnd_recover_work() runs at T1 + 5 timeouts, virtsnd_recover_needed()
sees vss->control_uncertain or (vss->io_flush_failed && vss->msg_count) set
and immediately resets the device before the second event's 4-timeout
settle window has elapsed.
> +
> +/*
> + * Transport resets and driver recovery use the freeze and restore sequence.
> + * Recovery calls these callbacks from its own work item.
> + */
> +static int virtsnd_reset_prepare(struct virtio_device *vdev)
> +{
> + struct virtio_snd *snd = vdev->priv;
> +
> + if (current_work() != &snd->recover_work.work)
> + disable_delayed_work_sync(&snd->recover_work);
> + virtsnd_quiesce(snd);
> +
> + return 0;
> +}
> +
> +static int virtsnd_reset_done(struct virtio_device *vdev)
> +{
> + struct virtio_snd *snd = vdev->priv;
> + int rc;
> +
> + rc = virtsnd_restart(snd);
> + if (current_work() != &snd->recover_work.work)
> + enable_delayed_work(&snd->recover_work);
> +
> + return rc;
> +}
[Severity: Medium]
On a transport-initiated reset (where current_work() !=
&snd->recover_work.work), does virtsnd_reset_prepare() leave open ALSA PCM
streams in their active states instead of suspending them?
During system sleep, the PM core suspends child PCM devices before calling
virtsnd_freeze(), and virtsnd_recover_work() calls snd_pcm_suspend_all()
before virtio_device_reset_prepare(). A transport-initiated reset calls
virtsnd_reset_prepare() directly without setting snd->recovering or
calling snd_pcm_suspend_all(), so virtsnd_quiesce() detaches all virtqueue
buffers and sets vss->prepared = false and vss->suspended = true while
leaving substream->runtime->state in RUNNING, PREPARED, or PAUSED, causing
blocked readers/writers to hang until timeout (-EIO) instead of observing
-ESTRPIPE and subsequent triggers to fail with -EBADFD. In addition, if
virtsnd_restart() fails in virtsnd_reset_done(), it still re-enables
snd->recover_work and does not set snd->recover_failed or disconnect the
card.
> +
> +#ifdef CONFIG_PM_SLEEP
> +/**
> + * virtsnd_freeze() - Suspend device.
> + * @vdev: VirtIO parent device.
> + *
> + * Context: Any context.
> + * Return: 0 on success, -errno on failure.
> + */
> +static int virtsnd_freeze(struct virtio_device *vdev)
> +{
> + struct virtio_snd *snd = vdev->priv;
> +
> + disable_delayed_work_sync(&snd->recover_work);
> + virtsnd_quiesce(snd);
> +
> + return 0;
> +}
> +
> +/**
> + * virtsnd_restore() - Resume device.
> + * @vdev: VirtIO parent device.
> + *
> + * Context: Any context.
> + * Return: 0 on success, -errno on failure.
> + */
> +static int virtsnd_restore(struct virtio_device *vdev)
> +{
> + struct virtio_snd *snd = vdev->priv;
> + int rc;
> +
> + rc = virtsnd_restart(snd);
> + enable_delayed_work(&snd->recover_work);
> +
> + return rc;
> +}
[Severity: Medium]
Can virtsnd_recover_work() race with virtio_device_freeze() or a
transport-initiated virtio_device_reset_prepare(), and can
virtsnd_restore() or virtsnd_reset_done() revive a card that was already
disconnected after a failed recovery?
virtio_device_freeze() and virtio_device_reset_prepare() call
virtio_config_core_disable(vdev) before invoking the driver callback:
CPU0 kworker
---- -------
virtio_device_freeze(vdev) virtsnd_recover_work()
virtio_config_core_disable(vdev)
virtsnd_freeze(vdev)
disable_delayed_work_sync(...)
virtio_device_reset_done(vdev)
virtio_device_restore_priv(vdev)
virtio_config_core_enable(vdev)
virtsnd_quiesce(snd) <-- vdev->config_core_enabled left true
Furthermore, if virtsnd_recover_work() fails its reset, sets
snd->recover_failed = true, and disconnects snd->card, a subsequent
virtsnd_restore() or transport-initiated virtsnd_reset_done() does not
check snd->recover_failed and unconditionally calls virtsnd_restart() and
enable_delayed_work(&snd->recover_work), re-initializing virtqueues and
enabling event dispatch on a disconnected card.
> diff --git a/sound/virtio/virtio_pcm_ops.c b/sound/virtio/virtio_pcm_ops.c
> index 15d4473f4c0a0..dcce428d704c7 100644
> --- a/sound/virtio/virtio_pcm_ops.c
> +++ b/sound/virtio/virtio_pcm_ops.c
[ ... ]
> @@ -661,6 +706,9 @@ static int virtsnd_pcm_hw_params(struct snd_pcm_substream *substream,
> u64 epoch;
> int rc;
>
> + rc = virtsnd_recover_wait(vss->snd);
> + if (rc)
> + return rc;
> guard(mutex)(&vss->lifecycle_lock);
[Severity: High]
Why don't virtsnd_pcm_open(), virtsnd_pcm_hw_free(), and
virtsnd_pcm_sync_stop() also wait for an in-progress recovery reset?
The commit message notes that open and hw_free do not wait because they
run under pcm->open_mutex, which recovery takes to suspend the streams.
However:
1. virtsnd_recover_work() only holds pcm->open_mutex inside
snd_pcm_suspend_all() and releases it before calling
virtio_device_reset_prepare() and virtio_device_reset_done(), so
pcm->open_mutex is not held while vss->resetting and snd->recovering
are true.
2. SNDRV_PCM_IOCTL_HW_FREE (snd_pcm_hw_free()) and the error path of
snd_pcm_hw_params() only take runtime->buffer_mutex, never
pcm->open_mutex.
As a result, during recovery virtsnd_pcm_open() fails with -ECANCELED or
-EIO; virtsnd_pcm_hw_free() on close returns -EIO or -ECANCELED from
virtsnd_pcm_sync_stop_locked() before virtsnd_reset_vqs() detaches pending
buffers, causing !virtsnd_pcm_msg_pending_num(vss) to be false and skipping
virtsnd_pcm_msg_free(vss) so vss->msgs and vss->dma_pages remain allocated;
and when snd_pcm_hw_params() or snd_pcm_do_prepare() calls
snd_pcm_sync_stop() before invoking ops->hw_params() or ops->prepare(),
virtsnd_pcm_sync_stop() sees vss->resetting == true and sets
substream->runtime->stop_operating = true again before
virtsnd_recover_wait() runs.
[ ... ]
> @@ -933,6 +982,10 @@ static int virtsnd_pcm_prepare(struct snd_pcm_substream *substream)
>
> int rc;
>
> + /* Applications prepare again as soon as recovery suspends the stream. */
> + rc = virtsnd_recover_wait(vss->snd);
> + if (rc)
> + return rc;
> guard(mutex)(&vss->lifecycle_lock);
> rc = virtsnd_pcm_prepare_locked(substream);
[Severity: Medium]
Can ordering races between virtsnd_pcm_control_settle(),
virtsnd_recover_work(), and virtsnd_recover_wait() cause
snd_pcm_prepare() and snd_pcm_hw_params() to fail with -EIO or -ESTRPIPE
instead of waiting for recovery?
Two sequences can happen when recovery starts:
1. In snd_pcm_do_prepare() and snd_pcm_hw_params(), ALSA core calls
snd_pcm_sync_stop() before ops->prepare() or ops->hw_params(). When
vss->control_uncertain is true and runtime->stop_operating is set,
snd_pcm_sync_stop() -> virtsnd_pcm_control_settle() waits 4 timeouts,
calls virtsnd_recover_schedule(vss->snd, 0), and returns -EIO (which
snd_pcm_sync_stop() ignores). Because virtsnd_recover_schedule() only
queues snd->recover_work without setting snd->recovering = true,
virtsnd_recover_wait() in virtsnd_pcm_prepare() or
virtsnd_pcm_hw_params() sees snd->recovering == false and returns 0
immediately. virtsnd_pcm_prepare_locked() or virtsnd_pcm_hw_params()
then enters virtsnd_pcm_control_settle() a second time, which either
returns -EIO as soon as virtsnd_recover_work() sets snd->recovering =
true (leaving runtime->state in SNDRV_PCM_STATE_SETUP, which
snd_pcm_suspend_all() then skips with -EBUSY) or waits another 4
timeouts if rate-limited by snd->recover_next.
2. In virtsnd_recover_work(), WRITE_ONCE(snd->recovering, true) runs
before snd_pcm_suspend_all(). If a thread enters snd_pcm_prepare() on
a stream in PREPARED or XRUN state after snd->recovering becomes true
and sleeps in virtsnd_recover_wait() before snd_pcm_suspend_all()
suspends that substream, snd_pcm_suspend_all() sets runtime->state to
SNDRV_PCM_STATE_SUSPENDED while the thread is sleeping. After the
reset completes, virtsnd_pcm_prepare_locked() configures and prepares
the reset device, but snd_pcm_finish_prepare() then sees
runtime->state == SNDRV_PCM_STATE_SUSPENDED, cancels the completed
prepare via virtsnd_pcm_prepare_complete(s, false), and returns
-ESTRPIPE:
sound/core/pcm_native.c:snd_pcm_finish_prepare() {
if (runtime->state == SNDRV_PCM_STATE_SUSPENDED) {
snd_pcm_Substream_ops_prepare_complete(substream, false);
return -ESTRPIPE;
}
}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-fix-virtio-snd-recovery-final-v32-0-4e95979e312c@gmail.com?part=20
^ permalink raw reply [flat|nested] 41+ messages in thread