From a Branch Misprediction to Kernel Data
This chapter follows the BPF-assisted Spectre-PHT case on a T-Head Xuantie C910 to answer a concrete systems question: where does a security guarantee live when an untrusted program passes through a verifier, a just-in-time compiler, and a speculative processor? The case matters because it turns a predictor measurement into a kernel-confidentiality problem. It also gives a practical test for security claims: an annotation or policy is protection only if the generated instructions and the processor’s behavior preserve its meaning.
The security intent begins before the CPU
BPF programs can be loaded into the Linux kernel and executed as native code after just-in-time compilation. That arrangement has an obvious security advantage: the kernel does not have to trust every program simply because it is syntactically valid. A verifier can inspect control flow and data use, reject dangerous patterns, or mark paths that require special protection. But the verifier is not the final executor. Its decision must cross an interface into the JIT compiler, and the JIT must express that decision in instructions whose security behavior is real on the target processor.
The study’s BPF case centers on a type-confusion pattern. A program can carry either a valid pointer or a scalar value in a register. On the architectural path, a branch distinguishes those cases, and the scalar path is never supposed to be dereferenced. If the processor has learned to expect the pointer path, however, it may transiently follow that prediction when the scalar path is presented. The wrong-path load can then use an attacker-controlled value before the branch resolves. The architectural rules still reject the invalid dereference; the transient operation is the problem.
Linux has a concept for this boundary: BPF_NOSPEC. The verifier can insert such a marker at a potentially dangerous point. On other architectures, the JIT lowers the marker into an architecture-specific speculation barrier. The paper reports that the x86 BPF backend emits lfence, while ARM64 emits a speculation barrier when available and otherwise uses a stronger sequence involving instruction and data synchronization. The marker therefore has a clear intended lifecycle: policy identifies a risk, code generation emits a barrier, and the processor gives that barrier the required effect.
On RISC-V, the lifecycle breaks at code generation. The study reports that the RISC-V BPF JIT emits no instruction for BPF_NOSPEC. From the verifier’s perspective, a protection was inserted. From the native code’s perspective, there is no corresponding operation to constrain speculation. The mismatch is more serious than an omitted optimization. It means that the same security-aware kernel policy has different meanings depending on the architecture to which BPF is compiled.