Commit message: HID: hiddev: validate num_values for HIDIOCGUSAGES, HIDIOCSUSAGES commands

This patch validates the num_values parameter from userland during the
HIDIOCGUSAGES and HIDIOCSUSAGES commands. Previously, if the report id was set
to HID_REPORT_ID_UNKNOWN, we would fail to validate the num_values parameter
leading to a heap overflow.

Cc: stable@vger.kernel.org
Signed-off-by: Scott Bauer <sbauer@plzdonthack.me>
Signed-off-by: Jiri Kosina <jkosina@suse.cz>
CVE: CVE-2016-5829
CWE: CWE-119
Vulnerability description: Multiple heap-based buffer overflows in the hiddev_ioctl_usage function in drivers/hid/usbhid/hiddev.c in the Linux kernel through 4.6.3 allow local users to cause a denial of service or possibly have unspecified other impact via a crafted (1) HIDIOCGUSAGES or (2) HIDIOCSUSAGES ioctl call.

Project: linux
File: hiddev.c

static long hiddev_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	return hiddev_ioctl(file, cmd, (unsigned long)compat_ptr(arg));
}

Project: linux
File: hiddev.c

static char *hiddev_devnode(struct device *dev, umode_t *mode)
{
	return kasprintf(GFP_KERNEL, "usb/%s", dev_name(dev));
}

Project: linux
File: hiddev.c

void hiddev_disconnect(struct hid_device *hid)
{
	struct hiddev *hiddev = hid->hiddev;
	struct usbhid_device *usbhid = hid->driver_data;

	usb_deregister_dev(usbhid->intf, &hiddev_class);

	mutex_lock(&hiddev->existancelock);
	hiddev->exist = 0;

	if (hiddev->open) {
		mutex_unlock(&hiddev->existancelock);
		usbhid_close(hiddev->hid);
		wake_up_interruptible(&hiddev->wait);
	} else {
		mutex_unlock(&hiddev->existancelock);
		kfree(hiddev);
	}
}

Project: linux
File: hiddev.c

static int hiddev_fasync(int fd, struct file *file, int on)
{
	struct hiddev_list *list = file->private_data;

	return fasync_helper(fd, file, on, &list->fasync);
}

Project: linux
File: hiddev.c

void hiddev_hid_event(struct hid_device *hid, struct hid_field *field,
		      struct hid_usage *usage, __s32 value)
{
	unsigned type = field->report_type;
	struct hiddev_usage_ref uref;

	uref.report_type =
	  (type == HID_INPUT_REPORT) ? HID_REPORT_TYPE_INPUT :
	  ((type == HID_OUTPUT_REPORT) ? HID_REPORT_TYPE_OUTPUT :
	   ((type == HID_FEATURE_REPORT) ? HID_REPORT_TYPE_FEATURE : 0));
	uref.report_id = field->report->id;
	uref.field_index = field->index;
	uref.usage_index = (usage - field->usage);
	uref.usage_code = usage->hid;
	uref.value = value;

	hiddev_send_event(hid, &uref);
}

Project: linux
File: hiddev.c

static long hiddev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct hiddev_list *list = file->private_data;
	struct hiddev *hiddev = list->hiddev;
	struct hid_device *hid;
	struct hiddev_collection_info cinfo;
	struct hiddev_report_info rinfo;
	struct hiddev_field_info finfo;
	struct hiddev_devinfo dinfo;
	struct hid_report *report;
	struct hid_field *field;
	void __user *user_arg = (void __user *)arg;
	int i, r = -EINVAL;

	/* Called without BKL by compat methods so no BKL taken */

	mutex_lock(&hiddev->existancelock);
	if (!hiddev->exist) {
		r = -ENODEV;
		goto ret_unlock;
	}

	hid = hiddev->hid;

	switch (cmd) {

	case HIDIOCGVERSION:
		r = put_user(HID_VERSION, (int __user *)arg) ?
			-EFAULT : 0;
		break;

	case HIDIOCAPPLICATION:
		if (arg >= hid->maxapplication)
			break;

		for (i = 0; i < hid->maxcollection; i++)
			if (hid->collection[i].type ==
			    HID_COLLECTION_APPLICATION && arg-- == 0)
				break;

		if (i < hid->maxcollection)
			r = hid->collection[i].usage;
		break;

	case HIDIOCGDEVINFO:
		{
			struct usb_device *dev = hid_to_usb_dev(hid);
			struct usbhid_device *usbhid = hid->driver_data;

			memset(&dinfo, 0, sizeof(dinfo));

			dinfo.bustype = BUS_USB;
			dinfo.busnum = dev->bus->busnum;
			dinfo.devnum = dev->devnum;
			dinfo.ifnum = usbhid->ifnum;
			dinfo.vendor = le16_to_cpu(dev->descriptor.idVendor);
			dinfo.product = le16_to_cpu(dev->descriptor.idProduct);
			dinfo.version = le16_to_cpu(dev->descriptor.bcdDevice);
			dinfo.num_applications = hid->maxapplication;

			r = copy_to_user(user_arg, &dinfo, sizeof(dinfo)) ?
				-EFAULT : 0;
			break;
		}

	case HIDIOCGFLAG:
		r = put_user(list->flags, (int __user *)arg) ?
			-EFAULT : 0;
		break;

	case HIDIOCSFLAG:
		{
			int newflags;

			if (get_user(newflags, (int __user *)arg)) {
				r = -EFAULT;
				break;
			}

			if ((newflags & ~HIDDEV_FLAGS) != 0 ||
			    ((newflags & HIDDEV_FLAG_REPORT) != 0 &&
			     (newflags & HIDDEV_FLAG_UREF) == 0))
				break;

			list->flags = newflags;

			r = 0;
			break;
		}

	case HIDIOCGSTRING:
		r = hiddev_ioctl_string(hiddev, cmd, user_arg);
		break;

	case HIDIOCINITREPORT:
		usbhid_init_reports(hid);
		r = 0;
		break;

	case HIDIOCGREPORT:
		if (copy_from_user(&rinfo, user_arg, sizeof(rinfo))) {
			r = -EFAULT;
			break;
		}

		if (rinfo.report_type == HID_REPORT_TYPE_OUTPUT)
			break;

		report = hiddev_lookup_report(hid, &rinfo);
		if (report == NULL)
			break;

		hid_hw_request(hid, report, HID_REQ_GET_REPORT);
		hid_hw_wait(hid);

		r = 0;
		break;

	case HIDIOCSREPORT:
		if (copy_from_user(&rinfo, user_arg, sizeof(rinfo))) {
			r = -EFAULT;
			break;
		}

		if (rinfo.report_type == HID_REPORT_TYPE_INPUT)
			break;

		report = hiddev_lookup_report(hid, &rinfo);
		if (report == NULL)
			break;

		hid_hw_request(hid, report, HID_REQ_SET_REPORT);
		hid_hw_wait(hid);

		r = 0;
		break;

	case HIDIOCGREPORTINFO:
		if (copy_from_user(&rinfo, user_arg, sizeof(rinfo))) {
			r = -EFAULT;
			break;
		}

		report = hiddev_lookup_report(hid, &rinfo);
		if (report == NULL)
			break;

		rinfo.num_fields = report->maxfield;

		r = copy_to_user(user_arg, &rinfo, sizeof(rinfo)) ?
			-EFAULT : 0;
		break;

	case HIDIOCGFIELDINFO:
		if (copy_from_user(&finfo, user_arg, sizeof(finfo))) {
			r = -EFAULT;
			break;
		}

		rinfo.report_type = finfo.report_type;
		rinfo.report_id = finfo.report_id;

		report = hiddev_lookup_report(hid, &rinfo);
		if (report == NULL)
			break;

		if (finfo.field_index >= report->maxfield)
			break;

		field = report->field[finfo.field_index];
		memset(&finfo, 0, sizeof(finfo));
		finfo.report_type = rinfo.report_type;
		finfo.report_id = rinfo.report_id;
		finfo.field_index = field->report_count - 1;
		finfo.maxusage = field->maxusage;
		finfo.flags = field->flags;
		finfo.physical = field->physical;
		finfo.logical = field->logical;
		finfo.application = field->application;
		finfo.logical_minimum = field->logical_minimum;
		finfo.logical_maximum = field->logical_maximum;
		finfo.physical_minimum = field->physical_minimum;
		finfo.physical_maximum = field->physical_maximum;
		finfo.unit_exponent = field->unit_exponent;
		finfo.unit = field->unit;

		r = copy_to_user(user_arg, &finfo, sizeof(finfo)) ?
			-EFAULT : 0;
		break;

	case HIDIOCGUCODE:
		/* fall through */
	case HIDIOCGUSAGE:
	case HIDIOCSUSAGE:
	case HIDIOCGUSAGES:
	case HIDIOCSUSAGES:
	case HIDIOCGCOLLECTIONINDEX:
		r = hiddev_ioctl_usage(hiddev, cmd, user_arg);
		break;

	case HIDIOCGCOLLECTIONINFO:
		if (copy_from_user(&cinfo, user_arg, sizeof(cinfo))) {
			r = -EFAULT;
			break;
		}

		if (cinfo.index >= hid->maxcollection)
			break;

		cinfo.type = hid->collection[cinfo.index].type;
		cinfo.usage = hid->collection[cinfo.index].usage;
		cinfo.level = hid->collection[cinfo.index].level;

		r = copy_to_user(user_arg, &cinfo, sizeof(cinfo)) ?
			-EFAULT : 0;
		break;

	default:
		if (_IOC_TYPE(cmd) != 'H' || _IOC_DIR(cmd) != _IOC_READ)
			break;

		if (_IOC_NR(cmd) == _IOC_NR(HIDIOCGNAME(0))) {
			int len = strlen(hid->name) + 1;
			if (len > _IOC_SIZE(cmd))
				 len = _IOC_SIZE(cmd);
			r = copy_to_user(user_arg, hid->name, len) ?
				-EFAULT : len;
			break;
		}

		if (_IOC_NR(cmd) == _IOC_NR(HIDIOCGPHYS(0))) {
			int len = strlen(hid->phys) + 1;
			if (len > _IOC_SIZE(cmd))
				len = _IOC_SIZE(cmd);
			r = copy_to_user(user_arg, hid->phys, len) ?
				-EFAULT : len;
			break;
		}
	}

ret_unlock:
	mutex_unlock(&hiddev->existancelock);
	return r;
}

Project: linux
File: hiddev.c

static noinline int hiddev_ioctl_string(struct hiddev *hiddev, unsigned int cmd, void __user *user_arg)
{
	struct hid_device *hid = hiddev->hid;
	struct usb_device *dev = hid_to_usb_dev(hid);
	int idx, len;
	char *buf;

	if (get_user(idx, (int __user *)user_arg))
		return -EFAULT;

	if ((buf = kmalloc(HID_STRING_SIZE, GFP_KERNEL)) == NULL)
		return -ENOMEM;

	if ((len = usb_string(dev, idx, buf, HID_STRING_SIZE-1)) < 0) {
		kfree(buf);
		return -EINVAL;
	}

	if (copy_to_user(user_arg+sizeof(int), buf, len+1)) {
		kfree(buf);
		return -EFAULT;
	}

	kfree(buf);

	return len;
}

Project: linux
File: hiddev.c

hiddev_lookup_report(struct hid_device *hid, struct hiddev_report_info *rinfo)
{
	unsigned int flags = rinfo->report_id & ~HID_REPORT_ID_MASK;
	unsigned int rid = rinfo->report_id & HID_REPORT_ID_MASK;
	struct hid_report_enum *report_enum;
	struct hid_report *report;
	struct list_head *list;

	if (rinfo->report_type < HID_REPORT_TYPE_MIN ||
	    rinfo->report_type > HID_REPORT_TYPE_MAX)
		return NULL;

	report_enum = hid->report_enum +
		(rinfo->report_type - HID_REPORT_TYPE_MIN);

	switch (flags) {
	case 0: /* Nothing to do -- report_id is already set correctly */
		break;

	case HID_REPORT_ID_FIRST:
		if (list_empty(&report_enum->report_list))
			return NULL;

		list = report_enum->report_list.next;
		report = list_entry(list, struct hid_report, list);
		rinfo->report_id = report->id;
		break;

	case HID_REPORT_ID_NEXT:
		report = report_enum->report_id_hash[rid];
		if (!report)
			return NULL;

		list = report->list.next;
		if (list == &report_enum->report_list)
			return NULL;

		report = list_entry(list, struct hid_report, list);
		rinfo->report_id = report->id;
		break;

	default:
		return NULL;
	}

	return report_enum->report_id_hash[rinfo->report_id];
}

Project: linux
File: hiddev.c

hiddev_lookup_usage(struct hid_device *hid, struct hiddev_usage_ref *uref)
{
	int i, j;
	struct hid_report *report;
	struct hid_report_enum *report_enum;
	struct hid_field *field;

	if (uref->report_type < HID_REPORT_TYPE_MIN ||
	    uref->report_type > HID_REPORT_TYPE_MAX)
		return NULL;

	report_enum = hid->report_enum +
		(uref->report_type - HID_REPORT_TYPE_MIN);

	list_for_each_entry(report, &report_enum->report_list, list) {
		for (i = 0; i < report->maxfield; i++) {
			field = report->field[i];
			for (j = 0; j < field->maxusage; j++) {
				if (field->usage[j].hid == uref->usage_code) {
					uref->report_id = report->id;
					uref->field_index = i;
					uref->usage_index = j;
					return field;
				}
			}
		}
	}

	return NULL;
}

Project: linux
File: hiddev.c

static int hiddev_open(struct inode *inode, struct file *file)
{
	struct hiddev_list *list;
	struct usb_interface *intf;
	struct hid_device *hid;
	struct hiddev *hiddev;
	int res;

	intf = usbhid_find_interface(iminor(inode));
	if (!intf)
		return -ENODEV;
	hid = usb_get_intfdata(intf);
	hiddev = hid->hiddev;

	if (!(list = vzalloc(sizeof(struct hiddev_list))))
		return -ENOMEM;
	mutex_init(&list->thread_lock);
	list->hiddev = hiddev;
	file->private_data = list;

	/*
	 * no need for locking because the USB major number
	 * is shared which usbcore guards against disconnect
	 */
	if (list->hiddev->exist) {
		if (!list->hiddev->open++) {
			res = usbhid_open(hiddev->hid);
			if (res < 0) {
				res = -EIO;
				goto bail;
			}
		}
	} else {
		res = -ENODEV;
		goto bail;
	}

	spin_lock_irq(&list->hiddev->list_lock);
	list_add_tail(&list->node, &hiddev->list);
	spin_unlock_irq(&list->hiddev->list_lock);

	mutex_lock(&hiddev->existancelock);
	if (!list->hiddev->open++)
		if (list->hiddev->exist) {
			struct hid_device *hid = hiddev->hid;
			res = usbhid_get_power(hid);
			if (res < 0) {
				res = -EIO;
				goto bail_unlock;
			}
			usbhid_open(hid);
		}
	mutex_unlock(&hiddev->existancelock);
	return 0;
bail_unlock:
	mutex_unlock(&hiddev->existancelock);
bail:
	file->private_data = NULL;
	vfree(list);
	return res;
}

Project: linux
File: hiddev.c

static unsigned int hiddev_poll(struct file *file, poll_table *wait)
{
	struct hiddev_list *list = file->private_data;

	poll_wait(file, &list->hiddev->wait, wait);
	if (list->head != list->tail)
		return POLLIN | POLLRDNORM;
	if (!list->hiddev->exist)
		return POLLERR | POLLHUP;
	return 0;
}

Project: linux
File: hiddev.c

static ssize_t hiddev_read(struct file * file, char __user * buffer, size_t count, loff_t *ppos)
{
	DEFINE_WAIT(wait);
	struct hiddev_list *list = file->private_data;
	int event_size;
	int retval;

	event_size = ((list->flags & HIDDEV_FLAG_UREF) != 0) ?
		sizeof(struct hiddev_usage_ref) : sizeof(struct hiddev_event);

	if (count < event_size)
		return 0;

	/* lock against other threads */
	retval = mutex_lock_interruptible(&list->thread_lock);
	if (retval)
		return -ERESTARTSYS;

	while (retval == 0) {
		if (list->head == list->tail) {
			prepare_to_wait(&list->hiddev->wait, &wait, TASK_INTERRUPTIBLE);

			while (list->head == list->tail) {
				if (signal_pending(current)) {
					retval = -ERESTARTSYS;
					break;
				}
				if (!list->hiddev->exist) {
					retval = -EIO;
					break;
				}
				if (file->f_flags & O_NONBLOCK) {
					retval = -EAGAIN;
					break;
				}

				/* let O_NONBLOCK tasks run */
				mutex_unlock(&list->thread_lock);
				schedule();
				if (mutex_lock_interruptible(&list->thread_lock)) {
					finish_wait(&list->hiddev->wait, &wait);
					return -EINTR;
				}
				set_current_state(TASK_INTERRUPTIBLE);
			}
			finish_wait(&list->hiddev->wait, &wait);

		}

		if (retval) {
			mutex_unlock(&list->thread_lock);
			return retval;
		}


		while (list->head != list->tail &&
		       retval + event_size <= count) {
			if ((list->flags & HIDDEV_FLAG_UREF) == 0) {
				if (list->buffer[list->tail].field_index != HID_FIELD_INDEX_NONE) {
					struct hiddev_event event;

					event.hid = list->buffer[list->tail].usage_code;
					event.value = list->buffer[list->tail].value;
					if (copy_to_user(buffer + retval, &event, sizeof(struct hiddev_event))) {
						mutex_unlock(&list->thread_lock);
						return -EFAULT;
					}
					retval += sizeof(struct hiddev_event);
				}
			} else {
				if (list->buffer[list->tail].field_index != HID_FIELD_INDEX_NONE ||
				    (list->flags & HIDDEV_FLAG_REPORT) != 0) {

					if (copy_to_user(buffer + retval, list->buffer + list->tail, sizeof(struct hiddev_usage_ref))) {
						mutex_unlock(&list->thread_lock);
						return -EFAULT;
					}
					retval += sizeof(struct hiddev_usage_ref);
				}
			}
			list->tail = (list->tail + 1) & (HIDDEV_BUFFER_SIZE - 1);
		}

	}
	mutex_unlock(&list->thread_lock);

	return retval;
}

Project: linux
File: hiddev.c

static int hiddev_release(struct inode * inode, struct file * file)
{
	struct hiddev_list *list = file->private_data;
	unsigned long flags;

	spin_lock_irqsave(&list->hiddev->list_lock, flags);
	list_del(&list->node);
	spin_unlock_irqrestore(&list->hiddev->list_lock, flags);

	mutex_lock(&list->hiddev->existancelock);
	if (!--list->hiddev->open) {
		if (list->hiddev->exist) {
			usbhid_close(list->hiddev->hid);
			usbhid_put_power(list->hiddev->hid);
		} else {
			mutex_unlock(&list->hiddev->existancelock);
			kfree(list->hiddev);
			vfree(list);
			return 0;
		}
	}

	mutex_unlock(&list->hiddev->existancelock);
	vfree(list);

	return 0;
}

Project: linux
File: hiddev.c

void hiddev_report_event(struct hid_device *hid, struct hid_report *report)
{
	unsigned type = report->type;
	struct hiddev_usage_ref uref;

	memset(&uref, 0, sizeof(uref));
	uref.report_type =
	  (type == HID_INPUT_REPORT) ? HID_REPORT_TYPE_INPUT :
	  ((type == HID_OUTPUT_REPORT) ? HID_REPORT_TYPE_OUTPUT :
	   ((type == HID_FEATURE_REPORT) ? HID_REPORT_TYPE_FEATURE : 0));
	uref.report_id = report->id;
	uref.field_index = HID_FIELD_INDEX_NONE;

	hiddev_send_event(hid, &uref);
}

Project: linux
File: hiddev.c

static void hiddev_send_event(struct hid_device *hid,
			      struct hiddev_usage_ref *uref)
{
	struct hiddev *hiddev = hid->hiddev;
	struct hiddev_list *list;
	unsigned long flags;

	spin_lock_irqsave(&hiddev->list_lock, flags);
	list_for_each_entry(list, &hiddev->list, node) {
		if (uref->field_index != HID_FIELD_INDEX_NONE ||
		    (list->flags & HIDDEV_FLAG_REPORT) != 0) {
			list->buffer[list->head] = *uref;
			list->head = (list->head + 1) &
				(HIDDEV_BUFFER_SIZE - 1);
			kill_fasync(&list->fasync, SIGIO, POLL_IN);
		}
	}
	spin_unlock_irqrestore(&hiddev->list_lock, flags);

	wake_up_interruptible(&hiddev->wait);
}

Project: linux
File: hiddev.c

static ssize_t hiddev_write(struct file * file, const char __user * buffer, size_t count, loff_t *ppos)
{
	return -EINVAL;
}