diff --git a/drivers/dma/tegra210-adma.c b/drivers/dma/tegra210-adma.c index 6fb90aa7f85b1..03e88ea1841bb 100644 --- a/drivers/dma/tegra210-adma.c +++ b/drivers/dma/tegra210-adma.c @@ -734,18 +734,18 @@ static void tegra_adma_terminate_all(struct dma_chan *dc) /* * The tasklet may be active and so set the current callback * count to zero to terminate the tasklet. + * 1. If tegra_adma_terminate_all is called by the tasklet callback + * itself due to xrun, callback_count=0 ensures the callback is not + * called again after the current callback returns. + * 2. If tegra_adma_terminate_all is called by the client thread that + * wants to stop audio, no new callbacks are going to be scheduled + * after this point. However, the callback can still be running on + * another CPU at this point if it was started before tdc->lock is + * grabbed. We sync with the callback in device_free_chan_resources + * since the current context is atomic. */ tdc->callback_count = 0; - /* Make sure the tasklet has stopped running before we return. */ - if (!in_interrupt()) { - tdc->busy = true; - spin_unlock_irqrestore(&tdc->lock, flags); - tasklet_kill(&tdc->tasklet); - spin_lock_irqsave(&tdc->lock, flags); - tdc->busy = false; - } - spin_unlock_irqrestore(&tdc->lock, flags); } @@ -1141,6 +1141,18 @@ static void tegra_adma_free_chan_resources(struct dma_chan *dc) if (tdc->busy) tegra_adma_terminate_all(dc); + + /* Ensure the callback in the tasklet finishes before freeing resources. + * We cannot sync in tegra_adma_terminate_all, because it is called + * from atomic context SNDRV_PCM_TRIGGER_STOP->DMA_TERMINATE_ALL. + * The current context is not atomic (from pcm_close op), so it's OK + * to sleep and sync here. + * Without this sync, the callback (e.g. dmaengine_pcm_dma_complete) + * may still be running after snd_pcm_release which frees + * snd_pcm_substream, causing use-after-free crash. + */ + tasklet_kill(&tdc->tasklet); + pm_runtime_put(tdc->tdma->dev); spin_lock_irqsave(&tdc->lock, flags); list_splice_init(&tdc->pending_sg_req, &sg_req_list);