Last active
July 11, 2026 19:21
-
-
Save binarycraft007/e81aa94625a7b745072e94eb0c0fef9d to your computer and use it in GitHub Desktop.
udev hid bpf cli tool
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #define _GNU_SOURCE | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <unistd.h> | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <ftw.h> | |
| #include <libudev.h> | |
| #include <bpf/libbpf.h> | |
| #include <bpf/bpf.h> | |
| #include <bpf/btf.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <linux/limits.h> | |
| #include <dirent.h> | |
| /* Constants and Structs */ | |
| #define HID_MAX_DESCRIPTOR_SIZE 4096 | |
| #define BPF_FS_PATH "/sys/fs/bpf/hid" | |
| struct hid_bpf_probe_args { | |
| unsigned int hid; | |
| unsigned int rdesc_size; | |
| unsigned char rdesc[HID_MAX_DESCRIPTOR_SIZE]; | |
| int retval; | |
| }; | |
| struct attach_prog_args { | |
| int prog_fd; | |
| unsigned int hid; | |
| int retval; | |
| }; | |
| static int mkdir_p(const char *path) | |
| { | |
| char tmp[PATH_MAX]; | |
| char *p = NULL; | |
| size_t len; | |
| snprintf(tmp, sizeof(tmp), "%s", path); | |
| len = strlen(tmp); | |
| if (tmp[len - 1] == '/') | |
| tmp[len - 1] = 0; | |
| for (p = tmp + 1; *p; p++) { | |
| if (*p == '/') { | |
| *p = 0; | |
| if (mkdir(tmp, 0755) < 0 && errno != EEXIST) { | |
| return -1; | |
| } | |
| *p = '/'; | |
| } | |
| } | |
| if (mkdir(tmp, 0755) < 0 && errno != EEXIST) { | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| static int unlink_cb(const char *fpath, const struct stat *sb, int typeflag, | |
| struct FTW *ftwbuf) | |
| { | |
| int rv = remove(fpath); | |
| if (rv) | |
| perror(fpath); | |
| return rv; | |
| } | |
| static int rm_rf(const char *path) | |
| { | |
| if (access(path, F_OK) != 0) | |
| return 0; | |
| return nftw(path, unlink_cb, 64, FTW_DEPTH | FTW_PHYS); | |
| } | |
| static int get_hid_id(struct udev_device *dev, unsigned int *id) | |
| { | |
| const char *sysname = udev_device_get_sysname(dev); | |
| if (!sysname) | |
| return -1; | |
| const char *dot = strrchr(sysname, '.'); | |
| if (!dot) | |
| return -1; | |
| char *endptr; | |
| unsigned long val = strtoul(dot + 1, &endptr, 16); | |
| if (*endptr != '\0') | |
| return -1; | |
| *id = (unsigned int)val; | |
| return 0; | |
| } | |
| static int get_report_descriptor(const char *sysname, unsigned char *buf, | |
| size_t max_len, unsigned int *size) | |
| { | |
| char path[PATH_MAX]; | |
| snprintf(path, sizeof(path), | |
| "/sys/bus/hid/devices/%s/report_descriptor", sysname); | |
| int fd = open(path, O_RDONLY); | |
| if (fd < 0) | |
| return -errno; | |
| ssize_t ret = read(fd, buf, max_len); | |
| close(fd); | |
| if (ret < 0) | |
| return -errno; | |
| *size = (unsigned int)ret; | |
| return 0; | |
| } | |
| static void sanitize_str(char *str) | |
| { | |
| for (; *str; str++) { | |
| if (*str == ':' || *str == '.') | |
| *str = '_'; | |
| } | |
| } | |
| static const char *get_filename(const char *path) | |
| { | |
| const char *f = strrchr(path, '/'); | |
| return f ? f + 1 : path; | |
| } | |
| static int inject_properties(struct bpf_object *obj, struct udev_device *dev) | |
| { | |
| struct btf *btf = bpf_object__btf(obj); | |
| if (!btf) | |
| return 0; | |
| struct bpf_map *map; | |
| bpf_object__for_each_map(map, obj) { | |
| const char *map_name = bpf_map__name(map); | |
| if (strcmp(map_name, ".bss") != 0 && | |
| strcmp(map_name, ".data") != 0 && | |
| strcmp(map_name, ".rodata") != 0) { | |
| continue; | |
| } | |
| int type_id = bpf_map__btf_value_type_id(map); | |
| if (type_id <= 0) | |
| continue; | |
| const struct btf_type *sec = btf__type_by_id(btf, type_id); | |
| if (!sec || btf_kind(sec) != BTF_KIND_DATASEC) | |
| continue; | |
| int num_vars = btf_vlen(sec); | |
| struct btf_var_secinfo *infos = btf_var_secinfos(sec); | |
| size_t value_sz = bpf_map__value_size(map); | |
| void *data = malloc(value_sz); | |
| if (!data) | |
| return -ENOMEM; | |
| int key = 0; | |
| /* Using libbpf high-level API correctly */ | |
| if (bpf_map__lookup_elem(map, &key, sizeof(key), data, value_sz, | |
| 0) < 0) { | |
| free(data); | |
| continue; | |
| } | |
| bool modified = false; | |
| for (int i = 0; i < num_vars; i++) { | |
| const struct btf_type *var_type = | |
| btf__type_by_id(btf, infos[i].type); | |
| const char *var_name = | |
| btf__name_by_offset(btf, var_type->name_off); | |
| if (strncmp(var_name, "UDEV_PROP_", 10) == 0) { | |
| const char *prop_name = var_name + 10; | |
| const char *prop_val = | |
| udev_device_get_property_value( | |
| dev, prop_name); | |
| if (prop_val) { | |
| size_t prop_len = strlen(prop_val); | |
| size_t var_size = infos[i].size; | |
| size_t offset = infos[i].offset; | |
| if (prop_len < var_size) { | |
| memset((char *)data + offset, 0, | |
| var_size); | |
| memcpy((char *)data + offset, | |
| prop_val, prop_len); | |
| modified = true; | |
| printf("Injected property %s=%s into map %s\n", | |
| prop_name, prop_val, | |
| map_name); | |
| } else { | |
| fprintf(stderr, | |
| "Property %s value too long for BPF variable\n", | |
| prop_name); | |
| } | |
| } | |
| } | |
| } | |
| if (modified) { | |
| if (bpf_map__update_elem(map, &key, sizeof(key), data, | |
| value_sz, BPF_ANY) < 0) { | |
| perror("Failed to update map with properties"); | |
| } | |
| } | |
| free(data); | |
| } | |
| return 0; | |
| } | |
| static int load_attach_helper(struct bpf_object **obj_out, int *prog_fd_out) | |
| { | |
| const char *paths[] = { "attach.bpf.o", "src/bpf/attach.bpf.o", | |
| "/usr/libexec/udev-hid-bpf/attach.bpf.o", | |
| NULL }; | |
| struct bpf_object *obj = NULL; | |
| int i = 0; | |
| while (paths[i]) { | |
| if (access(paths[i], R_OK) == 0) { | |
| obj = bpf_object__open_file(paths[i], NULL); | |
| if (!libbpf_get_error(obj)) { | |
| break; | |
| } | |
| } | |
| i++; | |
| } | |
| if (!obj) { | |
| fprintf(stderr, | |
| "Failed to locate or open attach.bpf.o helper.\n"); | |
| return -1; | |
| } | |
| if (bpf_object__load(obj)) { | |
| fprintf(stderr, "Failed to load helper object.\n"); | |
| bpf_object__close(obj); | |
| return -1; | |
| } | |
| struct bpf_program *prog = | |
| bpf_object__find_program_by_name(obj, "attach_prog"); | |
| if (!prog) { | |
| fprintf(stderr, "Helper object missing 'attach_prog'.\n"); | |
| bpf_object__close(obj); | |
| return -1; | |
| } | |
| *obj_out = obj; | |
| *prog_fd_out = bpf_program__fd(prog); | |
| return 0; | |
| } | |
| static const char *bus_to_string(int bus) | |
| { | |
| switch (bus) { | |
| case 0x01: | |
| return "BUS_PCI"; | |
| case 0x02: | |
| return "BUS_ISAPNP"; | |
| case 0x03: | |
| return "BUS_USB"; | |
| case 0x04: | |
| return "BUS_HIL"; | |
| case 0x05: | |
| return "BUS_BLUETOOTH"; | |
| case 0x06: | |
| return "BUS_VIRTUAL"; | |
| case 0x10: | |
| return "BUS_ISA"; | |
| case 0x11: | |
| return "BUS_I8042"; | |
| case 0x12: | |
| return "BUS_XTKBD"; | |
| case 0x13: | |
| return "BUS_RS232"; | |
| case 0x14: | |
| return "BUS_GAMEPORT"; | |
| case 0x15: | |
| return "BUS_PARPORT"; | |
| case 0x16: | |
| return "BUS_AMIGA"; | |
| case 0x17: | |
| return "BUS_ADB"; | |
| case 0x18: | |
| return "BUS_I2C"; | |
| case 0x19: | |
| return "BUS_HOST"; | |
| case 0x1A: | |
| return "BUS_GSC"; | |
| case 0x1B: | |
| return "BUS_ATARI"; | |
| case 0x1C: | |
| return "BUS_SPI"; | |
| case 0x1D: | |
| return "BUS_RMI"; | |
| case 0x1E: | |
| return "BUS_CEC"; | |
| case 0x1F: | |
| return "BUS_INTEL_ISHTP"; | |
| case 0x20: | |
| return "BUS_AMD_SFH"; | |
| default: | |
| return NULL; | |
| } | |
| } | |
| static const char *group_to_string(int group) | |
| { | |
| switch (group) { | |
| case 0x01: | |
| return "HID_GROUP_GENERIC"; | |
| case 0x02: | |
| return "HID_GROUP_MULTITOUCH"; | |
| case 0x03: | |
| return "HID_GROUP_SENSOR_HUB"; | |
| case 0x04: | |
| return "HID_GROUP_MULTITOUCH_WIN_8"; | |
| case 0x100: | |
| return "HID_GROUP_RMI"; | |
| case 0x101: | |
| return "HID_GROUP_WACOM"; | |
| case 0x102: | |
| return "HID_GROUP_LOGITECH_DJ_DEVICE"; | |
| case 0x103: | |
| return "HID_GROUP_STEAM"; | |
| case 0x104: | |
| return "HID_GROUP_LOGITECH_27MHZ_DEVICE"; | |
| case 0x105: | |
| return "HID_GROUP_VIVALDI"; | |
| default: | |
| return NULL; | |
| } | |
| } | |
| static int cmd_list_devices(void) | |
| { | |
| struct udev *udev; | |
| struct udev_enumerate *enumerate; | |
| struct udev_list_entry *devices, *dev_list_entry; | |
| udev = udev_new(); | |
| if (!udev) | |
| return -ENOMEM; | |
| enumerate = udev_enumerate_new(udev); | |
| udev_enumerate_add_match_subsystem(enumerate, "hid"); | |
| udev_enumerate_scan_devices(enumerate); | |
| devices = udev_enumerate_get_list_entry(enumerate); | |
| printf("devices:\n"); | |
| udev_list_entry_foreach(dev_list_entry, devices) | |
| { | |
| const char *path = udev_list_entry_get_name(dev_list_entry); | |
| struct udev_device *dev = | |
| udev_device_new_from_syspath(udev, path); | |
| if (dev) { | |
| const char *sysname = udev_device_get_sysname(dev); | |
| const char *name = | |
| udev_device_get_property_value(dev, "HID_NAME"); | |
| const char *modalias = | |
| udev_device_get_property_value(dev, "MODALIAS"); | |
| unsigned int bus = 0, group = 0, vid = 0, pid = 0; | |
| char bus_s[32], group_s[32]; | |
| if (modalias) { | |
| sscanf(modalias, "hid:b%Xg%Xv%Xp%X", &bus, | |
| &group, &vid, &pid); | |
| } | |
| const char *b_str = bus_to_string(bus); | |
| if (b_str) | |
| snprintf(bus_s, sizeof(bus_s), "%s", b_str); | |
| else | |
| snprintf(bus_s, sizeof(bus_s), "%04X", bus); | |
| const char *g_str = group_to_string(group); | |
| if (g_str) | |
| snprintf(group_s, sizeof(group_s), "%s", g_str); | |
| else | |
| snprintf(group_s, sizeof(group_s), "%04X", | |
| group); | |
| printf(" - syspath: \"%s\"\n", path); | |
| printf(" name: \"%s\"\n", | |
| name ? name : "UNKNOWN"); | |
| printf(" device entry: \"HID_DEVICE(%s, %s, 0x%04X, 0x%04X)\"\n", | |
| bus_s, group_s, vid, pid); | |
| /* Check for BPFs */ | |
| char sysname_clean[256]; | |
| strncpy(sysname_clean, sysname, sizeof(sysname_clean)); | |
| sanitize_str(sysname_clean); | |
| char pin_path[PATH_MAX]; | |
| snprintf(pin_path, sizeof(pin_path), "%s/%s", | |
| BPF_FS_PATH, sysname_clean); | |
| if (access(pin_path, F_OK) == 0) { | |
| DIR *dir = opendir(pin_path); | |
| if (dir) { | |
| struct dirent *ent; | |
| int count = 0; | |
| while ((ent = readdir(dir)) != NULL) { | |
| if (ent->d_type == DT_DIR && | |
| ent->d_name[0] != '.') { | |
| if (count == 0) | |
| printf(" bpfs:\n"); | |
| printf(" - \"%s\"\n", | |
| ent->d_name); | |
| count++; | |
| } | |
| } | |
| closedir(dir); | |
| } | |
| } | |
| udev_device_unref(dev); | |
| } | |
| } | |
| udev_enumerate_unref(enumerate); | |
| udev_unref(udev); | |
| return 0; | |
| } | |
| /* Helper to ensure we have the HID device (walks up parents) */ | |
| static struct udev_device *ensure_hid_device(struct udev_device *dev) | |
| { | |
| struct udev_device *parent = dev; | |
| udev_device_ref(parent); // Increment ref to handle the loop correctly | |
| while (parent) { | |
| const char *subsystem = udev_device_get_subsystem(parent); | |
| if (subsystem && strcmp(subsystem, "hid") == 0) { | |
| return parent; | |
| } | |
| struct udev_device *old = parent; | |
| parent = udev_device_get_parent_with_subsystem_devtype( | |
| old, "hid", NULL); | |
| if (parent) | |
| udev_device_ref(parent); | |
| udev_device_unref(old); | |
| } | |
| return NULL; | |
| } | |
| static int cmd_add(const char *syspath, const char *bpf_obj_path) | |
| { | |
| struct udev *udev = NULL; | |
| struct udev_device *dev_raw = NULL; | |
| struct udev_device *dev = NULL; | |
| struct bpf_object *obj = NULL; | |
| struct bpf_object *helper_obj = NULL; | |
| int helper_fd = -1; | |
| unsigned int hid_id = 0; | |
| int err = 0; | |
| udev = udev_new(); | |
| if (!udev) | |
| return -ENOMEM; | |
| dev_raw = udev_device_new_from_syspath(udev, syspath); | |
| if (!dev_raw) { | |
| fprintf(stderr, "Device not found: %s\n", syspath); | |
| err = -ENOENT; | |
| goto cleanup; | |
| } | |
| dev = ensure_hid_device(dev_raw); | |
| udev_device_unref(dev_raw); // We are done with the raw device | |
| if (!dev) { | |
| fprintf(stderr, | |
| "Device %s is not a HID device or has no HID parent.\n", | |
| syspath); | |
| err = -ENODEV; | |
| goto cleanup; | |
| } | |
| const char *sysname_raw = udev_device_get_sysname(dev); | |
| if (get_hid_id(dev, &hid_id) < 0) { | |
| fprintf(stderr, "Invalid HID sysname format: %s\n", | |
| sysname_raw); | |
| err = -EINVAL; | |
| goto cleanup; | |
| } | |
| printf("Target: %s (ID: 0x%04X)\n", sysname_raw, hid_id); | |
| obj = bpf_object__open_file(bpf_obj_path, NULL); | |
| if (libbpf_get_error(obj)) { | |
| fprintf(stderr, "Failed to open BPF object: %s\n", | |
| bpf_obj_path); | |
| err = -ENOENT; | |
| goto cleanup; | |
| } | |
| bool is_struct_ops = false; | |
| struct bpf_program *prog; | |
| bpf_object__for_each_program(prog, obj) { | |
| if (bpf_program__type(prog) == BPF_PROG_TYPE_STRUCT_OPS) { | |
| is_struct_ops = true; | |
| break; | |
| } | |
| } | |
| if (is_struct_ops) { | |
| struct bpf_map *map; | |
| bpf_object__for_each_map(map, obj) { | |
| if (bpf_map__type(map) == BPF_MAP_TYPE_STRUCT_OPS) { | |
| size_t sz = 0; | |
| void *data = bpf_map__initial_value(map, &sz); | |
| if (data && sz >= sizeof(hid_id)) { | |
| memcpy(data, &hid_id, sizeof(hid_id)); | |
| } else { | |
| fprintf(stderr, | |
| "Error: struct_ops map too small or no initial value.\n"); | |
| } | |
| } | |
| } | |
| } | |
| if (bpf_object__load(obj)) { | |
| fprintf(stderr, "Failed to load BPF object.\n"); | |
| err = -EINVAL; | |
| goto cleanup; | |
| } | |
| if (inject_properties(obj, dev) < 0) { | |
| fprintf(stderr, "Warning: Failed to inject properties.\n"); | |
| } | |
| struct bpf_program *probe_prog = | |
| bpf_object__find_program_by_name(obj, "probe"); | |
| if (probe_prog) { | |
| struct hid_bpf_probe_args args = { .hid = hid_id, | |
| .retval = -1 }; | |
| unsigned int rsize = 0; | |
| if (get_report_descriptor(sysname_raw, args.rdesc, | |
| sizeof(args.rdesc), &rsize) == 0) { | |
| args.rdesc_size = rsize; | |
| struct bpf_test_run_opts opts = { | |
| .sz = sizeof(struct bpf_test_run_opts), | |
| .ctx_in = &args, | |
| .ctx_size_in = sizeof(args), | |
| }; | |
| err = bpf_prog_test_run_opts( | |
| bpf_program__fd(probe_prog), &opts); | |
| if (err || opts.retval != 0) { | |
| fprintf(stderr, | |
| "Probe rejected device (ret=%d, err=%d).\n", | |
| opts.retval, err); | |
| err = -EPERM; | |
| goto cleanup; | |
| } | |
| } else { | |
| fprintf(stderr, | |
| "Warning: Could not read report descriptor for probe.\n"); | |
| } | |
| } | |
| /* Path Construction matching Rust: /sys/fs/bpf/hid/<sysname_clean>/<objname_clean> */ | |
| char sysname_clean[256]; | |
| strncpy(sysname_clean, sysname_raw, sizeof(sysname_clean)); | |
| sanitize_str(sysname_clean); | |
| char objname_clean[256]; | |
| strncpy(objname_clean, get_filename(bpf_obj_path), | |
| sizeof(objname_clean)); | |
| // Remove .bpf.o or .o extension if present? Rust does "file_stem". | |
| char *dot = strstr(objname_clean, ".bpf.o"); | |
| if (dot) | |
| *dot = 0; | |
| else { | |
| dot = strrchr(objname_clean, '.'); | |
| if (dot) | |
| *dot = 0; | |
| } | |
| sanitize_str(objname_clean); | |
| char pin_dir[PATH_MAX]; | |
| snprintf(pin_dir, sizeof(pin_dir), "%s/%s/%s", BPF_FS_PATH, | |
| sysname_clean, objname_clean); | |
| if (mkdir_p(pin_dir) < 0) { | |
| perror("Failed to create pin directory"); | |
| err = -errno; | |
| goto cleanup; | |
| } | |
| if (is_struct_ops) { | |
| struct bpf_map *map; | |
| bpf_object__for_each_map(map, obj) { | |
| if (bpf_map__type(map) == BPF_MAP_TYPE_STRUCT_OPS) { | |
| struct bpf_link *link = | |
| bpf_map__attach_struct_ops(map); | |
| if (!link) { | |
| fprintf(stderr, | |
| "Failed to attach struct_ops map %s\n", | |
| bpf_map__name(map)); | |
| continue; | |
| } | |
| char pin_path[PATH_MAX]; | |
| snprintf(pin_path, sizeof(pin_path), "%s/%s", | |
| pin_dir, bpf_map__name(map)); | |
| if (bpf_link__pin(link, pin_path)) { | |
| fprintf(stderr, | |
| "Failed to pin link %s\n", | |
| pin_path); | |
| } else { | |
| printf("Attached StructOps: %s\n", | |
| pin_path); | |
| } | |
| } | |
| } | |
| } else { | |
| if (load_attach_helper(&helper_obj, &helper_fd) < 0) { | |
| err = -ENOENT; | |
| goto cleanup; | |
| } | |
| bpf_object__for_each_program(prog, obj) { | |
| if (bpf_program__type(prog) == BPF_PROG_TYPE_TRACING) { | |
| struct attach_prog_args args = { | |
| .prog_fd = bpf_program__fd(prog), | |
| .hid = hid_id, | |
| .retval = -1 | |
| }; | |
| struct bpf_test_run_opts opts = { | |
| .sz = sizeof(struct bpf_test_run_opts), | |
| .ctx_in = &args, | |
| .ctx_size_in = sizeof(args), | |
| }; | |
| err = bpf_prog_test_run_opts(helper_fd, &opts); | |
| if (err) { | |
| fprintf(stderr, | |
| "Attach helper execution failed: %s\n", | |
| strerror(errno)); | |
| continue; | |
| } | |
| if (args.retval < 0) { | |
| fprintf(stderr, | |
| "Attach failed for %s (ret=%d)\n", | |
| bpf_program__name(prog), | |
| args.retval); | |
| continue; | |
| } | |
| int link_fd = args.retval; | |
| char pin_path[PATH_MAX]; | |
| snprintf(pin_path, sizeof(pin_path), "%s/%s", | |
| pin_dir, bpf_program__name(prog)); | |
| if (bpf_obj_pin(link_fd, pin_path)) { | |
| fprintf(stderr, | |
| "Failed to pin link FD to %s\n", | |
| pin_path); | |
| close(link_fd); | |
| } else { | |
| printf("Attached Tracing: %s\n", | |
| pin_path); | |
| } | |
| } | |
| } | |
| } | |
| struct bpf_map *map; | |
| bpf_object__for_each_map(map, obj) { | |
| if (bpf_map__type(map) != BPF_MAP_TYPE_STRUCT_OPS && | |
| !strchr(bpf_map__name(map), '.') && | |
| strcmp(bpf_map__name(map), ".bss") != 0 && | |
| strcmp(bpf_map__name(map), ".data") != 0 && | |
| strcmp(bpf_map__name(map), ".rodata") != 0) { | |
| char pin_path[PATH_MAX]; | |
| snprintf(pin_path, sizeof(pin_path), "%s/%s", pin_dir, | |
| bpf_map__name(map)); | |
| if (bpf_map__pin(map, pin_path) == 0) { | |
| printf("Pinned map: %s\n", pin_path); | |
| } | |
| } | |
| } | |
| cleanup: | |
| if (helper_obj) | |
| bpf_object__close(helper_obj); | |
| if (obj) | |
| bpf_object__close(obj); | |
| if (dev) | |
| udev_device_unref(dev); | |
| if (udev) | |
| udev_unref(udev); | |
| return err; | |
| } | |
| static int cmd_remove(const char *syspath) | |
| { | |
| struct udev *udev = NULL; | |
| struct udev_device *dev = NULL; | |
| int err = 0; | |
| udev = udev_new(); | |
| if (!udev) | |
| return -ENOMEM; | |
| dev = udev_device_new_from_syspath(udev, syspath); | |
| if (!dev) { | |
| fprintf(stderr, "Device not found: %s\n", syspath); | |
| udev_unref(udev); | |
| return -ENOENT; | |
| } | |
| const char *sysname_raw = udev_device_get_sysname(dev); | |
| char sysname_clean[256]; | |
| strncpy(sysname_clean, sysname_raw, sizeof(sysname_clean)); | |
| sanitize_str(sysname_clean); | |
| char pin_dir[PATH_MAX]; | |
| snprintf(pin_dir, sizeof(pin_dir), "%s/%s", BPF_FS_PATH, sysname_clean); | |
| printf("Detaching BPF programs (removing %s)...\n", pin_dir); | |
| if (rm_rf(pin_dir) < 0) { | |
| perror("Failed to remove BPF directory"); | |
| err = -errno; | |
| } else { | |
| printf("Success.\n"); | |
| } | |
| udev_device_unref(dev); | |
| udev_unref(udev); | |
| return err; | |
| } | |
| typedef struct { | |
| unsigned int bus; | |
| unsigned int group; | |
| unsigned int vid; | |
| unsigned int pid; | |
| } hid_device_id; | |
| static int parse_hid_bpf_config(struct bpf_object *obj, hid_device_id **ids_out, | |
| int *count_out) | |
| { | |
| struct btf *btf = bpf_object__btf(obj); | |
| if (!btf) | |
| return 0; | |
| int sec_id = btf__find_by_name_kind(btf, ".hid_bpf_config", | |
| BTF_KIND_DATASEC); | |
| if (sec_id < 0) | |
| return 0; | |
| const struct btf_type *sec = btf__type_by_id(btf, sec_id); | |
| if (!sec) | |
| return 0; | |
| struct btf_var_secinfo *infos = btf_var_secinfos(sec); | |
| int num_vars = btf_vlen(sec); | |
| for (int i = 0; i < num_vars; i++) { | |
| const struct btf_type *var = | |
| btf__type_by_id(btf, infos[i].type); | |
| /* Skip typedefs/consts/volatiles to get to the type */ | |
| int type_id = var->type; | |
| while (1) { | |
| const struct btf_type *t = | |
| btf__type_by_id(btf, type_id); | |
| if (btf_is_typedef(t) || btf_is_const(t) || | |
| btf_is_volatile(t)) { | |
| type_id = t->type; | |
| } else { | |
| break; | |
| } | |
| } | |
| const struct btf_type *t = btf__type_by_id(btf, type_id); | |
| if (!btf_is_union(t)) | |
| continue; | |
| int num_devices = btf_vlen(t); | |
| const struct btf_member *devices = btf_members(t); | |
| *ids_out = calloc(num_devices, sizeof(hid_device_id)); | |
| *count_out = num_devices; | |
| for (int j = 0; j < num_devices; j++) { | |
| int struct_id = devices[j].type; | |
| const struct btf_type *dev_struct = | |
| btf__type_by_id(btf, struct_id); | |
| if (!btf_is_struct(dev_struct)) | |
| continue; | |
| const struct btf_member *fields = | |
| btf_members(dev_struct); | |
| int num_fields = btf_vlen(dev_struct); | |
| for (int k = 0; k < num_fields; k++) { | |
| const char *fname = btf__name_by_offset( | |
| btf, fields[k].name_off); | |
| int ptr_id = fields[k].type; | |
| const struct btf_type *ptr = | |
| btf__type_by_id(btf, ptr_id); | |
| if (!btf_is_ptr(ptr)) | |
| continue; | |
| const struct btf_type *arr = | |
| btf__type_by_id(btf, ptr->type); | |
| if (!btf_is_array(arr)) | |
| continue; | |
| unsigned int val = btf_array(arr)->nelems; | |
| if (strcmp(fname, "bus") == 0) | |
| (*ids_out)[j].bus = val; | |
| else if (strcmp(fname, "group") == 0) | |
| (*ids_out)[j].group = val; | |
| else if (strcmp(fname, "vid") == 0) | |
| (*ids_out)[j].vid = val; | |
| else if (strcmp(fname, "pid") == 0) | |
| (*ids_out)[j].pid = val; | |
| } | |
| } | |
| return 0; | |
| } | |
| return 0; | |
| } | |
| static int cmd_inspect(const char *bpf_obj_path) | |
| { | |
| struct bpf_object *obj = bpf_object__open_file(bpf_obj_path, NULL); | |
| if (libbpf_get_error(obj)) { | |
| fprintf(stderr, "Failed to open %s\n", bpf_obj_path); | |
| return -1; | |
| } | |
| printf("Filename: %s\n", get_filename(bpf_obj_path)); | |
| hid_device_id *ids = NULL; | |
| int count = 0; | |
| parse_hid_bpf_config(obj, &ids, &count); | |
| if (count > 0) { | |
| printf("Supported Devices:\n"); | |
| for (int i = 0; i < count; i++) { | |
| printf(" - Bus: 0x%04X, Group: 0x%04X, VID: 0x%04X, PID: 0x%04X\n", | |
| ids[i].bus, ids[i].group, ids[i].vid, | |
| ids[i].pid); | |
| } | |
| free(ids); | |
| } else { | |
| printf("Supported Devices: (None found in .hid_bpf_config)\n"); | |
| } | |
| printf("Programs:\n"); | |
| struct bpf_program *prog; | |
| bpf_object__for_each_program(prog, obj) { | |
| printf(" - %s (%s)\n", bpf_program__name(prog), | |
| bpf_program__section_name(prog)); | |
| } | |
| printf("Maps:\n"); | |
| struct bpf_map *map; | |
| bpf_object__for_each_map(map, obj) { | |
| printf(" - %s\n", bpf_map__name(map)); | |
| } | |
| bpf_object__close(obj); | |
| return 0; | |
| } | |
| static int cmd_install(const char *bpf_obj_path) | |
| { | |
| char clean_name[256]; | |
| strncpy(clean_name, get_filename(bpf_obj_path), sizeof(clean_name)); | |
| char *dot = strstr(clean_name, ".bpf.o"); | |
| if (dot) | |
| *dot = 0; | |
| // 1. Inspect to get devices | |
| struct bpf_object *obj = bpf_object__open_file(bpf_obj_path, NULL); | |
| if (libbpf_get_error(obj)) { | |
| fprintf(stderr, "Failed to open %s\n", bpf_obj_path); | |
| return -1; | |
| } | |
| hid_device_id *ids = NULL; | |
| int count = 0; | |
| parse_hid_bpf_config(obj, &ids, &count); | |
| bpf_object__close(obj); | |
| if (count == 0) { | |
| fprintf(stderr, | |
| "No device matches found in %s, cannot generate udev rule.\n", | |
| bpf_obj_path); | |
| return -1; | |
| } | |
| // 2. Install file | |
| if (mkdir_p("/etc/udev-hid-bpf") < 0) { | |
| perror("Failed to create /etc/udev-hid-bpf"); | |
| return -1; | |
| } | |
| char target_path[PATH_MAX]; | |
| snprintf(target_path, sizeof(target_path), "/etc/udev-hid-bpf/%s", | |
| get_filename(bpf_obj_path)); | |
| char cp_cmd[PATH_MAX * 2]; | |
| snprintf(cp_cmd, sizeof(cp_cmd), "cp '%s' '%s'", bpf_obj_path, | |
| target_path); | |
| if (system(cp_cmd) != 0) { | |
| fprintf(stderr, "Failed to copy file to %s\n", target_path); | |
| free(ids); | |
| return -1; | |
| } | |
| printf("Installed BPF object to %s\n", target_path); | |
| // 3. Generate Rule | |
| if (mkdir_p("/etc/udev/rules.d") < 0) { | |
| perror("Failed to create /etc/udev/rules.d"); | |
| return -1; | |
| } | |
| char rule_path[PATH_MAX]; | |
| snprintf(rule_path, sizeof(rule_path), | |
| "/etc/udev/rules.d/99-hid-bpf-%s.rules", clean_name); | |
| FILE *f = fopen(rule_path, "w"); | |
| if (!f) { | |
| perror("Failed to create udev rule file"); | |
| free(ids); | |
| return -1; | |
| } | |
| /* Find absolute path of ourselves for the rule */ | |
| char self_exe[PATH_MAX]; | |
| ssize_t len = | |
| readlink("/proc/self/exe", self_exe, sizeof(self_exe) - 1); | |
| if (len > 0) | |
| self_exe[len] = 0; | |
| else | |
| strcpy(self_exe, "/usr/local/bin/udev-hid-bpf"); | |
| fprintf(f, "# Generated by udev-hid-bpf install\n"); | |
| fprintf(f, "ACTION!=\"add|remove\", GOTO=\"hid_bpf_end\"\n"); | |
| fprintf(f, "SUBSYSTEM!=\"hid\", GOTO=\"hid_bpf_end\"\n\n"); | |
| for (int i = 0; i < count; i++) { | |
| char bus_s[10] = "*", group_s[10] = "*", vid_s[10] = "*", | |
| pid_s[10] = "*"; | |
| if (ids[i].bus) | |
| snprintf(bus_s, sizeof(bus_s), "%04X", ids[i].bus); | |
| if (ids[i].group) | |
| snprintf(group_s, sizeof(group_s), "%04X", | |
| ids[i].group); | |
| if (ids[i].vid) | |
| snprintf(vid_s, sizeof(vid_s), "%08X", ids[i].vid); | |
| if (ids[i].pid) | |
| snprintf(pid_s, sizeof(pid_s), "%08X", ids[i].pid); | |
| fprintf(f, "# Match %d\n", i); | |
| fprintf(f, | |
| "ENV{MODALIAS}==\"hid:b%sg%sv%sp%s\", RUN+=\"%s add $sys$devpath %s\"\n", | |
| bus_s, group_s, vid_s, pid_s, self_exe, target_path); | |
| fprintf(f, | |
| "ENV{MODALIAS}==\"hid:b%sg%sv%sp%s\", ACTION==\"remove\", RUN+=\"%s remove $sys$devpath\"\n", | |
| bus_s, group_s, vid_s, pid_s, self_exe); | |
| } | |
| fprintf(f, "\nLABEL=\"hid_bpf_end\"\n"); | |
| fclose(f); | |
| free(ids); | |
| printf("Installed udev rule to %s\n", rule_path); | |
| printf("Reloading udev rules...\n"); | |
| system("udevadm control --reload"); | |
| return 0; | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| if (argc < 2) { | |
| fprintf(stderr, | |
| "Usage: %s <add|remove|list-devices|inspect|install> [args...]\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| if (strcmp(argv[1], "add") == 0) { | |
| if (argc < 4) { | |
| fprintf(stderr, "Usage: %s add <syspath> <bpf_obj>\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| return cmd_add(argv[2], argv[3]); | |
| } else if (strcmp(argv[1], "remove") == 0) { | |
| if (argc < 3) { | |
| fprintf(stderr, "Usage: %s remove <syspath>\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| return cmd_remove(argv[2]); | |
| } else if (strcmp(argv[1], "list-devices") == 0) { | |
| return cmd_list_devices(); | |
| } else if (strcmp(argv[1], "inspect") == 0) { | |
| if (argc < 3) { | |
| fprintf(stderr, "Usage: %s inspect <bpf_obj>\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| return cmd_inspect(argv[2]); | |
| } else if (strcmp(argv[1], "install") == 0) { | |
| if (argc < 3) { | |
| fprintf(stderr, "Usage: %s install <bpf_obj>\n", | |
| argv[0]); | |
| return 1; | |
| } | |
| return cmd_install(argv[2]); | |
| } else { | |
| fprintf(stderr, "Unknown command: %s\n", argv[1]); | |
| return 1; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment