mirror of
https://github.com/torvalds/linux.git
synced 2024-11-24 21:21:41 +00:00
Documentation: add HID-BPF docs
Gives a primer on HID-BPF. Signed-off-by: Benjamin Tissoires <benjamin.tissoires@redhat.com> Signed-off-by: Jiri Kosina <jkosina@suse.cz>
This commit is contained in:
parent
a56a256933
commit
dfae6bec71
512
Documentation/hid/hid-bpf.rst
Normal file
512
Documentation/hid/hid-bpf.rst
Normal file
@ -0,0 +1,512 @@
|
||||
.. SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
=======
|
||||
HID-BPF
|
||||
=======
|
||||
|
||||
HID is a standard protocol for input devices but some devices may require
|
||||
custom tweaks, traditionally done with a kernel driver fix. Using the eBPF
|
||||
capabilities instead speeds up development and adds new capabilities to the
|
||||
existing HID interfaces.
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 2
|
||||
|
||||
|
||||
When (and why) to use HID-BPF
|
||||
=============================
|
||||
|
||||
There are several use cases when using HID-BPF is better
|
||||
than 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 apply this filtering in the kernel directly so userspace
|
||||
does not get woken up when nothing else is happening on the input controller.
|
||||
|
||||
Of course, given that this dead zone is specific to an individual device, we
|
||||
can not create a generic fix for all of the same joysticks. Adding a custom
|
||||
kernel API for this (e.g. by adding a sysfs entry) does not guarantee this new
|
||||
kernel API will be broadly adopted and maintained.
|
||||
|
||||
HID-BPF allows the userspace program 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, half of the drivers only fix one key or one byte
|
||||
in the report descriptor. These fixes all require a kernel patch and the
|
||||
subsequent shepherding into a release, a long and painful process for users.
|
||||
|
||||
We can reduce this burden by providing an eBPF program instead. Once such a
|
||||
program has been verified by the user, we can embed the source code into the
|
||||
kernel tree and ship the eBPF program and load it directly instead of loading
|
||||
a specific kernel module for it.
|
||||
|
||||
Note: distribution of eBPF programs and their inclusion in the kernel is not
|
||||
yet fully implemented
|
||||
|
||||
Add a new feature that requires a new kernel API
|
||||
------------------------------------------------
|
||||
|
||||
An example for such a feature are the Universal Stylus Interface (USI) pens.
|
||||
Basically, USI pens require a new kernel API because there are new
|
||||
channels of communication that our HID and input stack do not support.
|
||||
Instead of using hidraw or creating new sysfs entries 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
|
||||
------------------------------------------------------------------
|
||||
|
||||
The kernel has a relatively static mapping of HID items to evdev bits.
|
||||
It cannot decide to dynamically transform a given device into something else
|
||||
as it does not have the required context and any such transformation cannot be
|
||||
undone (or even discovered) by userspace.
|
||||
|
||||
However, some devices are useless with that static 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 visible on the
|
||||
screen we likely need to have a haptic click every 15 degrees. But when
|
||||
scrolling in a web page the user experience is better when the device emits
|
||||
events at the highest resolution.
|
||||
|
||||
Firewall
|
||||
--------
|
||||
|
||||
What if we want to prevent other users to access a specific feature of a
|
||||
device? (think a possibly broken firmware update entry point)
|
||||
|
||||
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 this may raise some eyebrows)
|
||||
2. hidraw doesn't catch other processes' 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 corollary 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 needs to emit input events, it needs to talk with the HID
|
||||
protocol, and rely on the HID kernel processing to translate the HID data into
|
||||
input events.
|
||||
|
||||
Available types of programs
|
||||
===========================
|
||||
|
||||
HID-BPF is built "on top" of BPF, meaning that we use tracing method to
|
||||
declare our programs.
|
||||
|
||||
HID-BPF has the following attachment types available:
|
||||
|
||||
1. event processing/filtering with ``SEC("fmod_ret/hid_bpf_device_event")`` in libbpf
|
||||
2. actions coming from userspace with ``SEC("syscall")`` in libbpf
|
||||
3. change of the report descriptor with ``SEC("fmod_ret/hid_bpf_rdesc_fixup")`` in libbpf
|
||||
|
||||
A ``hid_bpf_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.
|
||||
And given that we are in IRQ context, we can not talk back to the device.
|
||||
|
||||
A ``syscall`` means that userspace called the syscall ``BPF_PROG_RUN`` facility.
|
||||
This time, we can do any operations allowed by HID-BPF, and talking to the device is
|
||||
allowed.
|
||||
|
||||
Last, ``hid_bpf_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. Once a ``hid_bpf_rdesc_fixup``
|
||||
program has been loaded, it is not possible to overwrite it unless the program which
|
||||
inserted it allows us by pinning the program and closing all of its fds pointing to it.
|
||||
|
||||
Developer API:
|
||||
==============
|
||||
|
||||
User API data structures available in programs:
|
||||
-----------------------------------------------
|
||||
|
||||
.. kernel-doc:: include/linux/hid_bpf.h
|
||||
|
||||
Available tracing functions to attach a HID-BPF program:
|
||||
--------------------------------------------------------
|
||||
|
||||
.. kernel-doc:: drivers/hid/bpf/hid_bpf_dispatch.c
|
||||
:functions: hid_bpf_device_event hid_bpf_rdesc_fixup
|
||||
|
||||
Available API that can be used in all HID-BPF programs:
|
||||
-------------------------------------------------------
|
||||
|
||||
.. kernel-doc:: drivers/hid/bpf/hid_bpf_dispatch.c
|
||||
:functions: hid_bpf_get_data
|
||||
|
||||
Available API that can be used in syscall HID-BPF programs:
|
||||
-----------------------------------------------------------
|
||||
|
||||
.. kernel-doc:: drivers/hid/bpf/hid_bpf_dispatch.c
|
||||
:functions: hid_bpf_attach_prog hid_bpf_hw_request hid_bpf_allocate_context hid_bpf_release_context
|
||||
|
||||
General overview of a HID-BPF program
|
||||
=====================================
|
||||
|
||||
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 :c:func:`hid_bpf_get_data`.
|
||||
|
||||
``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 = hid_bpf_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-bit
|
||||
integer, we can then have a pointer to that value only::
|
||||
|
||||
__u16 *x = hid_bpf_get_data(ctx, offset, sizeof(*x));
|
||||
|
||||
if (!x)
|
||||
return 0; /* something went wrong */
|
||||
|
||||
*x += 1; /* increment X by one */
|
||||
|
||||
Effect of a HID-BPF program
|
||||
---------------------------
|
||||
|
||||
For all HID-BPF attachment types except for :c:func:`hid_bpf_rdesc_fixup`, several eBPF
|
||||
programs can be attached to the same device.
|
||||
|
||||
Unless ``HID_BPF_FLAG_INSERT_HEAD`` is added to the flags while attaching the
|
||||
program, the new program is appended at the end of the list.
|
||||
``HID_BPF_FLAG_INSERT_HEAD`` will insert the new program at the beginning of the
|
||||
list which is useful for e.g. tracing where we need to get the unprocessed events
|
||||
from the device.
|
||||
|
||||
Note that if there are multiple programs using the ``HID_BPF_FLAG_INSERT_HEAD`` flag,
|
||||
only the most recently loaded one is actually the first in the list.
|
||||
|
||||
``SEC("fmod_ret/hid_bpf_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 modified data but it will have *no* idea of what the original data was.
|
||||
|
||||
Once all the programs are run and return ``0`` or a positive value, the rest of the
|
||||
HID stack will work on the modified data, with the ``size`` field of the last hid_bpf_ctx
|
||||
being the new size of the input stream of data.
|
||||
|
||||
A BPF program returning a negative error discards the event, i.e. this event will not be
|
||||
processed by the HID stack. Clients (hidraw, input, LEDs) will **not** see this event.
|
||||
|
||||
``SEC("syscall")``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``syscall`` are not attached to a given device. To tell which device we are working
|
||||
with, userspace needs to refer to the device by its unique system id (the last 4 numbers
|
||||
in the sysfs path: ``/sys/bus/hid/devices/xxxx:yyyy:zzzz:0000``).
|
||||
|
||||
To retrieve a context associated with the device, the program must call
|
||||
:c:func:`hid_bpf_allocate_context` and must release it with :c:func:`hid_bpf_release_context`
|
||||
before returning.
|
||||
Once the context is retrieved, one can also request a pointer to kernel memory with
|
||||
:c:func:`hid_bpf_get_data`. This memory is big enough to support all input/output/feature
|
||||
reports of the given device.
|
||||
|
||||
``SEC("fmod_ret/hid_bpf_rdesc_fixup")``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``hid_bpf_rdesc_fixup`` program works in a similar manner to
|
||||
``.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`` (currently 4kB).
|
||||
|
||||
The eBPF program can modify the data buffer at-will and the kernel uses the
|
||||
modified content and size as the report descriptor.
|
||||
|
||||
Whenever a ``SEC("fmod_ret/hid_bpf_rdesc_fixup")`` program is attached (if no
|
||||
program was attached before), the kernel immediately disconnects the HID device
|
||||
and does a reprobe.
|
||||
|
||||
In the same way, when the ``SEC("fmod_ret/hid_bpf_rdesc_fixup")`` program is
|
||||
detached, the kernel issues a disconnect on the device.
|
||||
|
||||
There is no ``detach`` facility in HID-BPF. Detaching a program happens when
|
||||
all the user space file descriptors pointing at a program are closed.
|
||||
Thus, if we need to replace a report descriptor fixup, some cooperation is
|
||||
required from the owner of the original report descriptor fixup.
|
||||
The previous owner will likely pin the program in the bpffs, and we can then
|
||||
replace it through normal bpf operations.
|
||||
|
||||
Attaching a bpf program to a device
|
||||
===================================
|
||||
|
||||
``libbpf`` does not export any helper to attach a HID-BPF program.
|
||||
Users need to use a dedicated ``syscall`` program which will call
|
||||
``hid_bpf_attach_prog(hid_id, program_fd, flags)``.
|
||||
|
||||
``hid_id`` is the unique system ID of the HID device (the last 4 numbers in the
|
||||
sysfs path: ``/sys/bus/hid/devices/xxxx:yyyy:zzzz:0000``)
|
||||
|
||||
``progam_fd`` is the opened file descriptor of the program to attach.
|
||||
|
||||
``flags`` is of type ``enum hid_bpf_attach_flags``.
|
||||
|
||||
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).
|
||||
|
||||
On the other hand, the ``hid_id`` is stable for the entire life of the HID device,
|
||||
even if we change its report descriptor.
|
||||
|
||||
Given that hidraw is not stable when the device disconnects/reconnects, we recommend
|
||||
accessing the current report descriptor of the device through the sysfs.
|
||||
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 "vmlinux.h"
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
/* HID programs need to be GPL */
|
||||
char _license[] SEC("license") = "GPL";
|
||||
|
||||
/* HID-BPF kfunc API definitions */
|
||||
extern __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx,
|
||||
unsigned int offset,
|
||||
const size_t __sz) __ksym;
|
||||
extern int hid_bpf_attach_prog(unsigned int hid_id, int prog_fd, u32 flags) __ksym;
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_RINGBUF);
|
||||
__uint(max_entries, 4096 * 64);
|
||||
} ringbuf SEC(".maps");
|
||||
|
||||
struct attach_prog_args {
|
||||
int prog_fd;
|
||||
unsigned int hid;
|
||||
unsigned int flags;
|
||||
int retval;
|
||||
};
|
||||
|
||||
SEC("syscall")
|
||||
int attach_prog(struct attach_prog_args *ctx)
|
||||
{
|
||||
ctx->retval = hid_bpf_attach_prog(ctx->hid,
|
||||
ctx->prog_fd,
|
||||
ctx->flags);
|
||||
return 0;
|
||||
}
|
||||
|
||||
__u8 current_value = 0;
|
||||
|
||||
SEC("?fmod_ret/hid_bpf_device_event")
|
||||
int BPF_PROG(filter_switch, struct hid_bpf_ctx *hid_ctx)
|
||||
{
|
||||
__u8 *data = hid_bpf_get_data(hid_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;
|
||||
}
|
||||
|
||||
To attach ``filter_switch``, userspace needs to call the ``attach_prog`` syscall
|
||||
program first::
|
||||
|
||||
static int attach_filter(struct hid *hid_skel, int hid_id)
|
||||
{
|
||||
int err, prog_fd;
|
||||
int ret = -1;
|
||||
struct attach_prog_args args = {
|
||||
.hid = hid_id,
|
||||
};
|
||||
DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs,
|
||||
.ctx_in = &args,
|
||||
.ctx_size_in = sizeof(args),
|
||||
);
|
||||
|
||||
args.prog_fd = bpf_program__fd(hid_skel->progs.filter_switch);
|
||||
|
||||
prog_fd = bpf_program__fd(hid_skel->progs.attach_prog);
|
||||
|
||||
err = bpf_prog_test_run_opts(prog_fd, &tattrs);
|
||||
return err;
|
||||
}
|
||||
|
||||
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 ``SEC("syscall")`` program
|
||||
that talks to the device::
|
||||
|
||||
/* some more HID-BPF kfunc API definitions */
|
||||
extern struct hid_bpf_ctx *hid_bpf_allocate_context(unsigned int hid_id) __ksym;
|
||||
extern void hid_bpf_release_context(struct hid_bpf_ctx *ctx) __ksym;
|
||||
extern int hid_bpf_hw_request(struct hid_bpf_ctx *ctx,
|
||||
__u8* data,
|
||||
size_t len,
|
||||
enum hid_report_type type,
|
||||
enum hid_class_request reqtype) __ksym;
|
||||
|
||||
|
||||
struct hid_send_haptics_args {
|
||||
/* data needs to come at offset 0 so we can do a memcpy into it */
|
||||
__u8 data[10];
|
||||
unsigned int hid;
|
||||
};
|
||||
|
||||
SEC("syscall")
|
||||
int send_haptic(struct hid_send_haptics_args *args)
|
||||
{
|
||||
struct hid_bpf_ctx *ctx;
|
||||
int ret = 0;
|
||||
|
||||
ctx = hid_bpf_allocate_context(args->hid);
|
||||
if (!ctx)
|
||||
return 0; /* EPERM check */
|
||||
|
||||
ret = hid_bpf_hw_request(ctx,
|
||||
args->data,
|
||||
10,
|
||||
HID_FEATURE_REPORT,
|
||||
HID_REQ_SET_REPORT);
|
||||
|
||||
hid_bpf_release_context(ctx);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
And then userspace needs to call that program directly::
|
||||
|
||||
static int set_haptic(struct hid *hid_skel, int hid_id, __u8 haptic_value)
|
||||
{
|
||||
int err, prog_fd;
|
||||
int ret = -1;
|
||||
struct hid_send_haptics_args args = {
|
||||
.hid = hid_id,
|
||||
};
|
||||
DECLARE_LIBBPF_OPTS(bpf_test_run_opts, tattrs,
|
||||
.ctx_in = &args,
|
||||
.ctx_size_in = sizeof(args),
|
||||
);
|
||||
|
||||
args.data[0] = 0x02; /* report ID of the feature on our device */
|
||||
args.data[1] = haptic_value;
|
||||
|
||||
prog_fd = bpf_program__fd(hid_skel->progs.set_haptic);
|
||||
|
||||
err = bpf_prog_test_run_opts(prog_fd, &tattrs);
|
||||
return err;
|
||||
}
|
||||
|
||||
Now our userspace program is aware of the haptic state and can control it. The
|
||||
program could make this state further available to other userspace programs
|
||||
(e.g. via a DBus API).
|
||||
|
||||
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 at-will, because the userspace application is
|
||||
responsible for its own usage.
|
@ -11,6 +11,7 @@ Human Interface Devices (HID)
|
||||
hidraw
|
||||
hid-sensor
|
||||
hid-transport
|
||||
hid-bpf
|
||||
|
||||
uhid
|
||||
|
||||
|
@ -9097,6 +9097,7 @@ M: Benjamin Tissoires <benjamin.tissoires@redhat.com>
|
||||
L: linux-input@vger.kernel.org
|
||||
S: Maintained
|
||||
T: git git://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git
|
||||
F: Documentation/hid/
|
||||
F: drivers/hid/
|
||||
F: include/linux/hid*
|
||||
F: include/uapi/linux/hid*
|
||||
|
Loading…
Reference in New Issue
Block a user