Framework
rex-rs/rex avatar
rex-rs/rex

Rex runs Rust kernel extensions where eBPF programs usually go

Rex is a safe and usable kernel extension framework that allows loading and executing Rust kernel extension programs in the place of eBPF.

564 stars27 forksRustGPL-2.0

At a glance

What is it?
Rex is a kernel extension framework that loads Rust programs into the kernel in place of eBPF programs, and the design decision behind it is that safety comes from the Rust compiler rather than from the in-kernel verifier. Programs are written in a safe subset of Rust, compiled to native code, and skip the verifier entirely, which removes the complexity limits and the verifier-specific code shapes that eBPF programmers complain about.
Who is it for?
Adopt Rex if you are writing kernel-level instrumentation and the eBPF verifier has been the constraint, since Rex moves the safety argument to the Rust compiler and gives you the ordinary Rust subset to work in. Skip it if you need to ship something in production today, since the project is at version 2025.08.13 and the build requires a patched Linux tree with submodules, and if your program needs the full eBPF helper surface.
Can I use it commercially?
Yes, with conditions. GPL-2.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 4 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 October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The safety argument moves from the verifier to the compiler

Rex is described as a safe and usable kernel extension framework that allows loading and executing Rust kernel extension programs in the place of eBPF. The contrast is with eBPF-based frameworks such as Aya.

The mechanism is the whole point. Rex programs do not go through the in-kernel verifier. Instead, the programs are implemented in the safe subset of Rust, on which the Rust compiler performs the needed safety checks and generates native code directly.

So the argument about memory safety moves from runtime to compile time. In eBPF, the verifier walks the program before it runs and rejects anything it cannot prove safe, which is a fixed cost paid at load time and a fixed set of rules the program must satisfy. In Rex, the compiler rejects unsafe code before it reaches the kernel, and the kernel does not need to inspect the program at all.

Two consequences follow from that. The first is that the overly restricted verification requirements, and the resulting arcane verification errors, are gone. The second is that there is potentially a better optimization opportunity in the native compiler backend, in other words LLVM, than in the eBPF backend plus the in-kernel JIT approach.

That second point is a real argument and not just a technicality. The eBPF path is a constrained subset compiled to eBPF bytecode and then JIT compiled in the kernel, so the code you end up with is bounded by what the eBPF instruction set can express. The native path goes through the same LLVM backend that Rust code normally uses.

The project is Rust, GPL-2.0 licensed, with the default branch main, the last push on 2026-10-19 and one release, v2025.08.13 from 2025-08-13.

The feature list is what a working eBPF program actually needs

The supported feature list is short, and each entry maps to something an eBPF programmer would otherwise have to arrange separately.

There are five eBPF program types: kprobe, perf_event, tracepoint, xdp and tc. That covers the two ends of the spectrum, from kernel-level observation through to the networking data path, and the fact that the same mechanism serves both is a sign the framework is not built around one use case.

Then there is invocation of eBPF helper functions that are commonly used by these programs. Rex does not replace the helper API, it exposes it, which means an existing mental model of eBPF programs carries over.

Interaction with eBPF maps is supported, so the map types an eBPF program already uses are available.

The fourth entry is the one that most distinguishes it from a plain eBPF port. RAII-style management of kernel resources obtainable by programs, which is the Rust idiom applied to kernel objects: a resource acquired in a function is released when the function returns, including on an early return or a panic. In C-style kernel code that cleanup is manual and easy to get wrong on an error path.

Then cleanup and in-kernel exception handling of Rust runtime panics with call stack traces. A panic in an eBPF program is not a thing, and a panic in a native Rust kernel extension is a serious problem, so the framework handles it rather than leaving it undefined. The entry after that mentions kernel stack, only when CFG cannot be computed statically, and termination safety from a thin in-kernel runtime.

The last entry is bindings and abstractions of kernel data types commonly needed by eBPF programs, which is what makes porting an existing program a translation exercise rather than a rewrite.

The example program is small enough to read in one pass

The example in the documentation implements a kprobe program that attaches to a selected system call and injects an error, specified by errno, to the system call on a process, specified by its pid. The full example, including the loader program, lives under samples/error_injector.

Rust
#![no_std]
#![no_main]

use rex::kprobe::kprobe;
use rex::map::RexHashMap;
use rex::pt_regs::PtRegs;
use rex::rex_kprobe;
use rex::rex_map;
use rex::Result;

#[allow(non_upper_case_globals)]
#[rex_map]
static pid_to_errno: RexHashMap<i32, u64> = RexHashMap::new(1, 0);

#[rex_kprobe]
pub fn err_injector(obj: &kprobe, ctx: &mut PtRegs) -> Result {
    obj.bpf_get_current_task()
        .map(|t| t.get_pid())
        .and_then(|p| obj.bpf_map_lookup_elem(&pid_to_errno, &p).cloned())
        .map(|e| obj.bpf_override_return(ctx, e))
        .ok_or(0)
}

Several things about it are worth noticing.

The program is a no_std and no_main Rust program, which is the shape any kernel-side Rust code takes. The imports come from the rex crate itself: rex::kprobe::kprobe for the program type, rex::map::RexHashMap for the map type, rex::pt_regs::PtRegs for the register context, and rex::Result for the error type.

The map is declared with a procedural macro. The attribute is rex_map on a static of type RexHashMap, and the constructor takes two numbers, which are the map's capacity and flags. The allow attribute for non_upper_case_globals is needed because the map name is written in snake case.

The program function is annotated with rex_kprobe and takes two arguments: a reference to the kprobe object, which is the handle to the attached program, and a mutable reference to the register context. It returns a Result.

The body is ordinary Rust. It gets the current task, maps that to its pid, looks the pid up in the map to find the errno to inject, and overrides the return value of the system call with it. The chain is a sequence of combinators ending in ok_or(0), which is the idiomatic way to turn a sequence of fallible operations into a Result.

That is the argument the project is making with a single screen of code. The shape of the program is the shape of any Rust program, with no verifier constraints shaping the way the logic is written and no helper-function convention to learn.

The BMC comparison is the argument, made with real code

The Why Rex section is the strongest part of the documentation, because it argues the case with a program that exists rather than with a hypothetical.

The complaint about eBPF is stated as a list. Programs may exceed the inherent complexity constraints of static verification. Compilers may not generate verifier-friendly code. And the same logic may need to be implemented in a certain way to please the verifier.

The demonstration is the BPF Memcached Cache, BMC, described as a state-of-the-art extension program for Memcached acceleration. As a complex eBPF program, BMC is forced to be split into several components connected by BPF tail calls and to use awkward loop and branch implementations to please the verifier, none of which are needed in its Rex implementation.

The concrete example is the cache invalidation logic that searches for a SET command in the packet payload. The eBPF version looks like this:

C
// Searches for SET command in payload
for (unsigned int off = 0;
     off < BMC_MAX_PACKET_LENGTH &&  payload + off + 1 <= data_end;
     off++) {
    if (set_found == 0 && payload[off] == 's' &&
        payload + off + 3 <= data_end && payload[off + 1] == 'e' &&
        payload[off + 2] == 't') {
            off += 3;
            set_found = 1;
    }
    ...
}

The explanation of what is wrong with it is precise. The code not only introduces an extra constraint in the loop, off less than BMC_MAX_PACKET_LENGTH, solely for passing the verifier, but also employs repeated boilerplate code to check packet ends against data_end and uses cumbersome logic to match the set string in the packet.

Three separate costs, and each one is a cost the programmer pays rather than the machine. The extra loop bound exists to satisfy the verifier, not to describe the protocol. The repeated data_end checks are manual bounds checking that safe Rust would give you for free. And the character-by-character comparison is the kind of code you write when you cannot slice.

The Rex version of the same logic is:

rust
let set_iter = payload.windows(4).enumerate().filter_map(|(i, v)| {
    if v == b"set " {
      Some(i)
    } else {
      None
    }
});

The claim is that none of those burdens is needed with safe Rust in Rex, which has no complexity limits and provides more freedom on the implementation. The windows and filter_map formulation says what it means and gets on with it, and the bounds checking is handled by the type system rather than by the programmer.

Building it means working with a kernel tree

The build and run section points at a detailed guide, and the repository layout explains what that involves.

The top-level entries are the ones you would expect for a project that compiles code into a kernel: a linux directory, a rust directory, a librex/ directory, and .gitmodules with flake.nix and flake.lock. There is a meson.build at the root, and the CI badge points at a workflow named meson.yml, which is the build system in use. There is also a Cargo.toml and a Cargo.lock with a Cargo workspace whose members are the rex library and the rex-macros package, excluding samples, rust, tools and scripts from the workspace.

So there are two build systems with different jobs. Meson builds the kernel side, and Cargo builds the Rust side, with rex-macros providing the procedural macros the example program uses.

The Rust profile settings in the manifest are worth noting. Both the dev and release profiles set debug to 0 and panic to abort, and the release profile adds lto = true. Panicking to abort in kernel code is the sensible choice, and the framework handles panics through its own in-kernel exception handling with call stack traces.

The samples directory is large and is the best guide to what the framework can do: error_injector, hello, atomic, bmc, electrode, map_test, map_test_2, map_bench, recursive, spinlock_test, spinlock_cleanup_benchmark, startup_overhead_benchmark, syscall_tp, syscount, trace_event, tracex5 and xdp_test. Several of those names are benchmark suites, which tells you the project has been measured rather than only demonstrated.

There is a tools/ directory and a scripts/ directory, and the design documentation sits under docs/, which includes the getting started guide and a document on the Rust subset the framework accepts. That last one is the file to read before writing anything, because it is where the restrictions live.

The project has been presented at three conferences, with talks listed for Open Source Summit North America 2025, USENIX ATC 2025 and Linux Plumbers Conference 2025, which is the kind of visibility a young systems project needs to attract contributors.

Where this fits against eBPF and against a module

The realistic comparison is with eBPF itself, and the honest framing is that Rex and eBPF are not the same kind of tool.

eBPF is in the kernel and is verified at load time. That is why it is safe on a system running untrusted-ish code, why it can be attached to thousands of hooks at once, and why it has become the default for observability. Rex is a framework that compiles Rust to native code and loads it into the kernel, so it is a build-and-boot-time affair rather than a runtime one.

That difference is the deciding factor for most people. If you want to attach a probe to a running system without rebuilding the kernel, eBPF is the answer and Rex is not in the running. If you want to write a program that the eBPF verifier rejects, or you want to write in Rust with the ordinary subset and the ordinary idioms, Rex gives you that at the cost of owning the kernel build.

The other comparison is with a traditional loadable kernel module. A module can also be written in Rust, and Rust for Linux has been working on that for years. The difference is that a module is unrestricted in what it can do, including the parts that are genuinely dangerous, and it has to be trusted. Rex sits in between: it gets the ergonomics of Rust and the memory safety of the compiler, while staying inside the same program types and helper functions that eBPF exposes, so the surface area is bounded by the framework rather than by the programmer's discipline.

The licence is worth a note. Rex itself is GPLv2, and the submodules for Linux, Rust and LLVM in the repository are licensed under their own terms, so a project embedding Rex needs to read three licence files rather than one. The memcached benchmark is MIT licensed.

The honest limitation to weigh is maturity. There is one tagged release, v2025.08.13, the framework covers five program types rather than all of them, and the Rust subset carries documented restrictions. For research, for a controlled fleet, or for a project that has hit the verifier wall and wants out of it, that is a workable trade. For a general-purpose production tool, it is not there yet.

Editorial conclusion

Adopt Rex if you are writing kernel-level instrumentation and the eBPF verifier has been the constraint, since Rex moves the safety argument to the Rust compiler and gives you the ordinary Rust subset to work in. Skip it if you need to ship something in production today, since the project is at version 2025.08.13 and the build requires a patched Linux tree with submodules, and if your program needs the full eBPF helper surface. Verify first which of the five program types you need, that you can accept building the kernel from source, and which parts of the Rust subset your program uses, since the documentation points at a dedicated document for the restrictions.

Frequently asked questions

What is Rex and how does it differ from eBPF?

Rex is a safe and usable kernel extension framework that allows loading and executing Rust kernel extension programs in the place of eBPF. Rex programs do not go through the in-kernel verifier, they are implemented in the safe subset of Rust, and the Rust compiler performs the safety checks and generates native code directly. This avoids the restricted verification requirements and their arcane errors, and may give better optimization in the native compiler backend than the eBPF backend plus in-kernel JIT.

Which program types and features does Rex support?

Five eBPF program types are supported: kprobe, perf_event, tracepoint, xdp and tc. Also supported is invocation of the eBPF helper functions commonly used by these programs, interaction with eBPF maps, RAII-style management of kernel resources, cleanup and in-kernel exception handling of Rust panics with call stack traces, kernel stack where CFG cannot be computed statically with termination safety from a thin in-kernel runtime, and bindings and abstractions of common kernel data types.

Why does the Memcached cache example look so different in Rex?

The BPF Memcached Cache had to be split into components connected by tail calls and use awkward loop and branch logic to satisfy the verifier, including an extra loop bound and repeated manual data_end checks that exist only to pass verification. In Rex the same search for a SET command in the payload uses windows and filter_map with no complexity limit, and the bounds checking is handled by the compiler rather than by the programmer.

Official sources

  1. Issues
  2. License: GPL-2.0
  3. README
  4. Releases
  5. rex-rs/rex on GitHub
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/rex-rs-rex.svg)](https://hysenlabs.com/projects/rex-rs-rex)

Was this useful?