BPF gains indirect subprogram calls via new callx instruction
Alexei Starovoitov’s series lets the verifier and JITs accept LLVM’s function-pointer calls, unlocking vtables and dispatch tables in BPF C and Rust.
The BPF subsystem is gaining support for indirect calls to static subprograms. Alexei Starovoitov has posted a patch series that introduces the callx instruction, the encoding LLVM already emits for calls through a function pointer, so the verifier no longer rejects those programs as unknown opcodes.
Until now BPF could only make direct calls to known subprograms. callx lets a program hold a pointer to a static function in a register and invoke it, which matches ordinary C patterns such as an array of handlers or a struct of operations, and the vtable-style dispatch common in Rust. Only static subprograms are allowed; global subprograms, helpers, and kfuncs remain off-limits. The callee is verified in the caller’s context the same way a direct static call is.
Function addresses may come from the existing pseudo-immediate load for static functions, or from read-only data that mixes pointers with other constants. libbpf places such tables in a frozen per-program map, stores byte offsets of the static functions, and the kernel recognizes those offsets before control-flow analysis. After JIT, the offsets are rewritten to real addresses. Because the program can read those addresses as data, CAP_PERFMON is required, and the map may have only that one user.
The main verifier pass records the resulting caller-callee edges so recursion and stack-depth checks still work. JITs must opt in; x86 and arm64 backends are updated, while the interpreter does not implement callx and forces JIT. FineIBT configurations that lack the necessary CFI preamble still reject the programs at load time.
The change closes a long-standing gap between what compilers generate for higher-level BPF code and what the kernel would accept, particularly for Rust and for C that relies on function tables rather than hard-coded direct calls.