summaryrefslogtreecommitdiffstats
path: root/drivers/hid/uhid.c
diff options
context:
space:
mode:
authorDavid Herrmann <dh.herrmann@googlemail.com>2012-06-10 15:16:25 +0200
committerJiri Kosina <jkosina@suse.cz>2012-06-18 13:42:03 +0200
commitfcfcf0deb89ece6eb9ae23768fec1bc1718f9b7f (patch)
treefcf86b0c55d4feea91ee00a6e59c423b6442708a /drivers/hid/uhid.c
parent3b3baa82e4306b5160692643fab2fa322ceb94f9 (diff)
downloadlinux-stable-fcfcf0deb89ece6eb9ae23768fec1bc1718f9b7f.tar.gz
linux-stable-fcfcf0deb89ece6eb9ae23768fec1bc1718f9b7f.tar.bz2
linux-stable-fcfcf0deb89ece6eb9ae23768fec1bc1718f9b7f.zip
HID: uhid: implement feature requests
HID standard allows sending a feature request to the device which is answered by an HID report. uhid implements this by sending a UHID_FEATURE event to user-space which then must answer with UHID_FEATURE_ANSWER. If it doesn't do this in a timely manner, the request is discarded silently. We serialize the feature requests, that is, there is always only a single active feature-request sent to user-space, other requests have to wait. HIDP and USB-HID do it the same way. Because we discard feature-requests silently, we must make sure to match a response to the corresponding request. We use sequence-IDs for this so user-space must copy the ID from the request into the answer. Feature-answers are ignored if they do not contain the same ID as the currently pending feature request. Internally, we must make sure that feature-requests are synchronized with UHID_DESTROY and close() events. We must not dead-lock when closing the HID device, either, so we have to use separate locks. Signed-off-by: David Herrmann <dh.herrmann@googlemail.com> Signed-off-by: Jiri Kosina <jkosina@suse.cz>
Diffstat (limited to 'drivers/hid/uhid.c')
-rw-r--r--drivers/hid/uhid.c120
1 files changed, 119 insertions, 1 deletions
diff --git a/drivers/hid/uhid.c b/drivers/hid/uhid.c
index 421c492dc824..ea560bfa033d 100644
--- a/drivers/hid/uhid.c
+++ b/drivers/hid/uhid.c
@@ -42,6 +42,12 @@ struct uhid_device {
__u8 head;
__u8 tail;
struct uhid_event *outq[UHID_BUFSIZE];
+
+ struct mutex report_lock;
+ wait_queue_head_t report_wait;
+ atomic_t report_done;
+ atomic_t report_id;
+ struct uhid_event report_buf;
};
static struct miscdevice uhid_misc;
@@ -143,7 +149,84 @@ static int uhid_hid_parse(struct hid_device *hid)
static int uhid_hid_get_raw(struct hid_device *hid, unsigned char rnum,
__u8 *buf, size_t count, unsigned char rtype)
{
- return 0;
+ struct uhid_device *uhid = hid->driver_data;
+ __u8 report_type;
+ struct uhid_event *ev;
+ unsigned long flags;
+ int ret;
+ size_t len;
+ struct uhid_feature_answer_req *req;
+
+ if (!uhid->running)
+ return -EIO;
+
+ switch (rtype) {
+ case HID_FEATURE_REPORT:
+ report_type = UHID_FEATURE_REPORT;
+ break;
+ case HID_OUTPUT_REPORT:
+ report_type = UHID_OUTPUT_REPORT;
+ break;
+ case HID_INPUT_REPORT:
+ report_type = UHID_INPUT_REPORT;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = mutex_lock_interruptible(&uhid->report_lock);
+ if (ret)
+ return ret;
+
+ ev = kzalloc(sizeof(*ev), GFP_KERNEL);
+ if (!ev) {
+ ret = -ENOMEM;
+ goto unlock;
+ }
+
+ spin_lock_irqsave(&uhid->qlock, flags);
+ ev->type = UHID_FEATURE;
+ ev->u.feature.id = atomic_inc_return(&uhid->report_id);
+ ev->u.feature.rnum = rnum;
+ ev->u.feature.rtype = report_type;
+
+ atomic_set(&uhid->report_done, 0);
+ uhid_queue(uhid, ev);
+ spin_unlock_irqrestore(&uhid->qlock, flags);
+
+ ret = wait_event_interruptible_timeout(uhid->report_wait,
+ atomic_read(&uhid->report_done), 5 * HZ);
+
+ /*
+ * Make sure "uhid->running" is cleared on shutdown before
+ * "uhid->report_done" is set.
+ */
+ smp_rmb();
+ if (!ret || !uhid->running) {
+ ret = -EIO;
+ } else if (ret < 0) {
+ ret = -ERESTARTSYS;
+ } else {
+ spin_lock_irqsave(&uhid->qlock, flags);
+ req = &uhid->report_buf.u.feature_answer;
+
+ if (req->err) {
+ ret = -EIO;
+ } else {
+ ret = 0;
+ len = min(count,
+ min_t(size_t, req->size, UHID_DATA_MAX));
+ memcpy(buf, req->data, len);
+ }
+
+ spin_unlock_irqrestore(&uhid->qlock, flags);
+ }
+
+ atomic_set(&uhid->report_done, 1);
+
+unlock:
+ mutex_unlock(&uhid->report_lock);
+ return ret ? ret : len;
}
static int uhid_hid_output_raw(struct hid_device *hid, __u8 *buf, size_t count,
@@ -265,7 +348,11 @@ static int uhid_dev_destroy(struct uhid_device *uhid)
if (!uhid->running)
return -EINVAL;
+ /* clear "running" before setting "report_done" */
uhid->running = false;
+ smp_wmb();
+ atomic_set(&uhid->report_done, 1);
+ wake_up_interruptible(&uhid->report_wait);
hid_destroy_device(uhid->hid);
kfree(uhid->rd_data);
@@ -284,6 +371,31 @@ static int uhid_dev_input(struct uhid_device *uhid, struct uhid_event *ev)
return 0;
}
+static int uhid_dev_feature_answer(struct uhid_device *uhid,
+ struct uhid_event *ev)
+{
+ unsigned long flags;
+
+ if (!uhid->running)
+ return -EINVAL;
+
+ spin_lock_irqsave(&uhid->qlock, flags);
+
+ /* id for old report; drop it silently */
+ if (atomic_read(&uhid->report_id) != ev->u.feature_answer.id)
+ goto unlock;
+ if (atomic_read(&uhid->report_done))
+ goto unlock;
+
+ memcpy(&uhid->report_buf, ev, sizeof(*ev));
+ atomic_set(&uhid->report_done, 1);
+ wake_up_interruptible(&uhid->report_wait);
+
+unlock:
+ spin_unlock_irqrestore(&uhid->qlock, flags);
+ return 0;
+}
+
static int uhid_char_open(struct inode *inode, struct file *file)
{
struct uhid_device *uhid;
@@ -293,9 +405,12 @@ static int uhid_char_open(struct inode *inode, struct file *file)
return -ENOMEM;
mutex_init(&uhid->devlock);
+ mutex_init(&uhid->report_lock);
spin_lock_init(&uhid->qlock);
init_waitqueue_head(&uhid->waitq);
+ init_waitqueue_head(&uhid->report_wait);
uhid->running = false;
+ atomic_set(&uhid->report_done, 1);
file->private_data = uhid;
nonseekable_open(inode, file);
@@ -398,6 +513,9 @@ static ssize_t uhid_char_write(struct file *file, const char __user *buffer,
case UHID_INPUT:
ret = uhid_dev_input(uhid, &uhid->input_buf);
break;
+ case UHID_FEATURE_ANSWER:
+ ret = uhid_dev_feature_answer(uhid, &uhid->input_buf);
+ break;
default:
ret = -EOPNOTSUPP;
}