Contributed by Ryan Huang, University of Michigan.
This work is supported by the eBPF FoundationĀ Academic Research Grants.
This is the second blog post in our series on the ePass project. In our first post, we introduced the motivation behind ePass: the verifier relies on static analysis, which needs to be necessarily conservative for safety but this leads to semantically valid programs being rejected; at the same time, dynamic properties such as execution limits or helper arguments validity are fundamentally difficult to enforce through static verification. ePass addresses this tension by introducing a new safety model for eBPF: verifier-cooperative transformation, where the verifier remains the central gatekeeper but is enhanced with systematic runtime checks instrumented through a set of transformation passes. We realize this model in the ePass framework. ePass lifts the eBPF bytecode into an SSA-based intermediate representation (IR), applies transformation passes that insert targeted runtime checks or repair verifier-triggered rejections, and compiles the result back to bytecode. Importantly, ePass re-verifies the transformed program before it runs, preserving the original verifier safety properties.
Since that post, we have been progressing on several fronts: we presented ePass to the Linux kernel community at the Linux Plumbers Conference, undertook a major redesign of the ePass architecture informed by that engagement, pushed the pass model beyond safety checks into genuinely new programming capabilities, and expanded our evaluation. This post shares these updates.
Presenting ePass at Linux Plumbers
In December 2025, our project’s student lead, Yiming Xiang, presented ePass at the Linux Plumbers Conference. The talk walked through the design of the ePass IR, the pass-based compiler framework, the supervisor, and sample passes, along with our evaluation on real-world programs. Presenting to an audience of kernel and eBPF developers was very helpful to validate the project and get feedback. The discussions, particularly on how a transformation stage should interact with the kernel, and how ePass compares with other efforts that add runtime enforcement to eBPF, shaped the architectural change described below.
Architecture Change
The most significant change since our last update is where ePass runs. In our earlier prototype, the transformation logic, the IR, the lifter, the passes, and the supervisor that coordinates them resided inside the kernel, next to the verifier. That design made it easy for passes to consume the verifier’s analysis, but it also meant a non-trivial amount of compiler code living in the kernel, which raised concerns about maintainability and attack surface.
The redesigned ePass follows a user-kernel cooperative architecture with three components:
- Core, a compact compiler framework that runs entirely in user space and provides APIs to lift, transform, and compile eBPF bytecode.
- Supervisor, also in user space, implemented in libbpf and bpftool. It extends the eBPF loading path, selects passes according to administrator-approved options, invokes the core, and consumes verifier results.
- Recorder, a minimal kernel component that records selected verifier information during verification and exports it to user space on demand.

Figure 1. Overview of the redesigned ePass architecture. The supervisor and core now run in user space; the kernel side is reduced to a minimal recorder.
The key enabler is the Verifier Information map (VI map), which maps instruction indices to the verifier’s analysis results, such as register ranges and types at specific instructions. The recorder exports this map so that user-space passes can reason about the verifier’s conclusions, deciding where runtime checks or repairs are needed, without embedding the transformation engine in the kernel.
This split gives us the best of both worlds. Pure user-space instrumentation, e.g., with LLVM, cannot access verifier information, and general-purpose compilers lack awareness of subtle verifier constraints. Pure in-kernel transformation, on the other hand, bloats the kernel and is hard to develop and debug. With the new architecture, the kernel-side component of ePass shrinks to roughly a hundred lines, the richer rewriting logic lives where it is easiest to debug, and the framework depends only on the eBPF ISA and helper IDs, making it adaptable across kernel versions.
Crucially, the security model is unchanged: the verifier remains the sole authority for deciding whether transformed bytecode may run. Every transformed program is re-verified, so a bug in ePass or in a pass results in a verifier rejection, not unsafe execution. Administrators install and configure the available passes; regular users can only select from those pre-defined passes and cannot add arbitrary transformation logic. Unlike approaches that add a new runtime and an external watchdog process, ePass has no long-lived runtime component. Its inserted checks can only narrow behavior at runtime, not broaden it past what the verifier accepted.
How Transformation Passes Run
The redesign also clarified when passes run. ePass passes now fall into two categories:
Multi-Round (MR) passes respond to verifier-reported errors. When the verifier rejects a program, the recorder exports the error and related verifier information; ePass checks whether an MR pass can resolve the rejection, runs it in the user-space core, and resubmits the program for verification. Because the verifier may report multiple errors at different locations, this loop can repeat until the program passes or a retry limit is reached. MR passes are the main tool for repairing false rejections.
Final-Round (FR) passes run once, after static verification succeeds, in a predefined order. They typically add runtime enforcement for dynamic properties or optimize code. After FR passes complete, the program goes through the verifier one final time before loading.
This MR/FR structure is what lets a single framework serve two distinct roles, repairing rejections through verifier feedback loops and layering runtime enforcement onto already-accepted programs.
Beyond Repairs: Flexible Data Structures in eBPF
Our first post focused on passes that repair rejections and strengthen safety. ePass’s pass model, however, also enables genuinely new programming capabilities. A representative example is the heap pass, which brings pointer-based data structures, which is long a pain point in eBPF.
To ensure verifiability, eBPF restricts pointer usage, and the verifier cannot reason about structures like linked-list traversal. Advanced applications such as custom CPU schedulers and cache extensions have run into this wall: red-black trees are infeasible, and existing workarounds add kernel runtime or helpers that raise security and maintenance concerns.
ePass takes a different route, built on a new IR mechanism called eCall: a virtual, helper-call-like instruction whose semantics are implemented entirely within ePass passes and expanded into verifier-friendly native instructions during transformation. The heap pass introduces two eCalls, emalloc(size) and efree(ptr), which emulate heap allocation on top of a software heap backed by two ordinary eBPF maps (one for metadata, one for contents). Programs perform pointer arithmetic on the returned indices, and the pass rewrites raw memory operations into map lookups and updates with range checks. The result: code written with intuitive pointer-based data structures compiles into valid eBPF bytecode that respects all verifier constraints. A C header provides ready-to-use malloc and free wrappers, so the developer experience stays familiar.

Figure 2. The heap pass in action: a linked list written in C with familiar malloc/free (left) compiles to bytecode whose allocation calls are lifted into eCalls in the ePass IR and then expanded by the heap pass into verifier-friendly map operations (right).
Why is this useful? Pointer-based data structures are what advanced eBPF applications keep running into. Custom CPU schedulers built on sched_ext want runqueues and priority structures; cache-management extensions want trees and lists to track objects. Today those projects either give up on the data structure, push the logic into a kernel module, or adopt a separate runtime, each with its own cost. By expressing heap allocation as ordinary eBPF maps plus verifier-approved checks, the heap pass offers a path to these structures that stays inside the existing eBPF safety model and needs no new kernel machinery. We see it as the first of a family of expressiveness-oriented passes.
Notably, the heap allocator is implemented entirely in eBPF bytecode via ePass passes. There are no changes to the bpf system call, the verifier, or the JIT compiler. We used the heap pass to implement linked lists and binary search trees and compared them against equivalent implementations in kernel modules and KFlex. On single-threaded microbenchmarks (64K-node structures), the ePass versions were competitive, often faster, largely thanks to our deliberately simple fixed-size block allocator:
Figure 3. Linked list operation latency (64K operations) for kernel module (KM), KFlex, and ePass implementations. Each group is normalized to ePass; bar labels show absolute latency.
Figure 4. Binary search tree operation latency (64K operations) for kernel module (KM), KFlex, and ePass implementations. Each group is normalized to ePass; bar labels show absolute latency. These results demonstrate feasibility rather than a universal performance advantage.
Smarter Instruction Limits
We also refined how ePass handles the verifier’s instruction limit, a common source of confusing rejections. Developers today resort to workarounds like sprinkling bpf_repeat macros over every loop or splitting code via tail calls, the sched_ext scheduler framework alone uses bpf_for and bpf_repeat 260 times.
The improved loop counter pass automates this using verifier feedback. It first instruments loops with a configurable temporary bound and lets the verifier analyze the instrumented program. Loops that never reach the threshold during verification get their counters removed; for the loops that the verifier actually drove to the threshold, ePass redistributes the remaining instruction budget to derive final bounds. Compared with manual annotation, this avoids hard-coded bounds tied to verifier internals, produces more accurate bounds, and instruments only the loops that contribute to verifier blowup.
The embedded counter also pays off at runtime. In a head-to-head test with a dynamic loop that exceeds the runtime instruction limit, ePass terminated the program at about 20 ms, immediately upon the counter reaching the limit. In comparison, a watchdog-based approach had not terminated it within a 50 ms timeout. For programs with loops that are safe but rejected by the verifier (single, nested, and branching loop structures), the loop counter pass made all of them acceptable.
Evaluation Highlights
ePass currently includes 14 passes: 6 for flexibility, 5 for safety, and 3 for optimization. ePass is implemented in roughly 12K lines of C, of which the kernel-side recorder is only 112 lines. Beyond the results we shared last time, several analyses are added or refined:
- How ePass compares. We categorized our collection of real-world false-rejected programs by root cause: missing type information, inaccurate limit calculation, arithmetic derivation failures, and range-analysis limitations. We compared ePass against KFlex and BCF, a recent formal-verification framework. ePass resolves 21 of 23 programs across all four categories, while KFlex resolves 2 and BCF 19. BCF has stronger verification capability on analysis limitations, but it operates purely at the verification level: it cannot repair problems introduced by the LLVM toolchain, nor provide the transformation and runtime-enforcement capabilities that enable, for example, flexible data structures.
- Load time and compilation speed. We profiled the full loading pipeline: initial verification, transformation, and re-verification. We found that for complex programs, the verifier itself dominates; the ePass compilation stage is fast, achieving up to 91x speedup over compiling through LLVM’s llc for small programs and 6x for large ones.
- Pass-writing effort. Every implemented pass takes fewer than 150 lines of code; the MSan pass is 77 lines and the masking pass just 44. This validates a core goal from our roadmap: developing a typical new pass should take only tens of lines.
- Optimization. The optimization passes reduce the size of 32% of programs already optimized by LLVM at O2, shrinking them by 7% after running the passes.
Mitigating CVEs
A key benefit of verifier-cooperative runtime enforcement is to close safety gaps due to verifier bugs or vulnerabilities. We analyzed the bug descriptions and patches for eBPF-related CVEs from 2020 to 2024 and identified 19 that our implemented passes can mitigate. They fall into three exploit patterns, each addressed by a different set of passes:
| Exploit pattern | What goes wrong at runtime | Mitigating ePass passes | # CVEs |
|---|---|---|---|
| Invalid memory access | Uninitialized stack reads, and out-of-bounds reads/writes to the stack and eBPF maps | MSan, ASan | 10 |
| Inference error | Runtime values diverge from the ranges the verifier statically inferred, letting a malicious program steer the kernel | Helper validation, Runtime validation | 7 |
| Kernel denial-of-service | Infinite or excessively long programs (beyond the 1M-instruction limit) that the verifier failed to bound | Counter | 2 |
Table 1. eBPF CVEs mitigated by ePass, grouped by exploit pattern.
For CVEs with publicly available proof-of-concept exploits we ran a controlled experiment: revert the affected kernel component to its vulnerable version, launch the real exploit using a malicious eBPF program, and then confirm that the relevant ePass pass neutralizes it. Five CVEs have public PoC exploits, and ePass mitigated all five. For the remaining cases, where no public exploit exists, we report reasoned coverage based on the bug’s root cause and the guarantee provided by the corresponding pass.
Take the inference-error class as a concrete example. The verifier reasons about each register’s type and value using an abstract model, but once a program is admitted, nothing re-checks that the actual runtime value matches what that model assumed. CVE-2022-23222, which is a local privilege-escalation bug in the bpf verifier affecting kernels through 5.15.14 that exploits exactly this gap. The verifier mishandles pointers of the *_OR_NULL family, which may be either a valid pointer or NULL. In the published exploit, a program obtains such a pointer (e.g., via bpf_ringbuf_reserve), copies it to a second register before performing the NULL check, and then applies pointer arithmetic. The verifier’s NULL-check tracking concludes that both registers are scalar zero, so it permits arithmetic and memory accesses it should have rejected. At runtime the value is actually a kernel pointer, giving the attacker out-of-bounds and ultimately arbitrary kernel read/write, enough to zero out a process’s credentials and obtain root.
This is precisely the kind of deep verifier-inference gap that motivates pairing static verification with independent runtime enforcement. ePass’ helper-validation and runtime-validation passes insert lightweight runtime guards around the helper calls and pointer operations involved, checking the actual value against the constraints the verifier was meant to enforce, such as confirming a maybe-NULL pointer is genuinely validated before it is used, and safely terminating the program if the check fails. Because each guard is just a few ordinary eBPF instructions, it survives re-verification like any other code and adds negligible overhead, while catching the type-confused access at runtime even though it slipped past static verification.
ePass depends on the verifier’s information, so it can fall short when the verifier itself infers an incorrect range that a pass then reuses, when a vulnerability lives in kernel components outside the observable eBPF execution state, or for interpreter-specific flaws. Where a class of bugs lies beyond a pass’s reach, ePass still offers a pragmatic stopgap: an administrator can deploy a pass that disables the suspect operation until an official kernel fix lands.
Explore the Repository
Beyond the research results, we have been investing in making the ePass codebase something you can pick up and use today, with continuous-integration builds and documentation covering the IR specification, testing, and contribution guidelines.
Try ePass without touching the kernel. The ePass core compiles both into the kernel and as a user-space library. The repository ships a modified libbpf (plus patched bpftool, xdp-tools, and the Falco eBPF library) that runs ePass on a program right before it is loaded, so you can experiment with passes on an unmodified kernel. Just set a couple of environment variables:
```bash
sudo LIBBPF_ENABLE_EPASS=1 LIBBPF_EPASS_POPT="msan" \
bpftool prog load test.o /sys/fs/bpf/test
```
A standalone epass CLI can also transform and pretty-print programs offline (epass read prog.o), which we use extensively for debugging. See the user-space testing guide for full setup instructions.
Anatomy of a pass. To showcase the conciseness of passes, below is a simple example built-in division-by-zero guard pass (49 lines). It walks the control-flow graph collecting division instructions whose divisor is not a constant, then splits the basic block before each one and inserts a branch: if the divisor is zero, jump to a block that safely terminates the program.
```c
array_for(pos, fun->reachable_bbs) { /* collect DIV insns */
list_for_each_entry(insn, &bb->ir_insn_head, list_ptr)
if (insn->op == IR_INSN_DIV)
bpf_ir_array_push(env, &div_insns, &insn);
}
array_for(pos2, div_insns) { /* guard each one */
struct ir_basic_block *new_bb = bpf_ir_split_bb(env, fun, prev, INSERT_BACK);
struct ir_basic_block *err_bb = bpf_ir_create_bb(env, fun);
bpf_ir_create_throw_insn_bb(env, err_bb, INSERT_BACK);
bpf_ir_create_jbin_insn(env, prev, insn->values[1],
bpf_ir_value_const32(0), new_bb,
err_bb, IR_INSN_JEQ, IR_ALU_64, INSERT_BACK);
}
```
Notice what is absent: no register allocation, no instruction encoding, no stack management, no control-flow patching at the bytecode level. The IR APIs handle all of it, and the IR checker plus mandatory re-verification catch any mistakes.
An open corpus of falsely rejected programs. The repository also curates the real-world false-rejected programs from our evaluation, covering problematic LLVM-generated bytecode, missed bound checks, instruction-limit overruns, and loop constraints the verifier cannot infer, alongside a regression test suite that includes programs as large as Falco’s. We hope this corpus is useful to others studying verifier usability, and it is exactly where community contributions can have immediate impact: every new rejected program helps us decide which pass to build next.
Roadmap
This cycle’s work focused on improving ePass to be well-suited for real-world adoption, including a dramatically smaller kernel footprint, and capabilities that go beyond repairing rejections to expanding what eBPF programs can express. Looking ahead, several items are on our list:
- Broader program support. We are adding handling for bpf-to-bpf function calls, which will let ePass cover a wider range of real-world programs, and resolving the remaining cases involving tail calls.
- Scaling up testing. We are extending the test suite toward the largest real workloads, including the full set of Falco’s eBPF programs, so that pass bugs are caught early on realistic code.
- Growing the pass library. The heap pass points toward a family of expressiveness-oriented passes; we want to cover more of the data structures and patterns that advanced applications need.
- Stronger security passes. Building on the runtime checks that already mitigate 19 CVEs, we plan to broaden coverage of the exploit patterns we track and add passes for emerging classes of vulnerabilities. A particularly useful direction is rapid-response passes that let administrators neutralize a newly disclosed CVE, by validating the affected values or disabling the suspect operation, until an official kernel fix is available.
- A policy framework for administrators. Rather than enabling passes one by one, operators will select curated profiles that map to a vetted set of passes, making ePass easier for distributions and platform teams to adopt.
- Usability. We will keep refining the IR APIs, documentation, examples, and tutorials so that writing a new pass stays a tens-of-lines exercise.
Get Involved
ePass is open sourced at https://github.com/OrderLab/ePass.
We welcome feedback and suggestions. If you have eBPF programs that are incorrectly rejected by the verifier, rely on workarounds for programmability restrictions, would benefit from richer data structures in eBPF, require instrumenting runtime checks for security and dynamic policies, we would love to hear about them! The user-space libbpf path above makes it easy to try ePass on them without kernel changes.
We thank the eBPF Foundation for supporting this work, and the Linux Plumbers audience for the thoughtful questions and feedback. The conversation convinced us that verifier-cooperative runtime enforcement can become a practical part of the eBPF ecosystem.
The ePass project team is as follows:
- Yiming Xiang, PhD student, The University of Texas at Austin
- Wanning He, PhD student, University of Michigan
- Mehbubul Hasan Al-Quvi, PhD student, Northeastern University
- Emmett Witchel, Professor, The University of Texas at Austin
- Ryan Huang, Associate Professor, University of Michigan