Recently I've done small research to repurpose overvalued ebpf into something useful and even achieved some modest results. It seems that at least you can use ebpf maps in your old-school native drivers without writing single line of code for ebpf progs. You can ask me - c'mon, there are tons of ways to communicate with driver under linux, just to name few:
- ioctls
- read from driver/write to driver + possible employ polling
- file in kernfs
- futex in shared memory
- io_uring
- netlink like auditd or XFRM do
etc
Let's look at typical situation - your IT crew was bitten by violent adherent of the totalitarian sect of rust ebpf Witnesses and as a consequence you now have hundreds of ebpfs (and no single person who know how this pile of spaghetti code works)
So I made simple POC to show how you can do it. Source of driver& userland test program
Lets dive into gory details and see what other non-obvious advantages can be obtained from such heretical crossbreeding
From userland to kernel
One possible scenario - replace kernel module params. Your userland code creates ebpf map, fills it with params (including binary blobs - for example code for reverse shell, he-he) and then calls driver with ioctl passing file descriptor of ebpf map into driver
From kernel to userland
You could replace with ebpf maps lots of files in /sys or /proc. Really - you could have updated in real-time data in structured binary form and avoid tons of text parsers. Just think about it seriously
And finally lets check how to work with ebpf maps in your driver
Gathering maps
The first thing is to locate necessary maps. I've implemented 3 way to do this
- IOCTL_FROM_FD - by file descriptor returned from bpf_create_map_name. Official way is to call bpf_map_getand it even marked as EXPORT_SYMBOL. However I got
ERROR: modpost: "bpf_map_get" [/home/redp/lkcd/bmc/bmc.ko] undefined!
Well, this is not big deal - we can employ good old symbols lookup - IOCTL_BY_ID. All ebpf maps are stored in tree map_idr, so it just calls idr_find
- IOCTL_BY_NAME. Traverse all nodes in map_idr tree and find by name
CRUD operations
Official way to do this - call functions (sure non-exported) like bpf_map_update_value etc, but they require file descriptor and we can miss it. So we can just skip most of their wrappers logic and call corresponding map->ops methods (don't forget to get necessary RCU lock):
- for insert/update ops->map_update_elem
- for read ops->map_lookup_elem (or even ops->map_lookup_and_delete). See how bpf_map_copy_value implements this
- for delete ops->map_delete_elem, see map_delete_elem
Notifications
Surprisingly hardest part was to notify from driver code in userland that ebpf map was updated. Theoretically their file descriptors support polling - see function bpf_map_poll, it calls map->ops->map_poll method if it presents. Unfortunately the only map type implementing it is BPF_MAP_TYPE_RINGBUF
It was very tempting to patch map->ops with copy of original one and just add your own map_poll method. DO'NT DO THAT!
The main threat here is that your driver can be unloaded at arbitrary moments. There is high probability that for rare events (for example, for events that happen a couple of times year) return address to unloaded code will be present in stack of some thread waiting in poll - even if you revert all your patched maps before driver unloading
We should use polling on some other file descriptors - in my case there is only ebpf map so I decided to use FD of driver. For many maps driver could create bunch of files somewhere in kernfs (for example with kobject_uevent) and return them to userland