Gives a primer on HID-BPF.
Signed-off-by: Benjamin Tissoires benjamin.tissoires@redhat.com
---
new in v3 --- Documentation/hid/hid-bpf.rst | 444 ++++++++++++++++++++++++++++++++++ Documentation/hid/index.rst | 1 + include/uapi/linux/bpf_hid.h | 54 ++++- 3 files changed, 492 insertions(+), 7 deletions(-) create mode 100644 Documentation/hid/hid-bpf.rst
diff --git a/Documentation/hid/hid-bpf.rst b/Documentation/hid/hid-bpf.rst new file mode 100644 index 000000000000..0bf0d937b0e1 --- /dev/null +++ b/Documentation/hid/hid-bpf.rst @@ -0,0 +1,444 @@ +.. SPDX-License-Identifier: GPL-2.0 + +======= +HID-BPF +======= + +HID is a standard protocol for input devices and it can greatly make use +of the eBPF capabilities to speed up development and add new capabilities +to the existing HID interfaces. + +.. contents:: + :local: + :depth: 2 + + +When (and why) using HID-BPF +============================ + +We can enumerate several use cases for when using HID-BPF is better than +using a standard kernel driver fix. + +dead zone of a joystick +----------------------- + +Assuming you have a joystick that is getting older, it is common to see it +wobbling around its neutral point. This is usually filtered at the application +level by adding a *dead zone* for this specific axis. + +With HID-BPF, we can put the filtering of this dead zone in the kernel directly +so we don't wake up userspace when nothing else is happening on the input +controller. + +Of course, given that this dead zone is device specific, we can not create a +generic fix for all of the same joysticks. We *could* create a custom kernel +API for this (by adding a sysfs for instance), but there is no guarantees this +new kernel API will be broadly adopted and maintained. + +HID-BPF allows the userspace program who knows it will make use of this capability +to load the program itself, ensuring we only load the custom API when we have a user. + +simple fixup of report descriptor +--------------------------------- + +In the HID tree, we have half of the drivers that are "simple" and +that just fix one key or one byte in the report descriptor. +Currently, for users of such devices, the process of fixing them +is long and painful. + +With eBPF, we can reduce the burden of building those fixup kernel patches +by providing an eBPF program that does it. Once this has been validated by +the user, we can then embed the source code into the kernel tree and ship it +and load it directly instead of loading a specific kernel module for it. + +Note: the distribution and inclusion in the kernel is still not there yet. + +add a new fancy feature that requires a new kernel API +------------------------------------------------------ + +We have the case currently for the Universal Stylus Interface pens for example. +Basically, USI pens are requiring a new kernel API because there are +some channels of communication our HID and input stack are not capable +of. Instead of using hidraw or creating new sysfs or ioctls, we can rely +on eBPF to have the kernel API controlled by the consumer and to not +impact the performances by waking up userspace every time there is an +event. + +morph a device into something else and control that from userspace +------------------------------------------------------------------ + +Right now, the kernel has to make a choice on how a device looks like. +For that, it can not decide to transform a given device into something else +because that would be lying to userspace and will be even more harder to +unwind when we need the actual definition of the device. + +However, sometimes some new devices are useless with that sane way of defining +devices. For example, the Microsoft Surface Dial is a pushbutton with haptic +feedback that is barely usable as of today. + +With eBPF, userspace can morph that device into a mouse, and convert the dial +events into wheel events. Also, the userspace program can set/unset the haptic +feedback depending on the context. For example, if a menu is popped-up on the +screen we likely need to have a haptic click every 5 degrees, while when +we are fine-grain scrolling in a web page, we probably want the best resolution +without those annoying clicks. + +firewall +-------- + +What if we want to prevent other users to access a specific feature of a +device? (think a possibly bonker firmware update entry popint) + +With eBPF, we can intercept any HID command emitted to the device and +validate it or not. + +This also allows to sync the state between the userspace and the +kernel/bpf program because we can intercept any incoming command. + +tracing +------- + +The last usage is tracing events and all the fun we can do we BPF to summarize +and analyze events. + +Right now, tracing relies on hidraw. It works well except for a couple +of issues: + +1. if the driver doesn't export a hidraw node, we can't trace anything + (eBPF will be a "god-mode" there, so it might raise some eyebrows) +2. hidraw doesn't catch the other process requests to the device, which + means that we have cases where we need to add printks to the kernel + to understand what is happening. + +High-level view of HID-BPF +========================== + +The main idea behind HID-BPF is that it works at an array of bytes level. +Thus, all of the parsing of the HID report and the HID report descriptor +must be implemented in the userspace component that loads the eBPF +program. + +For example, in the dead zone joystick from above, knowing which fields +in the data stream needs to be set to ``0`` needs to be computed by userspace. + +A corrolar of this is that HID-BPF doesn't know about the other subsystems +available in the kernel. *You can not directly emit input event through the +input API from eBPF*. + +When a BPF program need to emit input events, it needs to talk HID, and rely +on the HID kernel processing to translate the HID data into input events. + +Available types of programs +=========================== + +HID-BPF has the following attachment types available: + +1. ``BPF_HID_DEVICE_EVENT`` defined with a ``SEC("hid/device_event")`` in libbpf +2. ``BPF_HID_USER_EVENT`` defined with a ``SEC("hid/user_event")`` in libbpf +3. ``BPF_HID_DRIVER_EVENT`` defined with a ``SEC("hid/driver_event")`` in libbpf +4. ``BPF_HID_RDESC_FIXUP`` defined with a ``SEC("hid/rdesc_fixup")`` in libbpf + +The above types are defined based on where the event came from. + +A ``BPF_HID_DEVICE_EVENT`` is calling a BPF program when an event is received from +the device. Thus we are in IRQ context and can act on the data or notify userspace. + +In the same way, a ``BPF_HID_USER_EVENT`` means that userspace called the syscall +``BPF_PROG_RUN`` facility. + +A ``BPF_HID_DRIVER_EVENT`` means that the kernel driver emitted an event that the bpf +programs want to be notified of. Such events are resume, reset, probed, but also +a call to a request toward the device (a call to ``hid_hw_raw_request()`` for example). + +Last, ``BPF_HID_RDESC_FIXUP`` is different from the others as there can be only one +BPF program of this type. This is called on ``probe`` from the driver and allows to +change the report descriptor from the BPF program. + +General overview of a HID-BPF program +===================================== + +User API available as a context in programs +------------------------------------------- + +.. kernel-doc:: include/uapi/linux/bpf_hid.h + +Accessing the data attached to the context +------------------------------------------ + +The ``struct hid_bpf_ctx`` doesn't export the ``data`` fields directly and to access +it, a bpf program needs to first call ``bpf_hid_get_data(context, offset, size)``. + +``offset`` can be any integer, but ``size`` needs to be constant, known at compile +time. + +This allows the following: + +1. for a given device, if we know that the report length will always be of a certain value, + we can request the ``data`` pointer to point at the full report length. + + The kernel will ensure we are using a correct size and offset and eBPF will ensure + the code will not attempt to read or write outside of the boundaries:: + + __u8 *data = bpf_hid_get_data(ctx, 0 /* offset */, 256 /* size */); + + if (!data) + return 0; /* ensure data is correct, now the verifier knows we + * have 256 bytes available */ + + bpf_printk("hello world: %02x %02x %02x", data[0], data[128], data[255]); + +2. if the report length is variable, but we know the value of ``X`` is always a 16-bits + integer, we can then have a pointer to that value only:: + + __u16 *x = bpf_hid_get_data(ctx, offset, sizeof(*x)); + + if (!x) + return 0; /* something when wrong */ + + *x += 1; /* increment X by one */ + +Bit access of data field (an alternative to ``bpf_hid_get_data``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The HID devices often work at the bit level in the emitted report. +For instance, a given button will be at a given offset (in bits) in the report +and usually of size 1 bit. + +In order to easily access those data, BPF program can rely on ``bpf_hid_get_bits()`` +and ``bpf_hid_set_bits()``. + +Those 2 functions do not require any of the arguments to be a constant like +``bpf_hid_get_data()``, meaning that they are appropriate for accessing +a field unknown at compile time of an unknown size. +The counterpart of those 2 helpers is that they are effectively copying the +data to/from a ``__u32`` instead of having a direct pointer access to the +field. + +Effect of a HID-BPF program +--------------------------- + +For all HID-BPF attachment types except for ``BPF_HID_RDESC_FIXUP``, several eBPF +programs can be attached to the same device. + +Unless ``BPF_F_INSERT_HEAD`` is added to the flags while attaching the program, the +new program is appended at the end of the list. In some cases (tracing for instance) +we need to get the unprocessed events from the device, and ``BPF_F_INSERT_HEAD`` will +insert the new program at the beginning of the list. + +Note however that there is no guarantees that another program is loaded with that +same flag, and thus our program may be second in the list. + +``BPF_HID_DEVICE_EVENT`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Whenever a matching event is raised, the eBPF programs are called one after the other +and are working on the same data buffer. + +If a program changes the data associated with the context, the next one will see +the new data and will have *no* idea of what the original data was. + +Once all the programs are run and return ``0``, the rest of the HID stack will +work on the modified data, with the ``size`` field of the hid_bpf_ctx being the new +size of the input stream of data. + +If a BPF program returns a negative error, this has the same effect than setting +the ``size`` field of ``hid_bpf_ctx`` to ``0``: the event is dropped from the HID +processing. No clients (hidraw, input, LEDs) will ever see that event coming in. + +``BPF_HID_USER_EVENT`` +~~~~~~~~~~~~~~~~~~~~~~ + +One can attach several ``BPF_HID_USER_EVENT`` on a given device. But because the caller +needs to set which BPF program is used when calling the syscall ``BPF_PROG_RUN``, only +this particular BPF program will be run. + +The ``data`` associated with the ``hid_bpf_ctx`` contains the input data of the given +syscall and is big enough to contain both the input data and the requested output data. + +The output data from the syscall, if ``size_out`` is set to a positive value, is copied +from the content of the ``data`` field of ``hid_bpf_ctx``. + +``BPF_HID_DRIVER_EVENT`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +The availability of the ``data`` field of ``hid_bpf_ctx`` depends on the event subtype: + +* **probe** event: ``data`` contains the report descriptor of the device (this is read-only) +* **reset**, **resume** events: no data is provided +* **device request**: ``data`` contains the incoming request made to the device. The BPF + program can decide whether or not forwarding the request to the device. +* **device request answer**: ``data`` contains the answer of the request. + +``BPF_HID_RDESC_FIXUP`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Last, the ``BPF_HID_RDESC_FIXUP`` program works in the similar maneer than +``.report_fixup`` of ``struct hid_driver``. + +When the device is probed, the kernel sets the data buffer of the context with the +content of the report descriptor. The memory associated with that buffer is +``HID_MAX_DESCRIPTOR_SIZE`` (4 kB as of now). + +The eBPF program can modify the data buffer at will and then the kernel uses the +new content and size as the report descriptor. + +Whenever a ``BPF_HID_RDESC_FIXUP`` program is attached (if no program was attached +before), the kernel immediately disconnects the HID device, and do a reprobe. + +In the same way, when the ``BPF_HID_RDESC_FIXUP`` program is detached, the kernel +issues a disconnect on the device. + +To update the report descriptor in place, users can replace the current +``BPF_HID_RDESC_FIXUP`` program through the usual ``BPF_LINK_UPDATE`` syscall. +There will be only one disconnect emitted by the kernel. + +Attaching a bpf program to a device +=================================== + +``libbpf`` exports a helper to attach a HID-BPF program: ``bpf_program__attach_hid(program, fd, flags)``. + +The ``program`` and ``flags`` parameters are relatively obvious, but what about the +``fd`` argument? + +While working on HID-BPF it came out quickly enough that we can not rely on +hidraw to bind a BPF program to a HID device. hidraw is an artefact of the processing +of the HID device, and is not stable. Some drivers even disable it, so that removes the +tracing capabilies on those devices (where it is interesting to get the non-hidraw +traces). + +The solution is to use the sysfs tree and more specifically the ``uevent`` sysfs entry. + +For a given HID device, we have the ``uevent`` node at ``/sys/bus/hid/devices/BUS:VID:PID.000N/uevent``. + +``uevent`` is convenient as it contains already some information about the device itself: +the driver in use, the name of the HID device, its ID, its Unique field and the modalias. + +However, as mentioned previously, we can not really rely on hidraw for HID-BPF. +Which means we need to get the report descriptor from the device thorugh the sysfs too. +This is available at ``/sys/bus/hid/devices/BUS:VID:PID.000N/report_descriptor`` as a +binary stream. + +Parsing the report descriptor is the responsibility of the BPF programmer or the userspace +component that loads the eBPF program. + +An (almost) complete example of a BPF enhanced HID device +========================================================= + +*Foreword: for most parts, this could be implemented as a kernel driver* + +Let's imagine we have a new tablet device that has some haptic capabilities +to simulate the surface the user is scratching on. This device would also have +a specific 3 positions switch to toggle between *pencil on paper*, *cray on a wall* +and *brush on a painting canvas*. To make things even better, we can control the +physical position of the switch through a feature report. + +And of course, the switch is relying on some userspace component to control the +haptic feature of the device itself. + +Filtering events +---------------- + +The first step consists in filtering events from the device. Given that the switch +position is actually reported in the flow of the pen events, using hidraw to implement +that filtering would mean that we wake up userspace for every single event. + +This is OK for libinput, but having an external library that is just interested in +one byte in the report is less than ideal. + +For that, we can create a basic skeleton for our BPF program:: + + #include <linux/bpf.h> + #include <bpf/bpf_helpers.h> + #include <linux/bpf_hid.h> + + /* HID programs need to be GPL */ + char _license[] SEC("license") = "GPL"; + + struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 4096 * 64); + } ringbuf SEC(".maps"); + + __u8 current_value = 0; + + SEC("hid/device_event") + int filter_switch(struct hid_bpf_ctx *ctx) + { + __u8 *data = bpf_hid_get_data(ctx, 0 /* offset */, 192 /* size */); + __u8 *buf; + + if (!data) + return 0; /* EPERM check */ + + if (current_value != data[152]) { + buf = bpf_ringbuf_reserve(&ringbuf, 1, 0); + if (!buf) + return 0; + + *buf = data[152]; + + bpf_ringbuf_commit(buf, 0); + + current_value = data[152]; + } + + return 0; + } + +Our userspace program can now listen to notifications on the ring buffer, and +is awaken only when the value changes. + +Controlling the device +---------------------- + +To be able to change the haptic feedback from the tablet, the userspace program +needs to emit a feature report on the device itself. + +Instead of using hidraw for that, we can create a ``BPF_HID_USER_EVENT`` program +that talks to the device:: + + SEC("hid/user_event") + int send_haptic(struct hid_bpf_ctx *ctx) + { + __u8 *data = bpf_hid_get_data(ctx, 0 /* offset */, 1 /* size */); + int ret; + + if (!data) + return 0; /* EPERM check */ + + ret = bpf_hid_raw_request(ctx, data, HID_FEATURE_REPORT, HID_REQ_SET_REPORT); + ctx->retval = ret; + + return 0; + } + +And then userspace needs to call that program directly:: + + static int set_haptic(struct hid *hid_skel, int sysfs_fd, __u8 haptic_value) + int err, prog_fd; + int ret = -1; + + LIBBPF_OPTS(bpf_test_run_opts, run_attrs, + .repeat = 1, + .ctx_in = &sysfs_fd, + .ctx_size_in = sizeof(sysfs_fd), + .data_in = &haptic_value, + .data_size_in = 1, + ); + + prog_fd = bpf_program__fd(hid_skel->progs.set_haptic); + + err = bpf_prog_test_run_opts(prog_fd, &run_attrs); + return err; + } + +Now the interesting bit is that our userspace program is aware of the haptic +state and can control it. + +Which means, that we can also export a dbus API for applications to change the +haptic feedback. The dbus API would be simple (probably just a string property). + +The interesting bit here is that we did not created a new kernel API for this. +Which means that if there is a bug in our implementation, we can change the +interface with the kernel as will, because the userspace application is responsible +for its own usage. diff --git a/Documentation/hid/index.rst b/Documentation/hid/index.rst index e50f513c579c..b2028f382f11 100644 --- a/Documentation/hid/index.rst +++ b/Documentation/hid/index.rst @@ -11,6 +11,7 @@ Human Interface Devices (HID) hidraw hid-sensor hid-transport + hid-bpf
uhid
diff --git a/include/uapi/linux/bpf_hid.h b/include/uapi/linux/bpf_hid.h index 64a8b9dd8809..8e907e7fe77b 100644 --- a/include/uapi/linux/bpf_hid.h +++ b/include/uapi/linux/bpf_hid.h @@ -11,20 +11,60 @@
#include <linux/types.h>
+/** + * enum hid_bpf_event - event types in struct hid_bpf_ctx + * + * Event types are not tied to a given attach type, there might + * be multiple event types for one attach type. + * + * @HID_BPF_UNDEF: sentinel value, should never be set by the kernel + * @HID_BPF_DEVICE_EVENT: used when attach type is ``BPF_HID_DEVICE_EVENT`` + * @HID_BPF_RDESC_FIXUP: used when attach type is ``BPF_HID_RDESC_FIXUP`` + * @HID_BPF_USER_EVENT: used when attach type is ``BPF_HID_USER_EVENT`` + */ enum hid_bpf_event { HID_BPF_UNDEF = 0, - HID_BPF_DEVICE_EVENT, /* when attach type is BPF_HID_DEVICE_EVENT */ - HID_BPF_RDESC_FIXUP, /* ................... BPF_HID_RDESC_FIXUP */ - HID_BPF_USER_EVENT, /* ................... BPF_HID_USER_EVENT */ + HID_BPF_DEVICE_EVENT, + HID_BPF_RDESC_FIXUP, + HID_BPF_USER_EVENT, };
/* User accessible data for HID programs. Add new fields at the end. */ +/** + * struct hid_bpf_ctx - User accessible data for all HID programs + * + * ``data`` is not directly accessible from the context. We need to issue + * a call to ``bpf_hid_get_data()`` in order to get a pointer to that field. + * + * @type: Of type enum hid_bpf_event. This value is read-only and matters when there + * is more than one event type per attachment type. + * @allocated_size: Allocated size of data, read-only. + * + * This is how much memory is available and can be requested + * by the HID program. + * Note that for ``HID_BPF_RDESC_FIXUP``, that memory is set to + * ``4096`` (4 KB) + * @size: Valid data in the data field, read-write. + * + * Programs can get the available valid size in data by fetching this field. + * Programs can also change this value and even set it to ``0`` to discard the + * data from this event. + * + * ``size`` must always be less or equal than ``allocated_size`` (it is enforced + * once all BPF programs have been run). + * @retval: Return value of the program when type is ``HID_BPF_USER_EVENT``, read-write. + * + * Returning from the program an error means that the execution of the program + * failed. However, one may want to express that the program executed correctly, + * but the underlying calls failed for a specific reason. This is when we use + * retval. + */ struct hid_bpf_ctx { - __u32 type; /* enum hid_bpf_event, RO */ - __u32 allocated_size; /* allocated size of data, RO */ + __u32 type; + __u32 allocated_size;
- __u32 size; /* valid data in data field, RW */ - __s32 retval; /* when type is HID_BPF_USER_EVENT, RW */ + __u32 size; + __s32 retval; };
#endif /* _UAPI__LINUX_BPF_HID_H__ */