From d7792751e23fe22fbcd77b1ecfce062a960cb48f Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:42:13 +0300 Subject: [PATCH 1/7] ASoC: soc-pcm: Handle RESUME of a running or paused shared BE The SUSPEND trigger drops the be_start reference of the FE for every BE in START state, but RESUME only takes a reference if the BE is in SUSPEND state. If a BE is shared between FEs, the BE is moved to SUSPEND state only when the last FE is suspended and it is moved back to START state when the first FE is resumed. The rest of the FEs will skip the BE on RESUME and the be_start counter ends up being lower than the number of FEs using the BE, which will cause the BE to be stopped while it is still in use by another FE. The BE is also skipped on RESUME if it has been paused in the meantime by another FE which has been resumed already, leaving the BE paused while the resumed FE is running. Handle these cases similarly to START: if the BE is already running, take a reference for the FE and if the BE is paused, release it. The BE is still skipped if another FE has resumed and then stopped or re-prepared it while this FE is suspended. The suspended FE does not hold a reference on the BE and its DPCM state is STOP, handling this would need the suspended state of the FE to be tracked. Fixes: 848aedfdc6ba ("ASoC: soc-pcm: test refcount before triggering") Cc: stable@vger.kernel.org Signed-off-by: Peter Ujfalusi --- sound/soc/soc-pcm.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index ef8ccf52798eb6..98c415655ff9ca 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -2266,14 +2266,20 @@ int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; break; case SNDRV_PCM_TRIGGER_RESUME: - if ((be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND)) + if (!be->dpcm[stream].be_start && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) goto next; be->dpcm[stream].be_start++; if (be->dpcm[stream].be_start != 1) goto next; - ret = soc_pcm_trigger(be_substream, cmd); + if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_PAUSED) + ret = soc_pcm_trigger(be_substream, + SNDRV_PCM_TRIGGER_PAUSE_RELEASE); + else + ret = soc_pcm_trigger(be_substream, cmd); if (ret) { be->dpcm[stream].be_start--; goto next; From 75ddeed2638ea1d6297726ee9750e08137215e54 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:42:22 +0300 Subject: [PATCH 2/7] ASoC: soc-pcm: Restore the pause state when BE PAUSE_RELEASE fails On PAUSE_RELEASE the fe_pause flag is cleared and the be_pause counter is decremented before the BE is triggered, but only the be_start counter is restored if the trigger fails. The BE stays in PAUSED state while the pause reference of the FE is lost. When the stream is closed later, the PCM core releases the pause before stopping the stream which takes the be_pause counter negative and the STOP will be handled as a pause/stop transition, leaving the BE in PAUSED state. For example with SOF the BE pipeline cannot be freed on close and the stream is not usable anymore after this. Restore the fe_pause and be_pause as well when the PAUSE_RELEASE fails. The pause references are also not cleared when the FE or the BE is opened. If the PAUSE_RELEASE fails again when the paused stream is closed, the PCM core does not send STOP for it and the fe_pause and be_pause would be carried over to the next use of the FE and the BE. Clear them on open, as it is done for be_start. Fixes: 9995c1d096c8 ("ASoC: soc-pcm: improve BE transition for PAUSE_RELEASE") Cc: stable@vger.kernel.org # 6.1+ Signed-off-by: Peter Ujfalusi --- sound/soc/soc-compress.c | 1 + sound/soc/soc-pcm.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/sound/soc/soc-compress.c b/sound/soc/soc-compress.c index a941ed0908c667..10fb9783b13d5d 100644 --- a/sound/soc/soc-compress.c +++ b/sound/soc/soc-compress.c @@ -282,6 +282,7 @@ static int soc_compr_open_fe(struct snd_compr_stream *cstream) dpcm_path_put(&list); fe->dpcm[stream].state = SND_SOC_DPCM_STATE_OPEN; + fe->dpcm[stream].fe_pause = false; fe->dpcm[stream].runtime_update = SND_SOC_DPCM_UPDATE_NO; snd_soc_runtime_activate(fe, stream); diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index 98c415655ff9ca..324c84bd7b0ffb 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -1766,6 +1766,7 @@ int dpcm_be_dai_startup(struct snd_soc_pcm_runtime *fe, int stream) goto unwind; } be->dpcm[stream].be_start = 0; + be->dpcm[stream].be_pause = 0; be->dpcm[stream].state = SND_SOC_DPCM_STATE_OPEN; count++; } @@ -1997,6 +1998,7 @@ static int dpcm_fe_dai_startup(struct snd_pcm_substream *fe_substream) goto unwind; fe->dpcm[stream].state = SND_SOC_DPCM_STATE_OPEN; + fe->dpcm[stream].fe_pause = false; dpcm_runtime_setup_fe(fe_substream); @@ -2303,6 +2305,8 @@ int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, ret = soc_pcm_trigger(be_substream, cmd); if (ret) { be->dpcm[stream].be_start--; + fe->dpcm[stream].fe_pause = true; + be->dpcm[stream].be_pause++; goto next; } From e28c8e8a3c2752341e5f6f89d605976a29f822f7 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:43:07 +0300 Subject: [PATCH 3/7] ASoC: soc-pcm: Move the BE trigger handling to a helper function Move the trigger handling of a single BE out from dpcm_be_dai_trigger() to a new helper function to make it possible to re-use it for unwinding the BE triggers in case of an error. While at it, drop the unnecessary parentheses in the moved conditions and put the dpcm_be_dai_trigger() prototype on a single line. No functional change. Signed-off-by: Peter Ujfalusi --- sound/soc/soc-pcm.c | 275 +++++++++++++++++++++++--------------------- 1 file changed, 141 insertions(+), 134 deletions(-) diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index 324c84bd7b0ffb..1a521b4c229387 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -2218,177 +2218,184 @@ static int dpcm_fe_dai_hw_params(struct snd_pcm_substream *substream, return soc_pcm_ret(fe, ret); } -int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, - int cmd) +static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, + struct snd_soc_pcm_runtime *be, int stream, + int cmd) { - struct snd_soc_pcm_runtime *be; + struct snd_pcm_substream *be_substream; bool pause_stop_transition; - struct snd_soc_dpcm *dpcm; unsigned long flags; int ret = 0; - for_each_dpcm_be(fe, stream, dpcm) { - struct snd_pcm_substream *be_substream; + be_substream = snd_soc_dpcm_get_substream(be, stream); - be = dpcm->be; - be_substream = snd_soc_dpcm_get_substream(be, stream); + snd_pcm_stream_lock_irqsave_nested(be_substream, flags); - snd_pcm_stream_lock_irqsave_nested(be_substream, flags); + /* is this op for this BE ? */ + if (!snd_soc_dpcm_can_be_update(fe, be, stream)) + goto next; - /* is this op for this BE ? */ - if (!snd_soc_dpcm_can_be_update(fe, be, stream)) + dev_dbg(be->dev, "ASoC: trigger BE %s cmd %d\n", + be->dai_link->name, cmd); + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + if (!be->dpcm[stream].be_start && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PREPARE && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_STOP && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) goto next; - dev_dbg(be->dev, "ASoC: trigger BE %s cmd %d\n", - be->dai_link->name, cmd); + be->dpcm[stream].be_start++; + if (be->dpcm[stream].be_start != 1) + goto next; - switch (cmd) { - case SNDRV_PCM_TRIGGER_START: - if (!be->dpcm[stream].be_start && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_PREPARE) && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_STOP) && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED)) - goto next; + if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_PAUSED) + ret = soc_pcm_trigger(be_substream, + SNDRV_PCM_TRIGGER_PAUSE_RELEASE); + else + ret = soc_pcm_trigger(be_substream, + SNDRV_PCM_TRIGGER_START); + if (ret) { + be->dpcm[stream].be_start--; + goto next; + } - be->dpcm[stream].be_start++; - if (be->dpcm[stream].be_start != 1) - goto next; + be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; + break; + case SNDRV_PCM_TRIGGER_RESUME: + if (!be->dpcm[stream].be_start && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) + goto next; - if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_PAUSED) - ret = soc_pcm_trigger(be_substream, - SNDRV_PCM_TRIGGER_PAUSE_RELEASE); - else - ret = soc_pcm_trigger(be_substream, - SNDRV_PCM_TRIGGER_START); - if (ret) { - be->dpcm[stream].be_start--; - goto next; - } + be->dpcm[stream].be_start++; + if (be->dpcm[stream].be_start != 1) + goto next; - be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; - break; - case SNDRV_PCM_TRIGGER_RESUME: - if (!be->dpcm[stream].be_start && - be->dpcm[stream].state != SND_SOC_DPCM_STATE_SUSPEND && - be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) - goto next; + if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_PAUSED) + ret = soc_pcm_trigger(be_substream, + SNDRV_PCM_TRIGGER_PAUSE_RELEASE); + else + ret = soc_pcm_trigger(be_substream, cmd); + if (ret) { + be->dpcm[stream].be_start--; + goto next; + } - be->dpcm[stream].be_start++; - if (be->dpcm[stream].be_start != 1) - goto next; + be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; + break; + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + if (!be->dpcm[stream].be_start && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_START && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) + goto next; - if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_PAUSED) - ret = soc_pcm_trigger(be_substream, - SNDRV_PCM_TRIGGER_PAUSE_RELEASE); - else - ret = soc_pcm_trigger(be_substream, cmd); - if (ret) { - be->dpcm[stream].be_start--; - goto next; - } + fe->dpcm[stream].fe_pause = false; + be->dpcm[stream].be_pause--; - be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; - break; - case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: - if (!be->dpcm[stream].be_start && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED)) - goto next; + be->dpcm[stream].be_start++; + if (be->dpcm[stream].be_start != 1) + goto next; + + ret = soc_pcm_trigger(be_substream, cmd); + if (ret) { + be->dpcm[stream].be_start--; + fe->dpcm[stream].fe_pause = true; + be->dpcm[stream].be_pause++; + goto next; + } + + be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; + break; + case SNDRV_PCM_TRIGGER_STOP: + if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START && + be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) + goto next; + + if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_START) + be->dpcm[stream].be_start--; + + if (be->dpcm[stream].be_start != 0) + goto next; + pause_stop_transition = false; + if (fe->dpcm[stream].fe_pause) { + pause_stop_transition = true; fe->dpcm[stream].fe_pause = false; be->dpcm[stream].be_pause--; + } - be->dpcm[stream].be_start++; - if (be->dpcm[stream].be_start != 1) - goto next; + if (be->dpcm[stream].be_pause != 0) + ret = soc_pcm_trigger(be_substream, SNDRV_PCM_TRIGGER_PAUSE_PUSH); + else + ret = soc_pcm_trigger(be_substream, SNDRV_PCM_TRIGGER_STOP); - ret = soc_pcm_trigger(be_substream, cmd); - if (ret) { - be->dpcm[stream].be_start--; + if (ret) { + if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_START) + be->dpcm[stream].be_start++; + if (pause_stop_transition) { fe->dpcm[stream].fe_pause = true; be->dpcm[stream].be_pause++; - goto next; } + goto next; + } - be->dpcm[stream].state = SND_SOC_DPCM_STATE_START; - break; - case SNDRV_PCM_TRIGGER_STOP: - if ((be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) && - (be->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED)) - goto next; - - if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_START) - be->dpcm[stream].be_start--; + if (be->dpcm[stream].be_pause != 0) + be->dpcm[stream].state = SND_SOC_DPCM_STATE_PAUSED; + else + be->dpcm[stream].state = SND_SOC_DPCM_STATE_STOP; - if (be->dpcm[stream].be_start != 0) - goto next; + break; + case SNDRV_PCM_TRIGGER_SUSPEND: + if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) + goto next; - pause_stop_transition = false; - if (fe->dpcm[stream].fe_pause) { - pause_stop_transition = true; - fe->dpcm[stream].fe_pause = false; - be->dpcm[stream].be_pause--; - } + be->dpcm[stream].be_start--; + if (be->dpcm[stream].be_start != 0) + goto next; - if (be->dpcm[stream].be_pause != 0) - ret = soc_pcm_trigger(be_substream, SNDRV_PCM_TRIGGER_PAUSE_PUSH); - else - ret = soc_pcm_trigger(be_substream, SNDRV_PCM_TRIGGER_STOP); - - if (ret) { - if (be->dpcm[stream].state == SND_SOC_DPCM_STATE_START) - be->dpcm[stream].be_start++; - if (pause_stop_transition) { - fe->dpcm[stream].fe_pause = true; - be->dpcm[stream].be_pause++; - } - goto next; - } + ret = soc_pcm_trigger(be_substream, cmd); + if (ret) { + be->dpcm[stream].be_start++; + goto next; + } - if (be->dpcm[stream].be_pause != 0) - be->dpcm[stream].state = SND_SOC_DPCM_STATE_PAUSED; - else - be->dpcm[stream].state = SND_SOC_DPCM_STATE_STOP; + be->dpcm[stream].state = SND_SOC_DPCM_STATE_SUSPEND; + break; + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) + goto next; - break; - case SNDRV_PCM_TRIGGER_SUSPEND: - if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) - goto next; + fe->dpcm[stream].fe_pause = true; + be->dpcm[stream].be_pause++; - be->dpcm[stream].be_start--; - if (be->dpcm[stream].be_start != 0) - goto next; - - ret = soc_pcm_trigger(be_substream, cmd); - if (ret) { - be->dpcm[stream].be_start++; - goto next; - } + be->dpcm[stream].be_start--; + if (be->dpcm[stream].be_start != 0) + goto next; - be->dpcm[stream].state = SND_SOC_DPCM_STATE_SUSPEND; - break; - case SNDRV_PCM_TRIGGER_PAUSE_PUSH: - if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) - goto next; + ret = soc_pcm_trigger(be_substream, cmd); + if (ret) { + be->dpcm[stream].be_start++; + goto next; + } - fe->dpcm[stream].fe_pause = true; - be->dpcm[stream].be_pause++; + be->dpcm[stream].state = SND_SOC_DPCM_STATE_PAUSED; + break; + } +next: + snd_pcm_stream_unlock_irqrestore(be_substream, flags); - be->dpcm[stream].be_start--; - if (be->dpcm[stream].be_start != 0) - goto next; + return ret; +} - ret = soc_pcm_trigger(be_substream, cmd); - if (ret) { - be->dpcm[stream].be_start++; - goto next; - } +int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, int cmd) +{ + struct snd_soc_dpcm *dpcm; + int ret = 0; - be->dpcm[stream].state = SND_SOC_DPCM_STATE_PAUSED; - break; - } -next: - snd_pcm_stream_unlock_irqrestore(be_substream, flags); + for_each_dpcm_be(fe, stream, dpcm) { + ret = dpcm_be_dai_trigger_be(fe, dpcm->be, stream, cmd); if (ret) break; } From ed44acaa3100cd7a6a6a73c4b097239b02b9f43c Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:43:55 +0300 Subject: [PATCH 4/7] ASoC: soc-pcm: Unwind the triggered BEs on failure If the trigger of a BE fails in dpcm_be_dai_trigger() then the BEs which have been already triggered are left in the new state with the be_start and be_pause reference counts taken for the FE. The PCM core issues an undo trigger for a single substream, but not for the failing substream of a linked group and the compress core does not undo failed triggers at all. dpcm_run_update_startup() also expects that the BEs are stopped on failure. The references and BE states will remain unbalanced, the BEs might be left running. Unwind the already triggered BEs when a START, RESUME or PAUSE_RELEASE fails with the corresponding STOP, SUSPEND or PAUSE_PUSH command. Record in the DPCM link if the BE reference has been taken for the FE and only unwind those BEs. The state of a BE which has been skipped can be changed by the trigger of another FE between the trigger and the unwind, so the decision cannot be made on the BE state at unwind time. The per BE trigger handling takes care of the reference counting and the pause/stop transitions. The STOP used to unwind a failed START must not be handled as a pause/stop transition since the START does not take the BE pause reference: the FE can be paused when new BEs are started by dpcm_run_update_startup(). The SUSPEND used to unwind a RESUME must not suspend the BE if another FE has paused it in the meantime, this would leave the BE in SUSPEND state and the PAUSE_RELEASE of the other FE would skip it. Pause the BE instead, as it would have been done by the PAUSE_PUSH of the other FE without the RESUME. Add the dpcm_be_dai_trigger_unwind() function and the snd_soc_trigger_unwind_cmd() helper to soc-internal.h so they can be used to unwind the BEs when the FE trigger fails and by soc-compress as well. Failing STOP, SUSPEND or PAUSE_PUSH is not unwound. Signed-off-by: Peter Ujfalusi --- include/sound/soc-dpcm.h | 6 ++++ sound/soc/soc-internal.h | 24 ++++++++++++++ sound/soc/soc-pcm.c | 68 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 92 insertions(+), 6 deletions(-) diff --git a/include/sound/soc-dpcm.h b/include/sound/soc-dpcm.h index af24665e37e86b..f0598bff143a3f 100644 --- a/include/sound/soc-dpcm.h +++ b/include/sound/soc-dpcm.h @@ -77,6 +77,12 @@ struct snd_soc_dpcm { struct list_head list_be; struct list_head list_fe; + /* + * BE reference taken for the FE by START, RESUME or PAUSE_RELEASE, + * only accessed from the trigger path of the FE + */ + bool trigger_ref; + #ifdef CONFIG_DEBUG_FS struct dentry *debugfs_state; #endif diff --git a/sound/soc/soc-internal.h b/sound/soc/soc-internal.h index 31559918355f87..f5631876268fb0 100644 --- a/sound/soc/soc-internal.h +++ b/sound/soc/soc-internal.h @@ -36,4 +36,28 @@ struct clk *snd_soc_dai_get_bclk(struct snd_soc_dai *dai); unsigned int snd_soc_dai_get_bclk_ratio(struct snd_soc_dai *dai); void snd_soc_dai_active_update(struct snd_soc_dai *dai, int stream, int action); +/* + * In soc-pcm + */ +void dpcm_be_dai_trigger_unwind(struct snd_soc_pcm_runtime *fe, int stream, + int cmd); + +/* + * Get the trigger command to unwind a failed START, RESUME or PAUSE_RELEASE. + * Returns -EINVAL for commands that are not unwound. + */ +static inline int snd_soc_trigger_unwind_cmd(int cmd) +{ + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + return SNDRV_PCM_TRIGGER_STOP; + case SNDRV_PCM_TRIGGER_RESUME: + return SNDRV_PCM_TRIGGER_SUSPEND; + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + return SNDRV_PCM_TRIGGER_PAUSE_PUSH; + default: + return -EINVAL; + } +} + #endif /* __SOC_INTERNAL_H */ diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index 1a521b4c229387..3470e6ed25b910 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -2219,11 +2219,13 @@ static int dpcm_fe_dai_hw_params(struct snd_pcm_substream *substream, } static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, - struct snd_soc_pcm_runtime *be, int stream, - int cmd) + struct snd_soc_dpcm *dpcm, int stream, + int cmd, bool rollback) { + struct snd_soc_pcm_runtime *be = dpcm->be; struct snd_pcm_substream *be_substream; bool pause_stop_transition; + bool suspend_to_pause; unsigned long flags; int ret = 0; @@ -2247,6 +2249,7 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, goto next; be->dpcm[stream].be_start++; + dpcm->trigger_ref = true; if (be->dpcm[stream].be_start != 1) goto next; @@ -2258,6 +2261,7 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, SNDRV_PCM_TRIGGER_START); if (ret) { be->dpcm[stream].be_start--; + dpcm->trigger_ref = false; goto next; } @@ -2270,6 +2274,7 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, goto next; be->dpcm[stream].be_start++; + dpcm->trigger_ref = true; if (be->dpcm[stream].be_start != 1) goto next; @@ -2280,6 +2285,7 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, ret = soc_pcm_trigger(be_substream, cmd); if (ret) { be->dpcm[stream].be_start--; + dpcm->trigger_ref = false; goto next; } @@ -2295,12 +2301,14 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, be->dpcm[stream].be_pause--; be->dpcm[stream].be_start++; + dpcm->trigger_ref = true; if (be->dpcm[stream].be_start != 1) goto next; ret = soc_pcm_trigger(be_substream, cmd); if (ret) { be->dpcm[stream].be_start--; + dpcm->trigger_ref = false; fe->dpcm[stream].fe_pause = true; be->dpcm[stream].be_pause++; goto next; @@ -2319,8 +2327,12 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, if (be->dpcm[stream].be_start != 0) goto next; + /* + * A STOP to roll back a failed START is not a pause/stop + * transition, the BE pause reference has not been taken by START + */ pause_stop_transition = false; - if (fe->dpcm[stream].fe_pause) { + if (!rollback && fe->dpcm[stream].fe_pause) { pause_stop_transition = true; fe->dpcm[stream].fe_pause = false; be->dpcm[stream].be_pause--; @@ -2355,13 +2367,26 @@ static int dpcm_be_dai_trigger_be(struct snd_soc_pcm_runtime *fe, if (be->dpcm[stream].be_start != 0) goto next; - ret = soc_pcm_trigger(be_substream, cmd); + /* + * A SUSPEND to roll back a failed RESUME must not suspend the BE + * under a paused FE, pause the BE as the PAUSE_PUSH of that FE + * would have done without the RESUME. + */ + suspend_to_pause = rollback && be->dpcm[stream].be_pause != 0; + if (suspend_to_pause) + ret = soc_pcm_trigger(be_substream, + SNDRV_PCM_TRIGGER_PAUSE_PUSH); + else + ret = soc_pcm_trigger(be_substream, cmd); if (ret) { be->dpcm[stream].be_start++; goto next; } - be->dpcm[stream].state = SND_SOC_DPCM_STATE_SUSPEND; + if (suspend_to_pause) + be->dpcm[stream].state = SND_SOC_DPCM_STATE_PAUSED; + else + be->dpcm[stream].state = SND_SOC_DPCM_STATE_SUSPEND; break; case SNDRV_PCM_TRIGGER_PAUSE_PUSH: if (be->dpcm[stream].state != SND_SOC_DPCM_STATE_START) @@ -2394,15 +2419,46 @@ int dpcm_be_dai_trigger(struct snd_soc_pcm_runtime *fe, int stream, int cmd) struct snd_soc_dpcm *dpcm; int ret = 0; + for_each_dpcm_be(fe, stream, dpcm) + dpcm->trigger_ref = false; + for_each_dpcm_be(fe, stream, dpcm) { - ret = dpcm_be_dai_trigger_be(fe, dpcm->be, stream, cmd); + ret = dpcm_be_dai_trigger_be(fe, dpcm, stream, cmd, false); if (ret) break; } + + /* Unwind the BEs that have been triggered before the failing one */ + if (ret) + dpcm_be_dai_trigger_unwind(fe, stream, cmd); + return soc_pcm_ret(fe, ret); } EXPORT_SYMBOL_GPL(dpcm_be_dai_trigger); +/* + * Unwind a successful or partially failed START, RESUME or PAUSE_RELEASE on + * the BEs. Only the BEs which took a reference for the FE in the last + * dpcm_be_dai_trigger() call are unwound. + */ +void dpcm_be_dai_trigger_unwind(struct snd_soc_pcm_runtime *fe, int stream, + int cmd) +{ + int unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); + struct snd_soc_dpcm *dpcm; + + if (unwind_cmd < 0) + return; + + list_for_each_entry_reverse(dpcm, &fe->dpcm[stream].be_clients, list_be) { + if (!dpcm->trigger_ref) + continue; + + dpcm->trigger_ref = false; + dpcm_be_dai_trigger_be(fe, dpcm, stream, unwind_cmd, true); + } +} + static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream, int cmd, bool fe_first) { From c2f2b776b228085ad83bd98c8a243347f913f0e1 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:45:36 +0300 Subject: [PATCH 5/7] ASoC: soc-pcm: Unwind the FE and BE triggers on failure If the FE is triggered before the BEs and the BE trigger fails then the FE is left in the new state. If the BEs are triggered first and the FE trigger fails then the BEs are left in the new state. For a single substream the PCM core issues an undo trigger after a failed START (STOP) or PAUSE_RELEASE (PAUSE_PUSH), but there is no undo for the failing substream of a linked group, for a failed RESUME or for a trigger which has been deferred by DPCM during a runtime update. Unwind the part which has been successfully triggered when START, RESUME or PAUSE_RELEASE fails, the same way as soc_pcm_trigger() rolls back the DAIs and components. With the unwind in place the undo trigger from the PCM core is redundant and it is also harmful already without the unwind: the undo is propagated to the BEs as well, regardless of whether they have been triggered for this FE or not. If a BE is shared with another running FE then its be_start reference is dropped without being taken and the BE is stopped under the other FE. The FE components also receive a STOP or PAUSE_PUSH without the corresponding successful START or PAUSE_RELEASE. This has been observed with SOF using FE-first trigger order (as used by IPC4 for capture) when the START of the Speaker PCM fails while the Deepbuffer Speaker PCM is running on the same BE: the BE is stopped and the Deepbuffer playback fails. Ignore the STOP and PAUSE_PUSH if the FE is not in a state where these are valid. The dpcm state of the FE is only updated on successful trigger, so it can be used to detect this case. Link: https://lore.kernel.org/linux-sound/asS7ktuZo6z68chF@sirena.co.uk/ Suggested-by: Mark Brown Signed-off-by: Peter Ujfalusi --- sound/soc/soc-pcm.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sound/soc/soc-pcm.c b/sound/soc/soc-pcm.c index 3470e6ed25b910..531a85e0378525 100644 --- a/sound/soc/soc-pcm.c +++ b/sound/soc/soc-pcm.c @@ -2463,6 +2463,7 @@ static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream, int cmd, bool fe_first) { struct snd_soc_pcm_runtime *fe = snd_soc_substream_to_rtd(substream); + int unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); int ret; /* call trigger on the frontend before the backend. */ @@ -2475,6 +2476,8 @@ static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream, goto end; ret = dpcm_be_dai_trigger(fe, substream->stream, cmd); + if (ret < 0 && unwind_cmd >= 0) + soc_pcm_trigger(substream, unwind_cmd); } /* call trigger on the frontend after the backend. */ else { @@ -2486,6 +2489,8 @@ static int dpcm_dai_trigger_fe_be(struct snd_pcm_substream *substream, fe->dai_link->name, cmd); ret = soc_pcm_trigger(substream, cmd); + if (ret < 0) + dpcm_be_dai_trigger_unwind(fe, substream->stream, cmd); } end: return snd_soc_ret(fe->dev, ret, "trigger FE cmd: %d failed\n", cmd); @@ -2515,6 +2520,25 @@ static int dpcm_fe_dai_do_trigger(struct snd_pcm_substream *substream, int cmd) goto out; } + switch (cmd) { + case SNDRV_PCM_TRIGGER_STOP: + /* + * A failed START is unwound already, ignore the undo STOP from + * the PCM core if the FE has not been started. + */ + if (fe->dpcm[stream].state != SND_SOC_DPCM_STATE_START && + fe->dpcm[stream].state != SND_SOC_DPCM_STATE_PAUSED) + goto out; + break; + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + /* Same for the undo PAUSE_PUSH of a failed PAUSE_RELEASE */ + if (fe->dpcm[stream].state != SND_SOC_DPCM_STATE_START) + goto out; + break; + default: + break; + } + switch (cmd) { case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: From 6689ac2977c284025ae2e8d0f7dcd904d9fd32cb Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:52:29 +0300 Subject: [PATCH 6/7] ASoC: soc-component: Unwind the compress trigger of components on failure snd_soc_component_compr_trigger() returns on the first failing component leaving the components which have been triggered before in the new state. The compress core does not issue any undo trigger on failure, so these components would be left running. Unwind the already triggered components when a START, RESUME or PAUSE_RELEASE fails with the corresponding STOP, SUSPEND or PAUSE_PUSH command. Do the same in soc_compr_trigger() for non-DPCM compressed streams if the CPU DAI trigger fails after the components have been triggered. Signed-off-by: Peter Ujfalusi --- sound/soc/soc-component.c | 23 ++++++++++++++++++++++- sound/soc/soc-compress.c | 7 ++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/sound/soc/soc-component.c b/sound/soc/soc-component.c index 34efb270ce2989..f8d3d078332cc9 100644 --- a/sound/soc/soc-component.c +++ b/sound/soc/soc-component.c @@ -12,6 +12,7 @@ #include #include #include +#include "soc-internal.h" #define soc_component_ret(dai, ret) _soc_component_ret(dai, __func__, ret) static inline int _soc_component_ret(struct snd_soc_component *component, const char *func, int ret) @@ -456,6 +457,7 @@ int snd_soc_component_compr_trigger(struct snd_compr_stream *cstream, int cmd) { struct snd_soc_pcm_runtime *rtd = cstream->private_data; struct snd_soc_component *component; + int unwind_cmd; int i, ret; for_each_rtd_components(rtd, i, component) { @@ -464,11 +466,30 @@ int snd_soc_component_compr_trigger(struct snd_compr_stream *cstream, int cmd) ret = component->driver->compress_ops->trigger( component, cstream, cmd); if (ret < 0) - return soc_component_ret(component, ret); + goto unwind; } } return 0; + +unwind: + ret = soc_component_ret(component, ret); + + unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); + if (unwind_cmd < 0) + return ret; + + /* Unwind the components that have been triggered before the failing one */ + while (--i >= 0) { + component = rtd->components[i]; + if (component->driver->compress_ops && + component->driver->compress_ops->trigger) + component->driver->compress_ops->trigger(component, + cstream, + unwind_cmd); + } + + return ret; } EXPORT_SYMBOL_GPL(snd_soc_component_compr_trigger); diff --git a/sound/soc/soc-compress.c b/sound/soc/soc-compress.c index 10fb9783b13d5d..1f9cb698de00c0 100644 --- a/sound/soc/soc-compress.c +++ b/sound/soc/soc-compress.c @@ -366,6 +366,7 @@ static int soc_compr_trigger(struct snd_compr_stream *cstream, int cmd) struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0); struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); int stream = cstream->direction; /* SND_COMPRESS_xxx is same as SNDRV_PCM_STREAM_xxx */ + int unwind_cmd; int ret; snd_soc_dpcm_mutex_lock(rtd); @@ -375,8 +376,12 @@ static int soc_compr_trigger(struct snd_compr_stream *cstream, int cmd) goto out; ret = snd_soc_dai_compr_trigger(cpu_dai, cstream, cmd); - if (ret < 0) + if (ret < 0) { + unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); + if (unwind_cmd >= 0) + snd_soc_component_compr_trigger(cstream, unwind_cmd); goto out; + } switch (cmd) { case SNDRV_PCM_TRIGGER_START: From 645a425e3bddc358d78f491135f1ab1d022a66a5 Mon Sep 17 00:00:00 2001 From: Peter Ujfalusi Date: Tue, 20 Oct 2026 09:46:14 +0300 Subject: [PATCH 7/7] ASoC: soc-compress: Unwind the FE and BE triggers on failure If the FE is triggered before the BEs and the BE trigger fails then the FE is left in the new state. If the BEs are triggered first and the FE trigger fails then the BEs are left in the new state. The same is true for the FE CPU DAI if the component trigger fails. The compress core does not issue any undo trigger on failure and soc_compr_free_fe() will only stop the stream if the FE is in START state, leaving the FE or the BEs running with unbalanced reference counts after the stream is closed. For example with SOF the pipelines of the stream cannot be set up again after such failure and the stream is not usable anymore. Unwind the part which has been successfully triggered when START or PAUSE_RELEASE fails, the same way as it is done in soc-pcm. Link: https://lore.kernel.org/linux-sound/asS7ktuZo6z68chF@sirena.co.uk/ Suggested-by: Mark Brown Signed-off-by: Peter Ujfalusi --- sound/soc/soc-compress.c | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/sound/soc/soc-compress.c b/sound/soc/soc-compress.c index 1f9cb698de00c0..6c5658724bc134 100644 --- a/sound/soc/soc-compress.c +++ b/sound/soc/soc-compress.c @@ -397,26 +397,45 @@ static int soc_compr_trigger(struct snd_compr_stream *cstream, int cmd) return ret; } +static int soc_compr_trigger_fe_only(struct snd_compr_stream *cstream, int cmd) +{ + struct snd_soc_pcm_runtime *fe = cstream->private_data; + struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(fe, 0); + int unwind_cmd; + int ret; + + ret = snd_soc_dai_compr_trigger(cpu_dai, cstream, cmd); + if (ret < 0) + return ret; + + ret = snd_soc_component_compr_trigger(cstream, cmd); + if (ret < 0) { + unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); + if (unwind_cmd >= 0) + snd_soc_dai_compr_trigger(cpu_dai, cstream, unwind_cmd); + } + + return ret; +} + static int soc_compr_trigger_fe_be(struct snd_compr_stream *cstream, int cmd, bool fe_first) { struct snd_soc_pcm_runtime *fe = cstream->private_data; - struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(fe, 0); + int unwind_cmd = snd_soc_trigger_unwind_cmd(cmd); int ret; if (fe_first) { dev_dbg(fe->dev, "ASoC: pre trigger FE %s cmd %d\n", fe->dai_link->name, cmd); - ret = snd_soc_dai_compr_trigger(cpu_dai, cstream, cmd); - if (ret < 0) - goto out; - - ret = snd_soc_component_compr_trigger(cstream, cmd); + ret = soc_compr_trigger_fe_only(cstream, cmd); if (ret < 0) goto out; ret = dpcm_be_dai_trigger(fe, cstream->direction, cmd); + if (ret < 0 && unwind_cmd >= 0) + soc_compr_trigger_fe_only(cstream, unwind_cmd); } else { dev_dbg(fe->dev, "ASoC: post trigger FE %s cmd %d\n", fe->dai_link->name, cmd); @@ -425,11 +444,9 @@ static int soc_compr_trigger_fe_be(struct snd_compr_stream *cstream, int cmd, if (ret < 0) goto out; - ret = snd_soc_dai_compr_trigger(cpu_dai, cstream, cmd); + ret = soc_compr_trigger_fe_only(cstream, cmd); if (ret < 0) - goto out; - - ret = snd_soc_component_compr_trigger(cstream, cmd); + dpcm_be_dai_trigger_unwind(fe, cstream->direction, cmd); } out: