When using external eBPF verifiers like PREVAIL with uBPF, it's important to understand the contract between the verifier and the runtime regarding the context pointer.
In eBPF programs, register r1 contains a pointer to the context when the program starts execution. This context pointer is the first parameter passed to the program and provides access to:
In uBPF, this context is provided via the mem parameter to functions like:
ubpf_exec(vm, mem, mem_len, &ret)ubpf_jit_fn(mem, mem_len) (compiled functions)PREVAIL and other Linux-based eBPF verifiers make the following assumptions:
r1 points to a valid memory regionr1 are within the expected context sizeThe uBPF runtime and test harness have more flexible requirements:
-m, the test harness passes NULL as the contextConsider this eBPF program:
mov64 r0, 0x0 arsh64 r0, r5 ldxw r3, [r1+0x1] ; Read from context+1 mov64 r4, r1 exit
When verified with PREVAIL:
When executed with uBPF without a memory file:
r1 is NULL, so ldxw r3, [r1+0x1] tries to read from address 0x1If you're running eBPF programs that have been verified with PREVAIL or similar verifiers:
When using the vm/test utility, always provide a memory file if your program accesses the context:
# Create a dummy context file if needed echo -n "dummy_context_data" > context.bin # Run the program with context ./vm/test -m context.bin program.o
For programs verified against specific context types (e.g., XDP), ensure your context matches the expected structure:
// Example: XDP-like context struct xdp_context { uint64_t data; uint64_t data_end; // ... other fields }; struct xdp_context ctx; ctx.data = (uint64_t)packet_buffer; ctx.data_end = ctx.data + packet_size; ubpf_exec(vm, &ctx, sizeof(ctx), &ret);
If you're integrating uBPF into your application:
// Ensure non-null context for verified programs if (program_is_verified && (mem == NULL || mem_len == 0)) { fprintf(stderr, "Error: Verified programs require a valid context\n"); return -1; } ubpf_exec(vm, mem, mem_len, &ret);
If your use case allows, provide a default context buffer:
// Provide a minimal default context static uint8_t default_context[256] = {0}; void* context = user_provided_context; size_t context_len = user_context_len; if (context == NULL && program_requires_context) { context = default_context; context_len = sizeof(default_context); } ubpf_exec(vm, context, context_len, &ret);
The uBPF libfuzzer correctly handles this by always providing a proper context structure. See libfuzzer/libfuzz_harness.cc for the complete implementation. Here's the essential pattern:
// Simplified example - see libfuzz_harness.cc for full implementation ubpf_context_t context{}; context.data = reinterpret_cast<uint64_t>(memory.data()); context.data_end = context.data + memory.size(); context.stack_start = reinterpret_cast<uint64_t>(stack.data()); context.stack_end = context.stack_start + stack.size(); // ... other fields initialized
The fuzzer then executes with this properly initialized context, ensuring programs that access r1 have valid memory to work with.
A NULL context (or no memory file) is safe only when:
r1 (the context pointer)Example of a safe program with NULL context:
mov64 r0, 0x42 ; Use only local registers add64 r0, 0x10 exit
| Scenario | PREVAIL Verification | uBPF Execution | Result |
|---|---|---|---|
| Program accesses context + NULL context | ✅ Verified | ❌ Crash | Incompatible |
| Program accesses context + Valid context | ✅ Verified | ✅ Runs | Compatible |
| Program doesn't access context + NULL context | N/A (usually not verified) | ✅ Runs | Safe |
| Program doesn't access context + Valid context | ✅ Verified | ✅ Runs | Compatible |
If you're running programs verified for XDP context:
#include <stdint.h> #include <ubpf.h> // XDP-compatible context structure typedef struct { uint64_t data; uint64_t data_end; uint64_t data_meta; // ... other XDP fields as needed } xdp_md_t; int run_xdp_program(struct ubpf_vm* vm, void* packet, size_t packet_len) { xdp_md_t ctx = {0}; ctx.data = (uint64_t)packet; ctx.data_end = ctx.data + packet_len; ctx.data_meta = ctx.data; // No metadata by default uint64_t result; int ret = ubpf_exec(vm, &ctx, sizeof(ctx), &result); if (ret < 0) { fprintf(stderr, "Execution failed\n"); return -1; } return (int)result; // XDP action (PASS, DROP, etc.) }
The key to successfully using verified eBPF programs with uBPF is understanding and respecting the contract between the verifier and the runtime. Always provide a valid context when running programs that have been verified with assumptions about the context pointer.