Commit message: ALSA: pcm: prevent UAF in snd_pcm_info

When the device descriptor is closed, the `substream->runtime` pointer
is freed. But another thread may be in the ioctl handler, case
SNDRV_CTL_IOCTL_PCM_INFO. This case calls snd_pcm_info_user() which
calls snd_pcm_info() which accesses the now freed `substream->runtime`.

Note: this fixes CVE-2017-0861

Signed-off-by: Robb Glasser <rglasser@google.com>
Signed-off-by: Nick Desaulniers <ndesaulniers@google.com>
Cc: <stable@vger.kernel.org>
Signed-off-by: Takashi Iwai <tiwai@suse.de>
CVE: CVE-2017-0861
CWE: CWE-416
Vulnerability description: Use-after-free vulnerability in the snd_pcm_info function in the ALSA subsystem in the Linux kernel allows attackers to gain privileges via unspecified vectors.

Project: linux
File: pcm.c

static void free_chmap(struct snd_pcm_str *pstr)
{
	if (pstr->chmap_kctl) {
		snd_ctl_remove(pstr->pcm->card, pstr->chmap_kctl);
		pstr->chmap_kctl = NULL;
	}
}

Project: linux
File: pcm.c

void snd_pcm_detach_substream(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime;

	if (PCM_RUNTIME_CHECK(substream))
		return;
	runtime = substream->runtime;
	if (runtime->private_free != NULL)
		runtime->private_free(runtime);
	snd_free_pages((void*)runtime->status,
		       PAGE_ALIGN(sizeof(struct snd_pcm_mmap_status)));
	snd_free_pages((void*)runtime->control,
		       PAGE_ALIGN(sizeof(struct snd_pcm_mmap_control)));
	kfree(runtime->hw_constraints.rules);
	kfree(runtime);
	substream->runtime = NULL;
	put_pid(substream->pid);
	substream->pid = NULL;
	substream->pstr->substream_opened--;
}

Project: linux
File: pcm.c

static int _snd_pcm_new(struct snd_card *card, const char *id, int device,
		int playback_count, int capture_count, bool internal,
		struct snd_pcm **rpcm)
{
	struct snd_pcm *pcm;
	int err;
	static struct snd_device_ops ops = {
		.dev_free = snd_pcm_dev_free,
		.dev_register =	snd_pcm_dev_register,
		.dev_disconnect = snd_pcm_dev_disconnect,
	};
	static struct snd_device_ops internal_ops = {
		.dev_free = snd_pcm_dev_free,
	};

	if (snd_BUG_ON(!card))
		return -ENXIO;
	if (rpcm)
		*rpcm = NULL;
	pcm = kzalloc(sizeof(*pcm), GFP_KERNEL);
	if (!pcm)
		return -ENOMEM;
	pcm->card = card;
	pcm->device = device;
	pcm->internal = internal;
	mutex_init(&pcm->open_mutex);
	init_waitqueue_head(&pcm->open_wait);
	INIT_LIST_HEAD(&pcm->list);
	if (id)
		strlcpy(pcm->id, id, sizeof(pcm->id));

	err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK,
				 playback_count);
	if (err < 0)
		goto free_pcm;

	err = snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count);
	if (err < 0)
		goto free_pcm;

	err = snd_device_new(card, SNDRV_DEV_PCM, pcm,
			     internal ? &internal_ops : &ops);
	if (err < 0)
		goto free_pcm;

	if (rpcm)
		*rpcm = pcm;
	return 0;

free_pcm:
	snd_pcm_free(pcm);
	return err;
}

Project: linux
File: pcm.c

int snd_pcm_attach_substream(struct snd_pcm *pcm, int stream,
			     struct file *file,
			     struct snd_pcm_substream **rsubstream)
{
	struct snd_pcm_str * pstr;
	struct snd_pcm_substream *substream;
	struct snd_pcm_runtime *runtime;
	struct snd_card *card;
	int prefer_subdevice;
	size_t size;

	if (snd_BUG_ON(!pcm || !rsubstream))
		return -ENXIO;
	if (snd_BUG_ON(stream != SNDRV_PCM_STREAM_PLAYBACK &&
		       stream != SNDRV_PCM_STREAM_CAPTURE))
		return -EINVAL;
	*rsubstream = NULL;
	pstr = &pcm->streams[stream];
	if (pstr->substream == NULL || pstr->substream_count == 0)
		return -ENODEV;

	card = pcm->card;
	prefer_subdevice = snd_ctl_get_preferred_subdevice(card, SND_CTL_SUBDEV_PCM);

	if (pcm->info_flags & SNDRV_PCM_INFO_HALF_DUPLEX) {
		int opposite = !stream;

		for (substream = pcm->streams[opposite].substream; substream;
		     substream = substream->next) {
			if (SUBSTREAM_BUSY(substream))
				return -EAGAIN;
		}
	}

	if (file->f_flags & O_APPEND) {
		if (prefer_subdevice < 0) {
			if (pstr->substream_count > 1)
				return -EINVAL; /* must be unique */
			substream = pstr->substream;
		} else {
			for (substream = pstr->substream; substream;
			     substream = substream->next)
				if (substream->number == prefer_subdevice)
					break;
		}
		if (! substream)
			return -ENODEV;
		if (! SUBSTREAM_BUSY(substream))
			return -EBADFD;
		substream->ref_count++;
		*rsubstream = substream;
		return 0;
	}

	for (substream = pstr->substream; substream; substream = substream->next) {
		if (!SUBSTREAM_BUSY(substream) &&
		    (prefer_subdevice == -1 ||
		     substream->number == prefer_subdevice))
			break;
	}
	if (substream == NULL)
		return -EAGAIN;

	runtime = kzalloc(sizeof(*runtime), GFP_KERNEL);
	if (runtime == NULL)
		return -ENOMEM;

	size = PAGE_ALIGN(sizeof(struct snd_pcm_mmap_status));
	runtime->status = snd_malloc_pages(size, GFP_KERNEL);
	if (runtime->status == NULL) {
		kfree(runtime);
		return -ENOMEM;
	}
	memset((void*)runtime->status, 0, size);

	size = PAGE_ALIGN(sizeof(struct snd_pcm_mmap_control));
	runtime->control = snd_malloc_pages(size, GFP_KERNEL);
	if (runtime->control == NULL) {
		snd_free_pages((void*)runtime->status,
			       PAGE_ALIGN(sizeof(struct snd_pcm_mmap_status)));
		kfree(runtime);
		return -ENOMEM;
	}
	memset((void*)runtime->control, 0, size);

	init_waitqueue_head(&runtime->sleep);
	init_waitqueue_head(&runtime->tsleep);

	runtime->status->state = SNDRV_PCM_STATE_OPEN;

	substream->runtime = runtime;
	substream->private_data = pcm->private_data;
	substream->ref_count = 1;
	substream->f_flags = file->f_flags;
	substream->pid = get_pid(task_pid(current));
	pstr->substream_opened++;
	*rsubstream = substream;
	return 0;
}

Project: linux
File: pcm.c

static int snd_pcm_stream_proc_init(struct snd_pcm_str *pstr)
{
	struct snd_pcm *pcm = pstr->pcm;
	struct snd_info_entry *entry;
	char name[16];

	sprintf(name, "pcm%i%c", pcm->device, 
		pstr->stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c');
	entry = snd_info_create_card_entry(pcm->card, name,
					   pcm->card->proc_root);
	if (!entry)
		return -ENOMEM;
	entry->mode = S_IFDIR | S_IRUGO | S_IXUGO;
	if (snd_info_register(entry) < 0) {
		snd_info_free_entry(entry);
		return -ENOMEM;
	}
	pstr->proc_root = entry;
	entry = snd_info_create_card_entry(pcm->card, "info", pstr->proc_root);
	if (entry) {
		snd_info_set_text_ops(entry, pstr, snd_pcm_stream_proc_info_read);
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	pstr->proc_info_entry = entry;

#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	entry = snd_info_create_card_entry(pcm->card, "xrun_debug",
					   pstr->proc_root);
	if (entry) {
		entry->c.text.read = snd_pcm_xrun_debug_read;
		entry->c.text.write = snd_pcm_xrun_debug_write;
		entry->mode |= S_IWUSR;
		entry->private_data = pstr;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	pstr->proc_xrun_debug_entry = entry;
#endif
	return 0;
}

Project: linux
File: pcm.c

static void snd_pcm_substream_proc_status_read(struct snd_info_entry *entry,
					       struct snd_info_buffer *buffer)
{
	struct snd_pcm_substream *substream = entry->private_data;
	struct snd_pcm_runtime *runtime;
	struct snd_pcm_status status;
	int err;

	mutex_lock(&substream->pcm->open_mutex);
	runtime = substream->runtime;
	if (!runtime) {
		snd_iprintf(buffer, "closed\n");
		goto unlock;
	}
	memset(&status, 0, sizeof(status));
	err = snd_pcm_status(substream, &status);
	if (err < 0) {
		snd_iprintf(buffer, "error %d\n", err);
		goto unlock;
	}
	snd_iprintf(buffer, "state: %s\n", snd_pcm_state_name(status.state));
	snd_iprintf(buffer, "owner_pid   : %d\n", pid_vnr(substream->pid));
	snd_iprintf(buffer, "trigger_time: %ld.%09ld\n",
		status.trigger_tstamp.tv_sec, status.trigger_tstamp.tv_nsec);
	snd_iprintf(buffer, "tstamp      : %ld.%09ld\n",
		status.tstamp.tv_sec, status.tstamp.tv_nsec);
	snd_iprintf(buffer, "delay       : %ld\n", status.delay);
	snd_iprintf(buffer, "avail       : %ld\n", status.avail);
	snd_iprintf(buffer, "avail_max   : %ld\n", status.avail_max);
	snd_iprintf(buffer, "-----\n");
	snd_iprintf(buffer, "hw_ptr      : %ld\n", runtime->status->hw_ptr);
	snd_iprintf(buffer, "appl_ptr    : %ld\n", runtime->control->appl_ptr);
 unlock:
	mutex_unlock(&substream->pcm->open_mutex);
}

Project: linux
File: pcm.c

static int snd_pcm_substream_proc_init(struct snd_pcm_substream *substream)
{
	struct snd_info_entry *entry;
	struct snd_card *card;
	char name[16];

	card = substream->pcm->card;

	sprintf(name, "sub%i", substream->number);
	entry = snd_info_create_card_entry(card, name,
					   substream->pstr->proc_root);
	if (!entry)
		return -ENOMEM;
	entry->mode = S_IFDIR | S_IRUGO | S_IXUGO;
	if (snd_info_register(entry) < 0) {
		snd_info_free_entry(entry);
		return -ENOMEM;
	}
	substream->proc_root = entry;
	entry = snd_info_create_card_entry(card, "info", substream->proc_root);
	if (entry) {
		snd_info_set_text_ops(entry, substream,
				      snd_pcm_substream_proc_info_read);
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_info_entry = entry;
	entry = snd_info_create_card_entry(card, "hw_params",
					   substream->proc_root);
	if (entry) {
		snd_info_set_text_ops(entry, substream,
				      snd_pcm_substream_proc_hw_params_read);
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_hw_params_entry = entry;
	entry = snd_info_create_card_entry(card, "sw_params",
					   substream->proc_root);
	if (entry) {
		snd_info_set_text_ops(entry, substream,
				      snd_pcm_substream_proc_sw_params_read);
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_sw_params_entry = entry;
	entry = snd_info_create_card_entry(card, "status",
					   substream->proc_root);
	if (entry) {
		snd_info_set_text_ops(entry, substream,
				      snd_pcm_substream_proc_status_read);
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_status_entry = entry;

#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	entry = snd_info_create_card_entry(card, "xrun_injection",
					   substream->proc_root);
	if (entry) {
		entry->private_data = substream;
		entry->c.text.read = NULL;
		entry->c.text.write = snd_pcm_xrun_injection_write;
		entry->mode = S_IFREG | S_IWUSR;
		if (snd_info_register(entry) < 0) {
			snd_info_free_entry(entry);
			entry = NULL;
		}
	}
	substream->proc_xrun_injection_entry = entry;
#endif /* CONFIG_SND_PCM_XRUN_DEBUG */

	return 0;
}

Project: linux
File: pcm.c

static ssize_t show_pcm_class(struct device *dev,
			      struct device_attribute *attr, char *buf)
{
	struct snd_pcm_str *pstr = container_of(dev, struct snd_pcm_str, dev);
	struct snd_pcm *pcm = pstr->pcm;
	const char *str;
	static const char *strs[SNDRV_PCM_CLASS_LAST + 1] = {
		[SNDRV_PCM_CLASS_GENERIC] = "generic",
		[SNDRV_PCM_CLASS_MULTI] = "multi",
		[SNDRV_PCM_CLASS_MODEM] = "modem",
		[SNDRV_PCM_CLASS_DIGITIZER] = "digitizer",
	};

	if (pcm->dev_class > SNDRV_PCM_CLASS_LAST)
		str = "none";
	else
		str = strs[pcm->dev_class];
        return snprintf(buf, PAGE_SIZE, "%s\n", str);
}

Project: linux
File: pcm.c

static int snd_pcm_substream_proc_done(struct snd_pcm_substream *substream)
{
	snd_info_free_entry(substream->proc_info_entry);
	substream->proc_info_entry = NULL;
	snd_info_free_entry(substream->proc_hw_params_entry);
	substream->proc_hw_params_entry = NULL;
	snd_info_free_entry(substream->proc_sw_params_entry);
	substream->proc_sw_params_entry = NULL;
	snd_info_free_entry(substream->proc_status_entry);
	substream->proc_status_entry = NULL;
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	snd_info_free_entry(substream->proc_xrun_injection_entry);
	substream->proc_xrun_injection_entry = NULL;
#endif
	snd_info_free_entry(substream->proc_root);
	substream->proc_root = NULL;
	return 0;
}

Project: linux
File: pcm.c

static int snd_pcm_control_ioctl(struct snd_card *card,
				 struct snd_ctl_file *control,
				 unsigned int cmd, unsigned long arg)
{
	switch (cmd) {
	case SNDRV_CTL_IOCTL_PCM_NEXT_DEVICE:
		{
			int device;

			if (get_user(device, (int __user *)arg))
				return -EFAULT;
			mutex_lock(&register_mutex);
			device = snd_pcm_next(card, device);
			mutex_unlock(&register_mutex);
			if (put_user(device, (int __user *)arg))
				return -EFAULT;
			return 0;
		}
	case SNDRV_CTL_IOCTL_PCM_INFO:
		{
			struct snd_pcm_info __user *info;
			unsigned int device, subdevice;
			int stream;
			struct snd_pcm *pcm;
			struct snd_pcm_str *pstr;
			struct snd_pcm_substream *substream;
			int err;

			info = (struct snd_pcm_info __user *)arg;
			if (get_user(device, &info->device))
				return -EFAULT;
			if (get_user(stream, &info->stream))
				return -EFAULT;
			if (stream < 0 || stream > 1)
				return -EINVAL;
			if (get_user(subdevice, &info->subdevice))
				return -EFAULT;
			mutex_lock(&register_mutex);
			pcm = snd_pcm_get(card, device);
			if (pcm == NULL) {
				err = -ENXIO;
				goto _error;
			}
			pstr = &pcm->streams[stream];
			if (pstr->substream_count == 0) {
				err = -ENOENT;
				goto _error;
			}
			if (subdevice >= pstr->substream_count) {
				err = -ENXIO;
				goto _error;
			}
			for (substream = pstr->substream; substream;
			     substream = substream->next)
				if (substream->number == (int)subdevice)
					break;
			if (substream == NULL) {
				err = -ENXIO;
				goto _error;
			}
			mutex_lock(&pcm->open_mutex);
			err = snd_pcm_info_user(substream, info);
			mutex_unlock(&pcm->open_mutex);
		_error:
			mutex_unlock(&register_mutex);
			return err;
		}
	case SNDRV_CTL_IOCTL_PCM_PREFER_SUBDEVICE:
		{
			int val;
			
			if (get_user(val, (int __user *)arg))
				return -EFAULT;
			control->preferred_subdevice[SND_CTL_SUBDEV_PCM] = val;
			return 0;
		}
	}
	return -ENOIOCTLCMD;
}

Project: linux
File: pcm.c

int snd_pcm_new_stream(struct snd_pcm *pcm, int stream, int substream_count)
{
	int idx, err;
	struct snd_pcm_str *pstr = &pcm->streams[stream];
	struct snd_pcm_substream *substream, *prev;

#if IS_ENABLED(CONFIG_SND_PCM_OSS)
	mutex_init(&pstr->oss.setup_mutex);
#endif
	pstr->stream = stream;
	pstr->pcm = pcm;
	pstr->substream_count = substream_count;
	if (!substream_count)
		return 0;

	snd_device_initialize(&pstr->dev, pcm->card);
	pstr->dev.groups = pcm_dev_attr_groups;
	dev_set_name(&pstr->dev, "pcmC%iD%i%c", pcm->card->number, pcm->device,
		     stream == SNDRV_PCM_STREAM_PLAYBACK ? 'p' : 'c');

	if (!pcm->internal) {
		err = snd_pcm_stream_proc_init(pstr);
		if (err < 0) {
			pcm_err(pcm, "Error in snd_pcm_stream_proc_init\n");
			return err;
		}
	}
	prev = NULL;
	for (idx = 0, prev = NULL; idx < substream_count; idx++) {
		substream = kzalloc(sizeof(*substream), GFP_KERNEL);
		if (!substream)
			return -ENOMEM;
		substream->pcm = pcm;
		substream->pstr = pstr;
		substream->number = idx;
		substream->stream = stream;
		sprintf(substream->name, "subdevice #%i", idx);
		substream->buffer_bytes_max = UINT_MAX;
		if (prev == NULL)
			pstr->substream = substream;
		else
			prev->next = substream;

		if (!pcm->internal) {
			err = snd_pcm_substream_proc_init(substream);
			if (err < 0) {
				pcm_err(pcm,
					"Error in snd_pcm_stream_proc_init\n");
				if (prev == NULL)
					pstr->substream = NULL;
				else
					prev->next = NULL;
				kfree(substream);
				return err;
			}
		}
		substream->group = &substream->self_group;
		spin_lock_init(&substream->self_group.lock);
		mutex_init(&substream->self_group.mutex);
		INIT_LIST_HEAD(&substream->self_group.substreams);
		list_add_tail(&substream->link_list, &substream->self_group.substreams);
		atomic_set(&substream->mmap_count, 0);
		prev = substream;
	}
	return 0;
}

Project: linux
File: pcm.c

static int snd_pcm_dev_disconnect(struct snd_device *device)
{
	struct snd_pcm *pcm = device->device_data;
	struct snd_pcm_substream *substream;
	int cidx;

	mutex_lock(&register_mutex);
	mutex_lock(&pcm->open_mutex);
	wake_up(&pcm->open_wait);
	list_del_init(&pcm->list);
	for (cidx = 0; cidx < 2; cidx++) {
		for (substream = pcm->streams[cidx].substream; substream; substream = substream->next) {
			snd_pcm_stream_lock_irq(substream);
			if (substream->runtime) {
				if (snd_pcm_running(substream))
					snd_pcm_stop(substream,
						     SNDRV_PCM_STATE_DISCONNECTED);
				/* to be sure, set the state unconditionally */
				substream->runtime->status->state = SNDRV_PCM_STATE_DISCONNECTED;
				wake_up(&substream->runtime->sleep);
				wake_up(&substream->runtime->tsleep);
			}
			snd_pcm_stream_unlock_irq(substream);
		}
	}

	pcm_call_notify(pcm, n_disconnect);
	for (cidx = 0; cidx < 2; cidx++) {
		snd_unregister_device(&pcm->streams[cidx].dev);
		free_chmap(&pcm->streams[cidx]);
	}
	mutex_unlock(&pcm->open_mutex);
	mutex_unlock(&register_mutex);
	return 0;
}

Project: linux
File: pcm.c

static inline int snd_pcm_substream_proc_done(struct snd_pcm_substream *substream) { return 0; }

Project: linux
File: pcm.c

static void snd_pcm_xrun_injection_write(struct snd_info_entry *entry,
					 struct snd_info_buffer *buffer)
{
	struct snd_pcm_substream *substream = entry->private_data;
	struct snd_pcm_runtime *runtime;

	snd_pcm_stream_lock_irq(substream);
	runtime = substream->runtime;
	if (runtime && runtime->status->state == SNDRV_PCM_STATE_RUNNING)
		snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN);
	snd_pcm_stream_unlock_irq(substream);
}

Project: linux
File: pcm.c

static void snd_pcm_free_stream(struct snd_pcm_str * pstr)
{
	struct snd_pcm_substream *substream, *substream_next;
#if IS_ENABLED(CONFIG_SND_PCM_OSS)
	struct snd_pcm_oss_setup *setup, *setupn;
#endif
	substream = pstr->substream;
	while (substream) {
		substream_next = substream->next;
		snd_pcm_timer_done(substream);
		snd_pcm_substream_proc_done(substream);
		kfree(substream);
		substream = substream_next;
	}
	snd_pcm_stream_proc_done(pstr);
#if IS_ENABLED(CONFIG_SND_PCM_OSS)
	for (setup = pstr->oss.setup_list; setup; setup = setupn) {
		setupn = setup->next;
		kfree(setup->task_name);
		kfree(setup);
	}
#endif
	free_chmap(pstr);
	if (pstr->substream_count)
		put_device(&pstr->dev);
}

Project: linux
File: pcm.c

static int snd_pcm_stream_proc_done(struct snd_pcm_str *pstr)
{
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	snd_info_free_entry(pstr->proc_xrun_debug_entry);
	pstr->proc_xrun_debug_entry = NULL;
#endif
	snd_info_free_entry(pstr->proc_info_entry);
	pstr->proc_info_entry = NULL;
	snd_info_free_entry(pstr->proc_root);
	pstr->proc_root = NULL;
	return 0;
}