freenode
Kernel & Low-Level

BPF gains callx for indirect subprogram calls

Alexei Starovoitov’s verifier, JIT, and libbpf work lets programs invoke static functions through pointers in tables and vtables.

The BPF subsystem is adding callx, an indirect call that invokes a static BPF subprogram whose address sits in a register. Alexei Starovoitov posted the second revision of the series covering the verifier, x86 and arm64 JITs, libbpf, documentation, and selftests.

Until now the verifier treated direct calls and a few special cases. Compilers already emit function pointers into read-only data for vtables, struct-ops tables, and similar patterns, including Rust vtables, but BPF could not call through them. callx closes that gap: a program loads a pointer (from an immediate pseudo-func load or from a frozen read-only map) and issues the new instruction. The verifier must prove the register holds the address of a static BPF function; global functions, scalars, and modified or variable pointers are rejected.

Because the real callee is known only during the main verification pass, the series records caller-to-callee edges for both callx and synchronous callbacks. Those edges feed a second topological sort that catches bounded recursion and a stack-depth walk that accounts for every feasible callee, including addresses taken several frames above the call site. Callees reachable by tail call are disallowed for callx, since JITs do not pass the tail-call counter through an indirect call.

On x86 the JIT emits an indirect call, using ITS or retpoline thunks when the CPU requires them, and refuses FineIBT configurations until CFI preambles are handled. arm64 lowers callx to a register branch-and-link, reusing the same convention already used for out-of-range direct calls. Both architectures mark programs that use callx as JIT-required.

libbpf recognizes aligned function pointers inside .rodata and position-independent .data.rel.ro sections, rewrites them as per-program map copies holding byte offsets, and teaches the light-skeleton loader to create those maps at run time. The kernel later replaces the offsets with real addresses. Programs without callx keep the original maps unchanged.

The feature matters for any BPF code that dispatches through tables or object-style ops without hard-coding every target. Extensive verifier tests cover recursion, stack liveness, precision, mixed pointer/data tables, and CAP_PERFMON requirements; the tests skip when JIT is unavailable.