Skip to content

Report ID get overwritten when using hid_get_input_report on Linux #514

@acolombier

Description

@acolombier

When using hid_get_input_report on Linux, the first byte of the buffer containing the report ID gets overwritten by the report first byte of data while it should not (spec)

Here is a simple way to reproduce the bug with a uhid virtual device

test_dev.c
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

#include <sys/ioctl.h>
#include <linux/uhid.h>
#include <errno.h>


static volatile uint32_t done;

static unsigned char rdesc[] = {
	0x06, 0x02, 0xFF,               /*  Usage Page (FF02h),                     */
	0x09, 0x00,                     /*  Usage (00h),                            */
	0xA1, 0x01,                     /*  Collection (Application),               */
	0x09, 0x01,                     /*      Usage (01h),                        */
	0xA1, 0x02,                     /*      Collection (Logical),               */
	0x85, 0x01,                     /*          Report ID (1),                  */
	0x09, 0x02,                     /*          Usage (02h),                    */
	0x15, 0x00,                     /*          Logical Minimum (0),            */
	0x25, 0x01,                     /*          Logical Maximum (1),            */
	0x75, 0x01,                     /*          Report Size (1),                */
	0x95, 0x88,                     /*          Report Count (136),             */
	0x81, 0x02,                     /*          Input (Variable),               */
	0x09, 0x07,                     /*          Usage (07h),                    */
	0x15, 0x00,                     /*          Logical Minimum (0),            */
	0x25, 0x01,                     /*          Logical Maximum (1),            */
	0x75, 0x01,                     /*          Report Size (1),                */
	0x95, 0x10,                     /*          Report Count (16),              */
	0x81, 0x02,                     /*          Input (Variable),               */
	0x09, 0x03,                     /*          Usage (03h),                    */
	0x15, 0x00,                     /*          Logical Minimum (0),            */
	0x25, 0x0F,                     /*          Logical Maximum (15),           */
	0x75, 0x04,                     /*          Report Size (4),                */
	0x95, 0x06,                     /*          Report Count (6),               */
	0x81, 0x02,                     /*          Input (Variable),               */
	0xC0,                           /*      End Collection,                     */
	0xC0                            /*  End Collection                          */
};

void sighndlr(int signal)
{
	done = 1;
	printf("\n");
}

static int uhid_write(int fd, const struct uhid_event *ev)
{
	ssize_t ret;

	ret = write(fd, ev, sizeof(*ev));
	if (ret < 0) {
		fprintf(stderr, "Cannot write to uhid: %m\n");
		return -errno;
	} else if (ret != sizeof(*ev)) {
		fprintf(stderr, "Wrong size written to uhid: %zd != %zu\n",
			ret, sizeof(ev));
		return -EFAULT;
	} else {
		return 0;
	}
}

static int create(int fd)
{
	struct uhid_event ev;

	memset(&ev, 0, sizeof(ev));
	ev.type = UHID_CREATE;
	strcpy((char*)ev.u.create.name, "test-uhid-device");
	ev.u.create.rd_data = rdesc;
	ev.u.create.rd_size = sizeof(rdesc);
	ev.u.create.bus = BUS_USB;
	ev.u.create.vendor = 0xDEAD;
	ev.u.create.product = 0xBEAF;
	ev.u.create.version = 0;
	ev.u.create.country = 0;

	return uhid_write(fd, &ev);
}

static void destroy(int fd)
{
	struct uhid_event ev;

	memset(&ev, 0, sizeof(ev));
	ev.type = UHID_DESTROY;

	uhid_write(fd, &ev);
}

static void handle_report(struct uhid_event *ev, int fd)
{
	if (ev->u.get_report.rtype != UHID_START)
		return;

	struct uhid_event rep;

	memset(&rep, 0, sizeof(rep));
	rep.type = UHID_GET_REPORT_REPLY;

	rep.u.get_report_reply.id = ev->u.get_report.id;
	rep.u.get_report_reply.err = 0;
	rep.u.get_report_reply.size = 16;
	memset(rep.u.get_report_reply.data, 55, UHID_DATA_MAX);

	uhid_write(fd, &rep);
	return;
}

static int event(int fd)
{
	struct uhid_event ev;
	ssize_t ret;

	memset(&ev, 0, sizeof(ev));
	ret = read(fd, &ev, sizeof(ev));
	if (ret == 0) {
		fprintf(stderr, "Read HUP on uhid-cdev\n");
		return -EFAULT;
	} else if (ret < 0) {
		fprintf(stderr, "Cannot read uhid-cdev: %m\n");
		return -errno;
	} else if (ret != sizeof(ev)) {
		fprintf(stderr, "Invalid size read from uhid-dev: %zd != %zu\n",
			ret, sizeof(ev));
		return -EFAULT;
	}

	switch (ev.type) {
	case UHID_START:
		break;
	case UHID_STOP:
		break;
	case UHID_OPEN:
		break;
	case UHID_CLOSE:
	case UHID_OUTPUT:
		break;
	case UHID_OUTPUT_EV:
		break;
	case UHID_GET_REPORT:
		handle_report(&ev, fd);
		break;
	default:
		fprintf(stderr, "Invalid event from uhid-dev: %u\n", ev.type);
	}

	return 0;
}

int main(int argc, char **argv)
{
	signal(SIGINT, sighndlr);

	// UHID
	int fd = open("/dev/uhid", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

   	fprintf(stderr, "Create uhid device\n");
	if (create(fd)) {
		close(fd);
		return EXIT_FAILURE;
	}

	struct pollfd pfds;
	pfds.fd = fd;
	pfds.events = POLLIN;

	while(!done) {
		int ret = poll(&pfds, 1, 10);
		if (ret < 0) {
			fprintf(stderr, "Cannot poll for fds: %m\n");
			break;
		}
		if (pfds.revents & POLLHUP) {
			fprintf(stderr, "Received HUP on uhid-cdev\n");
			break;
		}
		if (pfds.revents & POLLIN) {
			ret = event(fd);
			if (ret)
				break;
		}
	}

	destroy(fd);

	return 0;
}
test_get_report.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <hidapi/hidapi.h>

#define VENDOR_ID 0xdead
#define PRODUCT_ID 0xbeaf

int main()
{
    int res;
    unsigned char buf[255];
    hid_device *handle;

    res = hid_init();
    if (res != 0) {
        printf("hid_init failed: %ls\n", hid_error(NULL));
        return -1;
    }

    handle = hid_open(VENDOR_ID, PRODUCT_ID, NULL);
    if (!handle) {
        printf("Failed to open UHID device: %ls\n", hid_error(NULL));
        return -1;
    }


    buf[0] = 42;
    int bytesRead = hid_get_input_report(handle, buf, 16);

	fprintf(stderr, "Report received for 42\n");
	for (int i = 0; i < 16; i++)
	{
		printf("%d ", buf[i]);
	}
    printf("\n");

    hid_close(handle);
    hid_exit();

    return 0;
}

With the two previous files in the current directory, run the following commands

sudo modprobe uhid
gcc test_dev.c -lhidapi-hidraw -o testdev && sudo ./testdev &
gcc test_get_report.c -lhidapi-hidraw -o testhid_report && sudo ./testhid_report

Expected output

Report received for 42
42 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 

Actual output

Report received for 42
55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 55 

Version

libhidapi-hidraw: 0.11.2
Linux Kernel: 6.1.11

I'd be happy to help addressing the bug once I get confirmation on it. At first glance, it feels like the fix may have to go in the kernel driver (drivers/hid/hid-core.c or drivers/hid/hidraw.c) as I cannot see any ways that would allow to make a bug fix without a memmove on the library side. But since the name of Alan Ott is mentioned at a few places on the kernel sources/docs, I thought I would raise the bug first before escalating it :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    hidrawRelated to Linux/hidraw backend

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions