How bpftime Performs Safety Verification for eBPF Programs: Architecture and Implementation
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 (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 and implemented in 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 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 (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 typesset_available_helpers()declares kernel helpers the program may invokeset_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:
#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 (line 503), the runtime calls:
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_programfunction inbpftime-verifier/src/bpftime-verifier.cppserves as the primary entry point, returningstd::optional<std::string>for error handling. - Custom helpers and maps are registered via
set_available_helpers,set_map_descriptors, andset_non_kernel_helpersbefore 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 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 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 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) receives this error string and rejects the program load, propagating the failure reason back to the client or logging it for debugging purposes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →