From 161314b856b5a00a4c425958ff746885bd974721 Mon Sep 17 00:00:00 2001
From: Ben Zhang <benzh@chromium.org>
Date: Tue, 3 May 2016 21:53:16 -0700
Subject: [PATCH] CHROMIUM: dmaengine: tegra-adma: fix race between
 tasklet/terminate_all (take 3)

commit 58148c0ad35f ("CHROMIUM: dmaengine: tegra-adma: Fix
another race between tasklet/terminate_all") added
tasklet_kill which is called from atomic context, causing
scheduling while atomic bug. This patch moves the the sync
point to device_free_chan_resources which is not atomic.

BUG=chrome-os-partner:46247, b:26806769
TEST=Add udelay(10000) in the tasklet before calling the
callback to simulate heavy system load. Observe the
scheduling while atomic bug. After applying this patch,
observe the bug is gone and audio playback works on Ryu.

Change-Id: I8d4408364cea492f094e15306c09d153315c9782
Signed-off-by: Ben Zhang <benzh@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/342355
Reviewed-by: Andrew Bresticker <abrestic@chromium.org>
(cherry picked from commit 20115e8a450a03cea5ff7b2e5db5fc35081b4637)
Reviewed-on: https://chromium-review.googlesource.com/342835
Commit-Queue: Andrew Bresticker <abrestic@chromium.org>
Tested-by: Andrew Bresticker <abrestic@chromium.org>
---
 drivers/dma/tegra210-adma.c | 30 +++++++++++++++++++++---------
 1 file changed, 21 insertions(+), 9 deletions(-)

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);