[RFC hid v1 03/10] HID: add a tool to convert a bpf source into a generic bpf loader

From: Benjamin Tissoires
Date: Thu Nov 24 2022 - 10:18:10 EST


We first use bpftool to generate a json representation of the program,
and then use a python script to convert it into a C array element that
will be able to be generically loaded by the kernel.

Signed-off-by: Benjamin Tissoires <benjamin.tissoires@xxxxxxxxxx>
---
MAINTAINERS | 1 +
drivers/hid/bpf/progs/Makefile | 105 +++++++++++
drivers/hid/bpf/progs/hid_bpf.h | 15 ++
drivers/hid/bpf/progs/hid_bpf_helpers.h | 22 +++
drivers/hid/bpf/progs/hid_bpf_progs.h | 49 +++++
tools/hid/build_progs_list.py | 231 ++++++++++++++++++++++++
6 files changed, 423 insertions(+)
create mode 100644 drivers/hid/bpf/progs/Makefile
create mode 100644 drivers/hid/bpf/progs/hid_bpf.h
create mode 100644 drivers/hid/bpf/progs/hid_bpf_helpers.h
create mode 100644 drivers/hid/bpf/progs/hid_bpf_progs.h
create mode 100755 tools/hid/build_progs_list.py

diff --git a/MAINTAINERS b/MAINTAINERS
index 752126fba795..8580895e280f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9102,6 +9102,7 @@ F: drivers/hid/
F: include/linux/hid*
F: include/uapi/linux/hid*
F: samples/hid/
+F: tools/hid
F: tools/testing/selftests/hid/

HID LOGITECH DRIVERS
diff --git a/drivers/hid/bpf/progs/Makefile b/drivers/hid/bpf/progs/Makefile
new file mode 100644
index 000000000000..ee0203d3349a
--- /dev/null
+++ b/drivers/hid/bpf/progs/Makefile
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: GPL-2.0
+OUTPUT := .output
+abs_out := $(abspath $(OUTPUT))
+
+CLANG ?= clang
+LLC ?= llc
+LLVM_STRIP ?= llvm-strip
+
+TOOLS_PATH := $(abspath ../../../../tools)
+BPFTOOL_SRC := $(TOOLS_PATH)/bpf/bpftool
+BPFTOOL_OUTPUT := $(abs_out)/bpftool
+DEFAULT_BPFTOOL := $(BPFTOOL_OUTPUT)/bootstrap/bpftool
+BPFTOOL ?= $(DEFAULT_BPFTOOL)
+
+LIBBPF_SRC := $(TOOLS_PATH)/lib/bpf
+LIBBPF_OUTPUT := $(abs_out)/libbpf
+LIBBPF_DESTDIR := $(LIBBPF_OUTPUT)
+LIBBPF_INCLUDE := $(LIBBPF_DESTDIR)/include
+BPFOBJ := $(LIBBPF_OUTPUT)/libbpf.a
+
+HID_BPF_CONVERTER := $(TOOLS_PATH)/hid/build_progs_list.py
+
+INCLUDES := -I$(OUTPUT) -I$(LIBBPF_INCLUDE) -I$(TOOLS_PATH)/include/uapi
+CFLAGS := -g -Wall
+
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \
+ $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \
+ ../../../../vmlinux \
+ /sys/kernel/btf/vmlinux \
+ /boot/vmlinux-$(shell uname -r)
+VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
+ifeq ($(VMLINUX_BTF),)
+$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
+endif
+
+ifeq ($(V),1)
+Q =
+msg =
+else
+Q = @
+msg = @printf ' %-8s %s%s\n' "$(1)" "$(notdir $(2))" "$(if $(3), $(3))";
+MAKEFLAGS += --no-print-directory
+submake_extras := feature_display=0
+endif
+
+.DELETE_ON_ERROR:
+
+.PHONY: all
+
+TARGETS = $(patsubst %.bpf.c,%.hidbpf.h, $(wildcard *.bpf.c))
+TARGETS += hid_bpf_progs.h
+
+all: $(TARGETS)
+
+clean:
+ $(call msg,CLEAN)
+ $(Q)rm -rf $(OUTPUT) $(TARGETS)
+
+$(OUTPUT)/%.json: $(OUTPUT)/%.bpf.o | $(BPFTOOL)
+ $(call msg,GEN-SKEL,$@)
+ $(Q)$(BPFTOOL) gen skeleton -L -j $< > $@
+
+%.hidbpf.h: $(OUTPUT)/%.json | $(HID_BPF_CONVERTER)
+ $(call msg,GEN-HIDBPF,$@)
+ $(Q)$(HID_BPF_CONVERTER) build_prog $< -o $@
+
+hid_bpf_progs.h: $(addprefix $(OUTPUT)/,$(patsubst %.bpf.c,%.json, $(wildcard *.bpf.c))) | $(HID_BPF_CONVERTER)
+ $(call msg,GEN-HIDBPF-LIST,$@)
+ $(Q)$(HID_BPF_CONVERTER) build_list $< -o $@
+
+$(OUTPUT)/%.bpf.o: %.bpf.c $(OUTPUT)/vmlinux.h $(BPFOBJ) | $(OUTPUT)
+ $(call msg,BPF,$@)
+ $(Q)$(CLANG) -g -O2 -target bpf $(INCLUDES) \
+ -c $(filter %.c,$^) -o $@ && \
+ $(LLVM_STRIP) -g $@
+
+$(OUTPUT)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
+ifeq ($(VMLINUX_H),)
+ $(call msg,GEN,,$@)
+ $(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+else
+ $(call msg,CP,,$@)
+ $(Q)cp "$(VMLINUX_H)" $@
+endif
+
+$(OUTPUT) $(LIBBPF_OUTPUT) $(BPFTOOL_OUTPUT):
+ $(call msg,MKDIR,$@)
+ $(Q)mkdir -p $@
+
+$(BPFOBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(LIBBPF_OUTPUT)
+ $(Q)$(MAKE) $(submake_extras) -C $(LIBBPF_SRC) \
+ OUTPUT=$(abspath $(dir $@))/ prefix= \
+ DESTDIR=$(LIBBPF_DESTDIR) $(abspath $@) install_headers
+
+ifeq ($(CROSS_COMPILE),)
+$(DEFAULT_BPFTOOL): $(BPFOBJ) | $(BPFTOOL_OUTPUT)
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOL_SRC) \
+ OUTPUT=$(BPFTOOL_OUTPUT)/ \
+ LIBBPF_BOOTSTRAP_OUTPUT=$(LIBBPF_OUTPUT)/ \
+ LIBBPF_BOOTSTRAP_DESTDIR=$(LIBBPF_DESTDIR)/ bootstrap
+else
+$(DEFAULT_BPFTOOL): $(BPFTOOL_OUTPUT)
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOL_SRC) \
+ OUTPUT=$(BPFTOOL_OUTPUT)/ bootstrap
+endif
diff --git a/drivers/hid/bpf/progs/hid_bpf.h b/drivers/hid/bpf/progs/hid_bpf.h
new file mode 100644
index 000000000000..7ee371cac2e1
--- /dev/null
+++ b/drivers/hid/bpf/progs/hid_bpf.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* Copyright (c) 2022 Benjamin Tissoires
+ */
+
+#ifndef ____HID_BPF__H
+#define ____HID_BPF__H
+
+struct hid_bpf_probe_args {
+ unsigned int hid;
+ unsigned int rdesc_size;
+ unsigned char rdesc[4096];
+ int retval;
+};
+
+#endif /* ____HID_BPF__H */
diff --git a/drivers/hid/bpf/progs/hid_bpf_helpers.h b/drivers/hid/bpf/progs/hid_bpf_helpers.h
new file mode 100644
index 000000000000..4c4e63a516b3
--- /dev/null
+++ b/drivers/hid/bpf/progs/hid_bpf_helpers.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* Copyright (c) 2022 Benjamin Tissoires
+ */
+
+#ifndef __HID_BPF_HELPERS_H
+#define __HID_BPF_HELPERS_H
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+
+extern __u8 *hid_bpf_get_data(struct hid_bpf_ctx *ctx,
+ unsigned int offset,
+ const size_t __sz) __ksym;
+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 buf__sz,
+ enum hid_report_type type,
+ enum hid_class_request reqtype) __ksym;
+
+#endif /* __HID_BPF_HELPERS_H */
diff --git a/drivers/hid/bpf/progs/hid_bpf_progs.h b/drivers/hid/bpf/progs/hid_bpf_progs.h
new file mode 100644
index 000000000000..430e0fb47484
--- /dev/null
+++ b/drivers/hid/bpf/progs/hid_bpf_progs.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+/* THIS FILE IS AUTOGENERATED BY build_progs_list.py ! */
+
+#ifndef __HID_BPF_PROGS_H__
+#define __HID_BPF_PROGS_H__
+
+/**
+ * struct hid_bpf_map: custom HID representation of a map
+ */
+struct hid_bpf_map {
+ unsigned int size;
+ unsigned int mmap_sz;
+ char *data;
+};
+
+/**
+ * struct hid_bpf_prog: custom HID representation of a program
+ */
+struct hid_bpf_prog {
+};
+
+/**
+ * struct hid_bpf_object: custom HID representation of a BPF object
+ * @map_cnt: number of maps available in the BPF object
+ * @prog_cnt: number of programs available
+ * @probe: index of the probe program if any (greater than prog_cnt if none)
+ */
+struct hid_bpf_object {
+ struct hid_device_id id;
+ unsigned int map_cnt;
+ struct hid_bpf_map maps[0];
+ unsigned int prog_cnt;
+ struct hid_bpf_prog progs[0];
+ unsigned int probe;
+ unsigned int rodata;
+ unsigned int data_sz;
+ char *data;
+ unsigned int insns_sz;
+ char *insns;
+};
+
+static struct hid_bpf_object hid_objects[] = {
+
+
+ { },
+};
+
+#endif /* __HID_BPF_PROGS_H__ */
+
diff --git a/tools/hid/build_progs_list.py b/tools/hid/build_progs_list.py
new file mode 100755
index 000000000000..8103bc2f433f
--- /dev/null
+++ b/tools/hid/build_progs_list.py
@@ -0,0 +1,231 @@
+#!/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) 2022 Benjamin Tissoires
+
+import argparse
+import json
+import re
+import sys
+
+from pathlib import Path
+
+
+def create_header(fp, max_maps_cnt, max_progs_cnt):
+ print(
+ f"""/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+/* THIS FILE IS AUTOGENERATED BY build_progs_list.py ! */
+
+#ifndef __HID_BPF_PROGS_H__
+#define __HID_BPF_PROGS_H__
+
+/**
+ * struct hid_bpf_map: custom HID representation of a map
+ */
+struct hid_bpf_map {{
+ unsigned int size;
+ unsigned int mmap_sz;
+ char *data;
+}};
+
+/**
+ * struct hid_bpf_prog: custom HID representation of a program
+ */
+struct hid_bpf_prog {{
+}};
+
+/**
+ * struct hid_bpf_object: custom HID representation of a BPF object
+ * @map_cnt: number of maps available in the BPF object
+ * @prog_cnt: number of programs available
+ * @probe: index of the probe program if any (greater than prog_cnt if none)
+ */
+struct hid_bpf_object {{
+ struct hid_device_id id;
+ unsigned int map_cnt;
+ struct hid_bpf_map maps[{max_maps_cnt}];
+ unsigned int prog_cnt;
+ struct hid_bpf_prog progs[{max_progs_cnt}];
+ unsigned int probe;
+ unsigned int rodata;
+ unsigned int data_sz;
+ char *data;
+ unsigned int insns_sz;
+ char *insns;
+}};
+
+static struct hid_bpf_object hid_objects[] = {{
+""",
+ file=fp,
+ )
+
+
+def create_footer(fp):
+ print(
+ """
+ { },
+};
+
+#endif /* __HID_BPF_PROGS_H__ */
+""",
+ file=fp,
+ )
+
+
+def get_bus_group_vid_pid(json_data):
+ path_regex = re.compile(
+ r"b([0-9a-fA-F]{4})g([0-9a-fA-F]{4})v([0-9a-fA-F]{4})p([0-9a-fA-F]{4}).*"
+ )
+ return path_regex.match(json_data["name"])
+
+
+def is_valid_bpf_json(json_data):
+ return get_bus_group_vid_pid(json_data) is not None and json_data["use_loader"]
+
+
+def build_prog_list(args):
+ input_files = [Path(p) for p in args.path]
+ output = args.output
+ max_maps_cnt = 0
+ max_progs_cnt = 0
+ valid_files = []
+
+ for prog in input_files:
+ with open(prog) as f:
+ data = json.load(f)
+ if not is_valid_bpf_json(data):
+ print(f"ignoring {prog.relative_to('.')}", file=sys.stderr)
+ continue
+ generated_name = prog.name.replace(".json", ".hidbpf.h")
+ max_maps_cnt = max(max_maps_cnt, len(data["maps"]))
+ max_progs_cnt = max(max_progs_cnt, len(data["progs"]))
+
+ valid_files.append(generated_name)
+
+ create_header(output, max_maps_cnt, max_progs_cnt)
+
+ valid_files.sort()
+
+ for file in valid_files:
+ print(f'#include "{file}"', file=output)
+
+ create_footer(output)
+
+
+def write_c_data(bytes):
+ outputs = ['"', '']
+ for b in bytes:
+ w = 2 if b == '0x00' else 4
+ if len(outputs[-1]) + w > 78:
+ outputs.append('')
+ if b != "0x00":
+ outputs[-1] += f"\\x{b[2:]}"
+ else:
+ outputs[-1] += "\\0"
+ outputs[-1] += '"'
+ return '\\\n'.join(outputs)
+
+
+def build_c_object(args):
+ input_file = Path(args.path[0])
+ output = args.output
+
+ with open(input_file) as f:
+ data = json.load(f)
+
+ if not is_valid_bpf_json(data):
+ print(f"ignoring {input_file.relative_to('.')}", file=sys.stderr)
+ return
+
+ try:
+ probe_idx = [p["name"] for p in data["progs"]].index("probe")
+ except ValueError:
+ probe_idx = len(data["progs"]) + 1
+
+ try:
+ rodata_idx = [m["ident"] for m in data["maps"]].index("rodata")
+ except ValueError:
+ rodata_idx = len(data["maps"]) + 1
+
+ header_marker = (
+ f"__{input_file.name}__".replace(".", "_")
+ .replace("-", "_")
+ .replace("json", "hidbpf_h")
+ .upper()
+ )
+ m = get_bus_group_vid_pid(data)
+ output.write(
+ f"""/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
+/* THIS FILE IS AUTOGENERATED BY build_progs_list.py ! */
+
+#ifndef {header_marker}
+#define {header_marker}
+
+{{
+ .id = {{
+ .bus = 0x{m.group(1)},
+ .group = 0x{m.group(2)},
+ .vendor = 0x{m.group(3)},
+ .product = 0x{m.group(4)},
+ }},
+ .map_cnt = {len(data['maps'])},
+ .prog_cnt = {len(data['progs'])},
+ .probe = {probe_idx},
+ .rodata = {rodata_idx},
+ .maps = {{""")
+
+ for map in data['maps']:
+ output.write(f"""
+ {{
+ .size = {map['size']},
+ .mmap_sz = {map['mmap_sz']},
+ .data = {write_c_data(map['data'])},
+ }},""")
+
+ output.write(f"""
+ }},
+ .data_sz = {data['data_sz']},
+ .data = {write_c_data(data['data'])},
+ .insns_sz = {data['insns_sz']},
+ .insns = {write_c_data(data['insns'])},
+}},
+
+#endif /* {header_marker} */
+"""
+ )
+
+
+def main():
+ ap = argparse.ArgumentParser(description="custom HID BPF json converter")
+
+ def add_output(parser):
+ parser.add_argument(
+ "-o",
+ "--output",
+ nargs="?",
+ type=argparse.FileType("w"),
+ default=sys.stdout,
+ help="Output file, if not given, use stdout",
+ )
+
+ sp = ap.add_subparsers(help="sub-command help")
+
+ obj_parser = sp.add_parser(
+ "build_prog", help="Convert a JSON into a generic HID BPF header"
+ )
+ obj_parser.add_argument("path", nargs=1, help="Input file.", metavar="PATH")
+ add_output(obj_parser)
+ obj_parser.set_defaults(func=build_c_object)
+
+ list_parser = sp.add_parser(
+ "build_list", help="Convert a list of JSON into a generic HID BPF header list"
+ )
+ list_parser.add_argument("path", nargs="*", help="Input file(s).", metavar="PATH")
+ add_output(list_parser)
+ list_parser.set_defaults(func=build_prog_list)
+
+ args = ap.parse_args()
+ args.func(args)
+
+
+if __name__ == "__main__":
+ main()
--
2.38.1