Rust virtual machine and JIT compiler for eBPF programs
Rust virtual machine and JIT compiler for eBPF programs
Rust (user-space) virtual machine for eBPF
This crate contains a virtual machine for eBPF program execution. BPF, as in Berkeley Packet Filter, is an assembly-like language initially developed for BSD systems, in order to filter packets in the kernel with tools such as tcpdump so as to avoid useless copies to user-space. It was ported to Linux, where it evolved into eBPF (extended BPF), a faster version with more features. While BPF programs are originally intended to run in the kernel, the virtual machine of this crate enables running it in user-space applications; it contains an interpreter, an x86_64 JIT-compiler for eBPF programs, as well as a disassembler.
It is based on Rich Lane's uBPF software, which does nearly the same, but is written in C.
The crate is supposed to compile and run on Linux, MacOS X, and Windows, although the JIT-compiler does not work with Windows at this time.
This crate is available from crates.io, so it
should work out of the box by adding it as a dependency in your Cargo.toml
file:
[dependencies]
rbpf = "0.4.1"You can also use the development version from this GitHub repository. This
should be as simple as putting this inside your Cargo.toml:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }Of course, if you prefer, you can clone it locally, possibly hack the crate,
and then indicate the path of your local version in Cargo.toml:
[dependencies]
rbpf = { path = "path/to/rbpf" }Then indicate in your source code that you want to use the crate:
extern crate rbpf;The API is pretty well documented inside the source code. You should also be able to access an online version of the documentation from here, automatically generated from the crates.io version (may not be up-to-date with the main branch). Examples and unit tests should also prove helpful. Here is a summary of how to use the crate.
Here are the steps to follow to run an eBPF program with rbpf:
eBPF has been initially designed to filter packets (now it has some other hooks
in the Linux kernel, such as kprobes, but this is not covered by rbpf). As a
consequence, most of the load and store instructions of the program are
performed on a memory area representing the packet data. However, in the Linux
kernel, the eBPF program does not immediately access this data area: initially,
it has access to a C struct sk_buff instead, which is a buffer containing
metadata about the packet—including memory addresses of the beginning and of
the end of the packet data area. So the program first loads those pointers from
the sk_buff, and then can access the packet data.
This behavior can be replicated with rbpf, but it is not mandatory. For this reason, we have several structs representing different kinds of virtual machines:
struct EbpfVmMbuffer mimics the kernel. When the program is run, the
address provided to its first eBPF register will be the address of a metadata
buffer provided by the user, and that is expected to contain pointers to the
start and the end of the packet data memory area.
struct EbpfVmFixedMbuff has one purpose: enabling the execution of programs
created to be compatible with the kernel, while saving the effort to manually
handle the metadata buffer for the user. In fact, this struct has a static
internal buffer that is passed to the program. The user has to indicate the
offset values at which the eBPF program expects to find the start and the end
of packet data in the buffer. On calling the function that runs the program
(JITted or not), the struct automatically updates the addresses in this
static buffer, at the appointed offsets, for the start and the end of the
packet data the program is called upon.
struct EbpfVmRaw is for programs that want to run directly on packet data.
No metadata buffer is involved, the eBPF program directly receives the
address of the packet data in its first register. This is the behavior of
uBPF.
struct EbpfVmNoData does not take any data. The eBPF program takes no
argument whatsoever and its return value is deterministic. Not so sure there
is a valid use case for that, but if nothing else, this is very useful for
unit tests.
All these structs implement the same public functions:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result, Error>This is used to create a new instance of a VM. The return type is dependent of
the struct from which the function is called. For instance,
rbpf::EbpfVmRaw::new(Some(my_program)) would return an instance of struct rbpf::EbpfVmRaw (wrapped in a Result). When a program is loaded, it is
checked with a very simple verifier (nothing close to the one for Linux
kernel). Users are also able to replace it with a custom verifier.
For struct EbpfVmFixedMbuff, two additional arguments must be passed to the
constructor: data_offset and data_end_offset. They are the offset (byte
number) at which the pointers to the beginning and to the end, respectively, of
the memory area of packet data are to be stored in the internal metadata buffer
each time the program is executed. Other structs do not use this mechanism and
do not need those offsets.
// for struct EbpfVmMbuff, struct EbpfVmRaw and struct EbpfVmRawData
pub fn set_program(&mut self, prog: &'a [u8]) -> Result
// for struct EbpfVmFixedMbuff
pub fn set_program(&mut self, prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> ResultYou can use for example my_vm.set_program(my_program); to change the loaded
program after the VM instance creation. This program is checked with the
verifier attached to the VM. The verifying function of the VM can be changed at
any moment.
pub type Verifier = fn(prog: &[u8]) -> Result;
pub fn set_verifier(&mut self,
verifier: Verifier) -> ResultNote that if a program has already been loaded into the VM, setting a new
verifier also immediately runs it on the loaded program. However, the verifier
is not run if no program has been loaded (if None was passed to the new()
method when creating the VM).
pub type Helper = fn (u64, u64, u64, u64, u64) -> u64;
pub fn register_helper(&mut self,
key: u32,
function: Helper) -> ResultThis function is used to register a helper function. The VM stores its
registers in a hashmap, so the key can be any u32 value you want. It may be
useful for programs that should be compatible with the Linux kernel and
therefore must use specific helper numbers.
pub fn register_allowed_memory(&mut self, addrs_range: Range) -> ()This function adds a list of memory addresses that the eBPF program is allowed to load and store. Multiple calls to this function will append the addresses to an internal HashSet. At the moment rbpf only validates memory accesses when using the interpreter. This function is useful when using kernel helpers which return pointers to objects stored in eBPF maps.
// for struct EbpfVmMbuff
pub fn execute_program(&self,
mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub fn execute_program(&self,
mem: &'a mut [u8]) -> Result
// for struct EbpfVmNoData
pub fn execute_program(&self) -> ResultInterprets the loaded program. The function takes a reference to the packet data and the metadata buffer, or only to the packet data, or nothing at all, depending on the kind of the VM used. The value returned is the result of the eBPF program.
pub fn jit_compile(&mut self) -> ResultJIT-compile the loaded program, for x86_64 architecture. If the program is to use helper functions, they must be registered into the VM before this function is called. The generated assembly function is internally stored in the VM.
// for struct EbpfVmMbuff
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8]) -> Result
// for struct EbpfVmNoData
pub unsafe fn execute_program_jit(&self) -> ResultCalls the JIT-compiled program. The arguments to provide are the same as for
execute_program(), again depending on the kind of VM that is used. The result of
the JIT-compiled program should be the same as with the interpreter, but it
should run faster. Note that if errors occur during the program execution, the
JIT-compiled version does not handle it as well as the interpreter, and the
program may crash. For this reason, the functions are marked as unsafe.
This comes from the unit test test_vm_add.
…This comes from the unit test test_jit_ldxh.
…This comes from the unit test test_jit_mbuff and derives from the unit test
test_jit_ldxh.
…This comes from unit test test_vm_block_port.
This example requires the following additional crates, you may have to add them
to your Cargo.toml file.
[dependencies]
rbpf = "0.4.1"
elf = "0.8.0"It also uses a kind of VM that uses an internal buffer used to simulate the
sk_buff used by eBPF programs in the kernel, without having to manually
create a new buffer for each packet. It may be useful for programs compiled for
the kernel and that assumes the data they receive is a sk_buff pointing to
the packet data start and end addresses. So here we just provide the offsets at
which the eBPF program expects to find those pointers, and the VM handles the
buffer update so that we only have to provide a reference to the packet data
for each run of the program.
…Besides passing the raw hexadecimal codes for building eBPF programs, two other methods are available.
The first method consists in using the assembler provided by the crate.
extern crate rbpf;
use rbpf::assembler::assemble;
let prog = assemble("add64 r1, 0x605
mov64 r2, 0x32
mov64 r1, r0
be16 r0
neg64 r2
exit").unwrap();
#[cfg(feature = "std")] {
prinNo open issues yet, or sync has not completed.