Tutorial

This tutorial will guide you step-by-step through writing your first HID BPF program. The assumption is that you have successfully compiled and installed this repo.

The HID report descriptor for this device is listed in the Example HID Report Descriptor.

Identifying the device

Let’s assume we have a mouse that needs some fixes, for example a mouse that keeps sending events for a button that doesn’t even exist on the device.

There is a page on Matching BPF programs to a device but for now we’ll use the tool:

$ udev-hid-bpf list-devices
/sys/bus/hid/devices/0003:045E:07A5.0001
  - name:         Microsoft Microsoft® 2.4GHz Transceiver v9.0
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x045E, 0x07A5)
/sys/bus/hid/devices/0003:045E:07A5.0002
  - name:         Microsoft Microsoft® 2.4GHz Transceiver v9.0
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x045E, 0x07A5)
/sys/bus/hid/devices/0003:045E:07A5.0003
  - name:         Microsoft Microsoft® 2.4GHz Transceiver v9.0
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x045E, 0x07A5)
/sys/bus/hid/devices/0003:046D:4088.0009
  - name:         Logitech ERGO K860
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_LOGITECH_DJ_DEVICE, 0x046D, 0x046D)
/sys/bus/hid/devices/0003:046D:C52B.0004
  - name:         Logitech USB Receiver
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x046D, 0xC52B)
/sys/bus/hid/devices/0003:046D:C52B.0005
  - name:         Logitech USB Receiver
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x046D, 0xC52B)
/sys/bus/hid/devices/0003:046D:C52B.0006
  - name:         Logitech USB Receiver
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x046D, 0xC52B)
/sys/bus/hid/devices/0003:1050:0407.0007
  - name:         Yubico YubiKey OTP+FIDO+CCID
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x1050, 0x0407)
/sys/bus/hid/devices/0003:1050:0407.0008
  - name:         Yubico YubiKey OTP+FIDO+CCID
  - device entry: HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x1050, 0x0407)
...

The syspath and device name are merely for you to identify the correct device. For our code, the device entry is what matters. In our case, this is

HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, 0x045E, 0x07A5)

This macro will identify our device for us in the BPF program.

Note

This device has multiple HID interfaces, so we will have to use the Run-time probe in our program.

Scaffolding

Let’s create the file and fill it with enough information to compile:

$ touch ./src/bpf/testing/10-vendor__mymouse.bpf.c

See the Filename conventions for details about this particular file name.

And this file contains:

// SPDX-License-Identifier: GPL-2.0-only
#include "vmlinux.h"
#include "hid_bpf.h"
#include "hid_bpf_helpers.h"
#include <bpf/bpf_tracing.h>

#define VID_MICROSOFT 0x045E
#define PID_SCULPT_ERGONOMIC_MOUSE 0x07A5

HID_BPF_CONFIG(
     HID_DEVICE(BUS_USB, HID_GROUP_GENERIC, VID_MICROSOFT, PID_SCULPT_ERGONOMIC_MOUSE)
);

SEC("fmod_ret/hid_bpf_rdesc_fixup")
int BPF_PROG(ignore_button_fix_rdesc, struct hid_bpf_ctx *hctx)
{
    return 0;
}

SEC("fmod_ret/hid_bpf_device_event")
int BPF_PROG(ignore_button_fix_event, struct hid_bpf_ctx *hid_ctx)
{
    return 0;
}

/* If your device only has a single HID interface you can skip
   the probe function altogether */
SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
    /* Bind to any device, we don't do anything yet anyway */
    ctx->retval = 0;

    return 0;
}

char _license[] SEC("license") = "GPL";

This doesn’t do anything but it should be buildable, can be installed and we can attempt to load it manually:

$ sudo ./install.sh
$ sudo udev-hid-bpf --verbose add /sys/bus/hid/devices/0003:045E:07A5.0001 10-vendor__mymouse.bpf.o
DEBUG - device added 0003:045E:07A5.0001, filename: target/bpf/10-vendor__mymouse.bpf.o
DEBUG - loading BPF object at "target/bpf/10-vendor__mymouse.bpf.o"
DEBUG - successfully attached ignore_button_fix_event to device id 1
DEBUG - Successfully pinned prog at /sys/fs/bpf/hid/0003_045E_07A5_0001/10-vendor__mymouse_bpf/ignore_button_fix_event

Because the BPF program is “pinned” it will remain even after the loading process terminates. And indeed, the BPF program shows up in the bpffs:

$ sudo tree /sys/fs/bpf/hid/
  /sys/fs/bpf/hid/
  └── 0003_045E_07A5_0001
      └── 10-vendor__mymouse_bpf
          └── ignore_button_fix_event

And we can remove it again (so we can re-add it later):

$ sudo udev-hid-bpf --verbose remove /sys/bus/hid/devices/0003:045E:07A5.0001

Note

The official tool for listing BPF programs is bpftool prog which will list all currently loaded BPF programs. Our program will be listed as ignore_button_fix_rdesc and/or ignore_button_fix_event.

Probing

Note

If your device only has one HID interface you do not need a probe function. Feel free to skip this section.

Now, before we do anything we want to make sure our program is only called for the HID interface we actually want to fix up. Most complex devices (gaming mice, anything on a receiver, etc.) will expose multiple HID interfaces and we don’t want to change the HID reports on the wrong device. We do this by looking at the HID report descriptor that is passed to us as a byte array in the ctx struct:

struct hid_bpf_probe_args {
  unsigned int hid;
  unsigned int rdesc_size;  /* number of valid bytes */
  unsigned char rdesc[4096]; /* the actual report descriptor */
  int retval;
};

In our case, we want to operate on the device that has a HID Usage Generic Desktop, Mouse. This particular device also has a Keyboard and a Consumer Control but we need to ignore those. So our probe() changes to check exactly that:

SEC("syscall")
int probe(struct hid_bpf_probe_args *ctx)
{
    if (ctx->rdesc_size > 4 &&
        ctx->rdesc[0] == 0x05 && /* Usage Page */
        ctx->rdesc[1] == 0x01 && /* Generic Desktop */
        ctx->rdesc[2] == 0x09 && /* Usage */
        ctx->rdesc[3] == 0x02)   /* Mouse */
        ctx->retval = 0;
    else
        ctx->retval = -EINVAL;

    return 0;
}

Note

Use the hid-recorder tool from hid-tools. to analyze HID report descriptors.

Now, as it turns out we actually stop loading the program now. Why? Because the device path we provided to the udev-hid-bpf tool is the Keyboard device, not the Mouse. Passing in the other interface (with the 0002 suffix) works:

$ sudo udev-hid-bpf --verbose add /sys/bus/hid/devices/0003:045E:07A5.0001 10-vendor__mymouse.bpf.o
DEBUG - device added 0003:045E:07A5.0001, filename: /usr/local/lib/firmware/hid/bpf/10-vendor__mymouse.bpf.o
DEBUG - loading BPF object at "/usr/local/lib/firmware/hid/bpf/10-vendor__mymouse.bpf.o"

$ sudo udev-hid-bpf --verbose add /sys/bus/hid/devices/0003:045E:07A5.0002 10-vendor__mymouse.bpf.o
DEBUG - device added 0003:045E:07A5.0002, filename: /usr/local/lib/firmware/hid/bpf/10-vendor__mymouse.bpf.o
DEBUG - loading BPF object at "/usr/local/lib/firmware/hid/bpf/10-vendor__mymouse.bpf.o"
DEBUG - successfully attached ignore_button_fix_event to device id 2
DEBUG - Successfully pinned prog at /sys/fs/bpf/hid/0003_045E_07A5_0002/10-vendor__mymouse_bpf/ignore_button_fix_event

This indicates our probe is working correctly.

Modifying the HID Reports

Now that the program loads for the right device, let’s make sure our fake buttons don’t go through. Our device sends a report with ID 26 with 5 bits that represent the buttons (see the Example HID Report Descriptor). The report is 6 bytes long (Report ID, button bits, two 16-bit values for x/y). So all we have to do is unset the bit for the annoying button:

SEC("fmod_ret/hid_bpf_device_event")
int BPF_PROG(ignore_button_fix_event, struct hid_bpf_ctx *hid_ctx)
{
    const int expected_length = 6;
    const int expected_report_id = 26;
    __u8 *data;

    if (hid_ctx->size < expected_length)
        return 0;

    data = hid_bpf_get_data(hid_ctx, 0, expected_length);
    if (!data || data[0] != expected_report_id)
        return 0; /* EPERM or the wrong report ID */

    data[1] &= 0x7; /* Unset all buttons but left/middle/right */

    return 0;
}

The only noteworthy bit here is that we don’t automatically get passed the data for the HID report, we have to fetch it with hid_bpf_get_data(ctx, offset, length). The returned buffer is the kernel buffer, not a copy, so modifications have near-zero costs.

Modifying the HID Report Descriptor

With our code in place we no longer get fake button events. But it would be nice if the device doesn’t even advertise those buttons to begin with. For that we can manipulate the report descriptor, much in the same way as we manipulated the HID report above:

SEC("fmod_ret/hid_bpf_rdesc_fixup")
int BPF_PROG(ignore_button_fix_rdesc, struct hid_bpf_ctx *hctx)
{
    const int expected_length = 223;
    if (hid_ctx->size != expected_length)
        return 0;

    __u8 *data = hid_bpf_get_data(hid_ctx, 0 /* offset */, 4096 /* size */);
    if (!data)
        return 0; /* EPERM */

    /* Safety check, our probe() should take care of this though */
    if (data[1] != 0x01 /* Generic Desktop */ || data[3] != 0x2 /* Mouse */)
        return 0;

    /* The report descriptor has 5 buttons and 3 pad bits, swap that around.
     * With some minimal safety check to ensure we're on the right HID fields
     * here. */
    if (data[22] == 0x29 && /* Usage Maximum */
        data[24] == 0x95 && /* Report Count */
        data[34] == 0x75) { /* Report Size */
        data[23] = 3; /* Usage Maximum to 3 buttons */
        data[25] = 3; /* Report count to 3 bits */
        data[35] = 5; /* Report size for padding bits to 5 bits */
    }

    return 0;
}

The data returned this time is the HID Report Descriptor as an allocated 4K buffer.

Because we’re modifying the HID report descriptor, injecting the BPF program causes a disconnect of our real HID device and a reconnect of the modified device (see dmesg or udevadm monitor). Likewise, removing our BPF program causes a disconnect of the modified device and a reconnect of the real HID device.

Bringing it all together

Once the BPF program works as expected, installing it sets up the systemd hwdb and the udev rules for the program to be loaded automatically whenever the device is plugged in. This can be verified by checking wether the HID_BPF_n property exists on the device:

$ udevadm info /sys/bus/hid/devices/0003:045E:07A5*
P: /devices/pci0000:00/0000:00:14.0/usb1/1-4/1-4:1.0/0003:045E:07A5.0022
M: 0003:045E:07A5.0022
R: 0022
U: hid
V: hid-generic
E: DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-4/1-4:1.0/0003:045E:07A5.0022
E: SUBSYSTEM=hid
E: DRIVER=hid-generic
E: HID_ID=0003:0000045E:000007A5
E: HID_NAME=Microsoft Microsoft® 2.4GHz Transceiver v9.0
E: HID_PHYS=usb-0000:00:14.0-4/input0
E: HID_UNIQ=
E: MODALIAS=hid:b0003g0001v0000045Ep000007A5
E: USEC_INITIALIZED=4768059665
E: HID_BPF_27=10-vendor__mymouse.bpf.o

...

This property is set by udev-hid-bpf’s hwdb entries and udev rule and if it exists, plugging/unplugging the device will load or unload the BPF program for this device.