[ALSA] Fix races between PCM drain and other ops

PCM Midlevel
Fix semaphore races between PCM drain and other ops.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
diff --git a/sound/core/pcm_native.c b/sound/core/pcm_native.c
index 4e58241..10c2c98 100644
--- a/sound/core/pcm_native.c
+++ b/sound/core/pcm_native.c
@@ -1368,43 +1368,32 @@
 	if (runtime->status->state == SNDRV_PCM_STATE_OPEN)
 		return -EBADFD;
 
-	down_read(&snd_pcm_link_rwsem);
 	snd_power_lock(card);
 	if (runtime->status->state == SNDRV_PCM_STATE_SUSPENDED) {
 		result = snd_power_wait(card, SNDRV_CTL_POWER_D0, substream->ffile);
-		if (result < 0)
-			goto _unlock;
+		if (result < 0) {
+			snd_power_unlock(card);
+			return result;
+		}
 	}
 
 	/* allocate temporary record for drain sync */
+	down_read(&snd_pcm_link_rwsem);
 	if (snd_pcm_stream_linked(substream)) {
 		drec = kmalloc(substream->group->count * sizeof(*drec), GFP_KERNEL);
 		if (! drec) {
-			result = -ENOMEM;
-			goto _unlock;
+			up_read(&snd_pcm_link_rwsem);
+			snd_power_unlock(card);
+			return -ENOMEM;
 		}
 	} else
 		drec = &drec_tmp;
 
-	snd_pcm_stream_lock_irq(substream);
-	/* resume pause */
-	if (runtime->status->state == SNDRV_PCM_STATE_PAUSED)
-		snd_pcm_pause(substream, 0);
-
-	/* pre-start/stop - all running streams are changed to DRAINING state */
-	result = snd_pcm_action(&snd_pcm_action_drain_init, substream, 0);
-	if (result < 0)
-		goto _end;
-
-	/* check streams with PLAYBACK & DRAINING */
+	/* count only playback streams */
 	num_drecs = 0;
 	snd_pcm_group_for_each(pos, substream) {
 		snd_pcm_substream_t *s = snd_pcm_group_substream_entry(pos);
 		runtime = s->runtime;
-		if (runtime->status->state != SNDRV_PCM_STATE_DRAINING) {
-			runtime->status->state = SNDRV_PCM_STATE_SETUP;
-			continue;
-		}
 		if (s->stream == SNDRV_PCM_STREAM_PLAYBACK) {
 			d = &drec[num_drecs++];
 			d->substream = s;
@@ -1418,9 +1407,21 @@
 				runtime->stop_threshold = runtime->buffer_size;
 		}
 	}
-
+	up_read(&snd_pcm_link_rwsem);
 	if (! num_drecs)
-		goto _end;
+		goto _error;
+
+	snd_pcm_stream_lock_irq(substream);
+	/* resume pause */
+	if (runtime->status->state == SNDRV_PCM_STATE_PAUSED)
+		snd_pcm_pause(substream, 0);
+
+	/* pre-start/stop - all running streams are changed to DRAINING state */
+	result = snd_pcm_action(&snd_pcm_action_drain_init, substream, 0);
+	if (result < 0) {
+		snd_pcm_stream_unlock_irq(substream);
+		goto _error;
+	}
 
 	for (;;) {
 		long tout;
@@ -1428,6 +1429,15 @@
 			result = -ERESTARTSYS;
 			break;
 		}
+		/* all finished? */
+		for (i = 0; i < num_drecs; i++) {
+			runtime = drec[i].substream->runtime;
+			if (runtime->status->state == SNDRV_PCM_STATE_DRAINING)
+				break;
+		}
+		if (i == num_drecs)
+			break; /* yes, all drained */
+
 		set_current_state(TASK_INTERRUPTIBLE);
 		snd_pcm_stream_unlock_irq(substream);
 		snd_power_unlock(card);
@@ -1444,15 +1454,11 @@
 			}
 			break;
 		}
-		/* all finished? */
-		for (i = 0; i < num_drecs; i++) {
-			runtime = drec[i].substream->runtime;
-			if (runtime->status->state == SNDRV_PCM_STATE_DRAINING)
-				break;
-		}
-		if (i == num_drecs)
-			break;
 	}
+
+	snd_pcm_stream_unlock_irq(substream);
+
+ _error:
 	for (i = 0; i < num_drecs; i++) {
 		d = &drec[i];
 		runtime = d->substream->runtime;
@@ -1460,13 +1466,9 @@
 		runtime->stop_threshold = d->stop_threshold;
 	}
 
- _end:
-	snd_pcm_stream_unlock_irq(substream);
 	if (drec != &drec_tmp)
 		kfree(drec);
- _unlock:
 	snd_power_unlock(card);
-	up_read(&snd_pcm_link_rwsem);
 
 	return result;
 }