Open-source project
qmonnet/rbpf avatar
qmonnet/rbpf

rbpf: running eBPF programs in user space, with an interpreter, a JIT and a disassembler

Rust virtual machine and JIT compiler for eBPF programs

1,136 stars382 forksRustApache-2.0

At a glance

What is it?
rbpf is a Rust crate that executes eBPF bytecode outside the kernel, providing an interpreter, an x86_64 JIT compiler and a disassembler, and it offers four virtual machine flavours that differ in how the program is handed its packet data. It is based on Rich Lane's uBPF, it supports no_std builds, and the JIT does not work on Windows.
Who is it for?
rbpf fits a Rust project that needs to run eBPF bytecode it did not compile itself, for example to execute a packet filter, to replay a captured program or to test eBPF logic without loading a kernel module, and that wants the same crate for an interpreter and a JIT. It is the wrong choice if you need the full range of eBPF hooks, since the crate covers the packet filtering model rather than kprobes, and the wrong choice on Windows if you need the JIT.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 157 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • qmonnet/rbpf1,136 ★ · +0
Star history
Repository2026-10-192026-11-07
qmonnet/rbpf1.1k1.1k

0 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Why run eBPF outside the kernel at all

rbpf is a Rust, user-space virtual machine for eBPF, and the crate contains a virtual machine for eBPF program execution.

The history in the README explains what eBPF is and why a user-space version is useful. 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.

So the original purpose was performance, and the original mechanism was to run a small program in the kernel to decide what to copy out. The user-space version exists for a different set of reasons. If the bytecode is portable and the virtual machine is portable, then a program written for the kernel can run somewhere else, and the somewhere else can be a test, a fuzzer, a userspace network stack, or a host that has no kernel support for the hook you want.

The crate therefore contains three things: an interpreter, an x86_64 JIT compiler for eBPF programs, and a disassembler. The disassembler is the one that is easy to overlook and is arguably the most useful for a developer, because it lets you read back the bytecode of a program you did not write, which is exactly what you need when a filter arrives as an opaque binary blob.

The crate is based on Rich Lane's uBPF software, which does nearly the same thing but is written in C. The licence is dual, Apache-2.0 and MIT, the primary language is Rust, and the crate is version 0.4.1 with the 2024 edition. The repository is not archived, the last push was on 2026-06-03, and there are no GitHub releases, so crates.io is the version reference.

The topics name the scope honestly: assembler, bpf, ebpf, ebpf-programs, interpreter, jit-compiler, packet-filtering and rust. There is no mention of kprobes or tracing, which is the first thing to check against your use case.

The four virtual machine flavours, and what they change

The API section explains something that is the real design of the crate, and it is worth reading because it is not obvious from the name.

eBPF was initially designed to filter packets, and while it has other hooks in the Linux kernel such as kprobes, those are not covered by rbpf. As a consequence, most of the load and store instructions of a packet filter program operate on a memory area representing the packet data. In the Linux kernel, though, the eBPF program does not immediately access that data area. Initially it has access to a C struct sk_buff, a buffer containing metadata about the packet, including the memory addresses of the beginning and the end of the packet data area. The program first loads those pointers from the sk_buff, and only then can it access the packet data.

That behaviour can be replicated with rbpf, but it is not mandatory, and the four virtual machine structs are the four answers.

EbpfVmMbuffer mimics the kernel. When the program runs, the address provided to its first eBPF register is 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. So you build the sk_buff-like structure yourself and the program behaves exactly as it would in the kernel.

EbpfVmFixedMbuff exists to save you the effort. It enables the execution of programs created to be compatible with the kernel without you having to handle the metadata buffer manually. It has a static internal buffer passed to the program, and you indicate the offset values at which the program expects to find the start and the end of the packet data. On calling the function that runs the program, whether JIT compiled or not, the struct automatically updates the addresses in that static buffer at the appointed offsets for the data the program is called upon.

EbpfVmRaw is for programs that want to run directly on packet data. No metadata buffer is involved, and the eBPF program directly receives the address of the packet data in its first register. The README says this is the behavior of uBPF.

EbpfVmNoData takes no data at all. The program takes no argument and its return value is deterministic. The author is candid that he is not sure there is a valid use case for it, but that if nothing else it is very useful for unit tests, and that is exactly right, because a program with no input is trivially testable.

All four implement the same public functions, with the return type depending on the struct you call new on.

```rust,ignore // called with EbpfVmMbuff:: prefix pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuffer<'a>, Error>

// called with EbpfVmFixedMbuff:: prefix pub fn new(prog: &'a [u8], data_offset: usize, data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>

// called with EbpfVmRaw:: prefix pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>

// called with EbpfVmNoData:: prefix pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error> ```

So the choice of flavour is a choice about the calling convention of your program, and picking the wrong one produces a program that reads the wrong memory rather than an error.

The verifier, and the fact that it is not the kernel verifier

The API section ends with a sentence that any user should stop and read. When a program is loaded, it is checked with a very simple verifier, nothing close to the one for the Linux kernel. Users are also able to replace it with a custom verifier.

This is the single most important limitation in the crate, and it is a security statement, not a correctness one. The Linux kernel verifier is the reason an eBPF program cannot crash the kernel: it proves, before the program runs, that the program will terminate, that every memory access is in bounds, and that the program obeys the privilege model. rbpf's verifier does not do that, so a malformed or malicious program running under rbpf can do whatever the process is allowed to do.

That is acceptable in exactly the situation rbpf is designed for, and unacceptable in a different one. If you are running your own filter, written by you, on data you control, the kernel verifier is redundant. If you are running a program that arrived from somewhere else, over a network, from a user, or from a shared library, then the absence of the kernel verifier means you are trusting the producer of that bytecode with the privileges of your process.

So the mitigation the README offers, replacing the verifier with your own, is not an advanced feature, it is a requirement in that case. And the JIT makes it more important, not less, because a program that passes an interpreter can still misbehave when compiled to native code, and because a JIT has to emit machine code for whatever the program does.

The dependency list explains what the interpreter is built on. The crate depends on combine at 4.6 with default-features off, and combine is a parser combinator library, which is how an eBPF bytecode decoder is naturally written. byteorder at 1.5 and log at 0.4 are also there with default features off, and the comment above them explains why, that the default features, std, are disabled so that the dependencies do not pull in the standard library when the crate is compiled for no_std.

That last point is a real feature rather than a detail. An eBPF virtual machine in a no_std context is useful in embedded and in kernel-adjacent tooling, where pulling in the standard library would defeat the point, and the optional libc dependency is only enabled by the std feature.

The Cargo.toml also shows the JIT is not the only option. There is a cranelift feature that pulls in cranelift-codegen, cranelift-frontend, cranelift-jit, cranelift-native and cranelift-module at 0.127, which is a second JIT backend, the default one being the hand written x86_64 compiler.

The no_std build and the two JIT backends

The Cargo.toml has a detail that changes who can use the crate, and it is easy to miss because it sits in a comment rather than in a field name.

The default features are std, and the comment above the dependency list explains that the default features, std, are disabled so that the dependencies do not pull in the standard library when the crate is compiled for no_std. So byteorder, log and combine are all declared with default-features = false, and the std feature is what turns on libc and combine's own std feature.

A no_std eBPF virtual machine is a genuinely useful thing and not a hypothetical. The tools that need one are the ones running in constrained environments or close to the kernel, where pulling in the standard library is either impossible or undesirable because the crate is going to be used to inspect or emulate kernel behaviour rather than to be a normal application.

The JIT side has two backends, and the feature flags make the choice explicit. The default path is the hand written x86_64 compiler, and there is a cranelift feature that enables the alternative: cranelift-codegen, cranelift-frontend, cranelift-jit, cranelift-native and cranelift-module, all at 0.127.

Why would you want Cranelift instead of the hand written compiler? The usual reasons. Cranelift is a production quality code generator maintained by the Wasmtime project, so it gives you a JIT backend with more targets than x86_64 and with a compiler that is tested on a scale the hand written one cannot match. The cost is five dependencies, and the benefit is portability if you need to JIT on something that is not x86_64. The hand written compiler stays the default because it has no dependencies and the x86_64 case is the common one.

There is also hashbrown at 0.16 with default-features off and the default-hasher feature enabled, which is the hash map used inside the VM, and its presence in the always-on dependency list tells you the interpreter needs a map at runtime rather than only at compile time.

The packaging directives show what ends up in the published crate, and they are worth reading for anyone who wants to vendor it. The include list covers src, examples, tests, bench, the licence files and Cargo.toml. The bench directory is included even though it is not in the repository listing shown here, and the examples and tests are shipped so that cargo test works on the published crate, which is the same reasoning you see in other Rust projects that care about their downstream packagers.

Using the crate, and the parts of eBPF it does not reach

The dependency section shows the three ways to get the crate, and the middle one is worth a word. From crates.io it is a version string:

toml
[dependencies]
rbpf = "0.4.1"

The development version from the GitHub repository is a git dependency:

toml
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }

And a local clone, possibly hacked, is a path dependency:

toml
[dependencies]
rbpf = { path = "path/to/rbpf" }

In the source you then indicate that you want to use the crate:

```rust,ignore extern crate rbpf; ```

The four steps to run a program are short and are the whole workflow. Create a virtual machine, passing the eBPF program to the constructor. If you want to use some helper functions, register them into the virtual machine. If you want a JIT compiled program, compile it. Then execute it, either by running the interpreter or by calling the JIT compiled function.

The helper registration step is the one that matters for anything beyond a toy. A real packet filter calls functions the VM does not know about, and rbpf lets you register them. That is the extension point that turns this from a bytecode runner into something you can actually run a useful filter on.

The examples directory shows what that looks like in practice, and the file names are a good index of the capabilities. There is date.rs, which is a program with no data and therefore runs under EbpfVmNoData. There is disassemble.rs, which uses the disassembler on a program. There is to_json.rs. There is load_elf.rs, with a matching C source and a compiled object file, load_elf__block_a_port.c and load_elf__block_a_port.o, which is a port of the Linux samples' block_a_port filter. And there is rbpf_plugin.rs, which is the example that shows how to register a helper function, since a plugin that cannot call anything is not much use. There is also an allowed-memory.rs example with an allowed-memory directory, which is about the memory the program is permitted to touch.

The dev dependencies also tell you what the test suite covers: elf at 0.8.0 for parsing ELF object files, json at 0.12 and hex at 0.4.3 for the to_json and disassemble paths.

The platform story is the last thing. 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. So on Windows you have the interpreter and the disassembler, and the JIT, which is the performance path, is unavailable.

Editorial conclusion

rbpf fits a Rust project that needs to run eBPF bytecode it did not compile itself, for example to execute a packet filter, to replay a captured program or to test eBPF logic without loading a kernel module, and that wants the same crate for an interpreter and a JIT. It is the wrong choice if you need the full range of eBPF hooks, since the crate covers the packet filtering model rather than kprobes, and the wrong choice on Windows if you need the JIT. Before adopting, read the caveats section, because the built-in verifier is described as very simple and nothing close to the Linux kernel one, and pick the virtual machine flavour that matches how your program expects its data.

Frequently asked questions

What is rbpf and what does it provide?

It is a Rust crate for executing eBPF programs in user space, containing an interpreter, an x86_64 JIT compiler for eBPF programs and a disassembler. It is based on Rich Lane's uBPF software, which does nearly the same but is written in C.

What are the different rbpf virtual machine types for?

They differ in how the program receives its packet data. EbpfVmMbuffer mimics the kernel and hands the program a metadata buffer with pointers to the packet data, EbpfVmFixedMbuff does that automatically using a static internal buffer and offsets you supply, EbpfVmRaw passes the packet data address directly in the first register like uBPF does, and EbpfVmNoData passes nothing and is useful for unit tests.

Is the rbpf verifier as strict as the Linux kernel verifier?

No. The README says that when a program is loaded it is checked with a very simple verifier, nothing close to the one for the Linux kernel, and that users are able to replace it with a custom verifier. If you run bytecode you did not write, you should supply a stricter verifier.

Does rbpf work on Windows?

The crate is supposed to compile and run on Linux, MacOS X and Windows, but the JIT compiler does not work with Windows at this time, so on Windows you have the interpreter and the disassembler rather than the compiled path.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. qmonnet/rbpf on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/qmonnet-rbpf.svg)](https://hysenlabs.com/projects/qmonnet-rbpf)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/qmonnet-rbpf.svg)](https://hysenlabs.com/projects/qmonnet-rbpf)

Was this useful?