# How bpftime Performs Safety Verification for eBPF Programs: Architecture and Implementation

> Discover how bpftime ensures eBPF program safety with static analysis and thread-local verification leveraging the Linux kernel's ebpf-verifier. Learn about memory safety and termination guarantees.

- Repository: [eunomia-bpf/bpftime](https://github.com/eunomia-bpf/bpftime)
- Tags: internals
- Published: 2026-03-01

---

**bpftime leverages a thin C++ wrapper around the Linux kernel's ebpf-verifier library to statically analyze eBPF bytecode before execution, ensuring memory safety and termination guarantees through thread-local platform specifications and explicit helper registration.**

The bpftime project provides a userspace runtime for eBPF programs, requiring robust safety verification to prevent unsafe memory operations and ensure program termination. By wrapping the kernel's proven ebpf-verifier library, bpftime delivers static analysis capabilities identical to the Linux kernel while allowing extensible helper and map definitions through a thread-local configuration system.

## The bpftime Safety Verification Architecture

### Integration with the Kernel ebpf-verifier Library

At the core of bpftime's verification system is the `ebpf_verify_program` function from the upstream ebpf-verifier library—the same static analyzer used by the Linux kernel. The wrapper implementation in [`bpftime-verifier/src/bpftime-verifier.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/src/bpftime-verifier.cpp) (lines 66-69) marshals the program bytecode and platform metadata into the format expected by the kernel verifier, enabling bpftime to inherit the kernel's rigorous safety checks without reimplementing the analysis logic.

### Thread-Local Configuration for Isolated Verification

bpftime implements thread-local storage for verification context to support concurrent safety checks with isolated helper and map configurations. The `set_available_helpers`, `set_non_kernel_helpers`, and `set_map_descriptors` functions store data in `thread_local` containers (defined in [`bpftime-verifier/include/bpftime-verifier.hpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/include/bpftime-verifier.hpp) and implemented in [`bpftime-verifier/src/platform-impl.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/src/platform-impl.cpp), lines 100-103), ensuring each verification thread maintains its own view of the execution environment without cross-thread contamination.

## Step-by-Step Verification Flow in bpftime

The verification process in [`bpftime-verifier/src/bpftime-verifier.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/src/bpftime-verifier.cpp) follows a six-stage pipeline:

**1. Program Preparation**

The raw eBPF bytecode (passed as `uint64_t*`) is copied into a `raw_program` structure. The target ELF section name is preserved to allow the platform to resolve the appropriate program type (lines 41-49).

**2. Platform Description**

A `bpftime_platform_spec` is constructed from [`platform-impl.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/platform-impl.cpp) (lines 18-99). This specification provides callbacks for program-type lookup, helper prototypes, map descriptors, and map-type resolution. The spec is attached to `prog.info` to inform the verifier about available resources.

**3. Map and Helper Registration**

Before verification, users must register maps and helpers via the thread-local API:
- `set_map_descriptors()` registers map metadata including file descriptors, key/value sizes, and map types
- `set_available_helpers()` declares kernel helpers the program may invoke
- `set_non_kernel_helpers()` defines custom helper prototypes for userspace extensions

These populate `usable_helpers`, `non_kernel_helpers`, and `map_descriptors` containers.

**4. Unmarshalling**

The raw instruction list is converted into an `InstructionSeq` via the `unmarshal` function (lines 58-63). This step translates the binary bytecode into the verifier's internal representation while collecting parsing notes for error reporting.

**5. Running the Verifier**

The `ebpf_verify_program` function is invoked with the assembled `InstructionSeq`, program info, and `verifier_options` (lines 66-69). The options structure enables termination checks, invariant printing, and failure diagnostics.

**6. Result Handling**

The verifier returns `true` on success. `verify_ebpf_program` returns an empty `std::optional<std::string>` to indicate success, or an error message on failure (lines 70-73).

## Implementing Custom Helpers and Maps

The bpftime verifier allows userspace programs to declare custom helpers beyond the kernel standard set. This requires defining helper prototypes and registering them before verification:

```cpp
#include <bpftime-verifier.hpp>
#include <map>
#include <vector>
#include <optional>
#include <iostream>

int main() {
    // Register maps used by the program
    set_map_descriptors({
        { 10, BpftimeMapDescriptor{
                .original_fd = 10,
                .type = BPF_MAP_TYPE_HASH,
                .key_size = 8,
                .value_size = 4,
                .max_entries = 1024,
                .inner_map_fd = 0 } }
    });

    // Declare kernel helpers
    set_available_helpers({ BPF_FUNC_map_lookup_elem, 1000001 });

    // Define custom helper prototype for userspace extension
    set_non_kernel_helpers({
        { 1000001, BpftimeHelperProrotype{
                .name = "my_helper",
                .return_type = EBPF_RETURN_TYPE_INTEGER,
                .argument_type = {
                    EBPF_ARGUMENT_TYPE_ANYTHING,
                    EBPF_ARGUMENT_TYPE_PTR_TO_MAP } } }
    });

    // Raw eBPF bytecode (example: tiny uprobe program)
    const uint64_t prog[] = {
        0x00000002000001b7, // r0 = 0
        0x00000000fff81a7b, // exit
    };

    // Run verification
    std::optional<std::string> err = verify_ebpf_program(
        prog, std::size(prog), "uprobe//proc/self/exe:my_uprobe");

    if (err) {
        std::cerr << "Verification failed:\n" << *err << '\n';
    } else {
        std::cout << "Program passed safety verification!\n";
    }
}

```

This example demonstrates the thread-local configuration pattern: maps and helpers are registered before calling `verify_ebpf_program`, allowing the verifier to validate memory accesses and helper calls against the declared resources.

## Runtime Integration

The bpftime runtime invokes the same verification API when loading programs through the syscall server. In [`runtime/syscall-server/syscall_context.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/runtime/syscall-server/syscall_context.cpp) (line 503), the runtime calls:

```cpp
auto result = verifier::verify_ebpf_program(raw_inst, n_inst, section_name);
if (result) {
    // Reject load, send error back to the client
}

```

This ensures that all eBPF programs loaded into the bpftime userspace runtime undergo the same static analysis as kernel-loaded programs, maintaining safety guarantees across the userspace execution environment.

## Summary

- bpftime wraps the Linux kernel's **ebpf-verifier** library to provide identical static analysis for userspace eBPF programs.
- Verification occurs in six stages: program preparation, platform description, map/helper registration, unmarshalling, verifier execution, and result handling.
- **Thread-local storage** isolates verification contexts, allowing concurrent safety checks with different helper and map configurations.
- The `verify_ebpf_program` function in [`bpftime-verifier/src/bpftime-verifier.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/src/bpftime-verifier.cpp) serves as the primary entry point, returning `std::optional<std::string>` for error handling.
- Custom helpers and maps are registered via `set_available_helpers`, `set_map_descriptors`, and `set_non_kernel_helpers` before verification.

## Frequently Asked Questions

### What verification library does bpftime use for eBPF safety checks?

bpftime uses the **ebpf-verifier** library—the same static analyzer used by the Linux kernel. This is wrapped in a thin C++ layer in [`bpftime-verifier/src/bpftime-verifier.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/bpftime-verifier/src/bpftime-verifier.cpp) to provide userspace eBPF programs with identical safety guarantees to kernel-loaded programs.

### How does bpftime handle custom helpers during verification?

Custom helpers are registered using `set_non_kernel_helpers()` before calling `verify_ebpf_program()`. This function stores helper prototypes (including name, return type, and argument types) in thread-local storage. During verification, the platform implementation in [`platform-impl.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/platform-impl.cpp) retrieves these prototypes via `bpftime_get_helper_prototype` to validate call arguments and return values.

### Can multiple eBPF programs be verified concurrently in bpftime?

Yes. bpftime uses **thread-local storage** for verification configuration. The `usable_helpers`, `non_kernel_helpers`, and `map_descriptors` containers are stored as `thread_local` variables (see [`platform-impl.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/platform-impl.cpp) lines 100-103). This ensures each verification thread maintains isolated helper and map contexts, preventing race conditions during concurrent safety checks.

### What happens when an eBPF program fails verification in bpftime?

When `ebpf_verify_program` returns false, `verify_ebpf_program` captures the accumulated error messages and returns them as a populated `std::optional<std::string>`. The caller (such as the runtime in [`syscall_context.cpp`](https://github.com/eunomia-bpf/bpftime/blob/main/syscall_context.cpp)) receives this error string and rejects the program load, propagating the failure reason back to the client or logging it for debugging purposes.