BPF Instruction Set Architecture (ISA)
Extracted elements (30)
BPF is no longer an acronym (formerly 'Berkeley Packet Filter'); the 'e' in eBPF stood for 'extended' but is now also dropped in common usage. The original cBPF (classic BPF) design is preserved through the deprecated 'packet' conformance group for legacy packet-access instructions, allowing a clean separation from the extended ISA.
Deprecating instructions is handled by defining a separate 'legacy' conformance group for the deprecated instructions and a new superseding group that includes the original but excludes the legacy group. This allows implementations that drop deprecated instructions to claim conformance to the newer group while retaining backward-compatibility claims via the original group.
Division and modulo by zero are handled by defining safe fallbacks rather than undefined behavior: DIV by zero sets the destination register to zero, and MOD by zero leaves the destination unchanged (for ALU64) or zeroes the upper 32 bits (for ALU). This avoids undefined behavior for potentially untrusted BPF programs.
Once a conformance group is registered with a fixed instruction set, no further instructions can be added to it; instead, a new conformance group must be created that includes the original (via the 'includes' column) and adds the new instructions. This immutability of conformance groups ensures that conformance claims remain stable and unambiguous over time.
The 'regs' byte encoding of src_reg and dst_reg is endian-dependent: on little-endian hosts, src_reg occupies the high nibble and dst_reg the low nibble; on big-endian hosts, the order is reversed. This design keeps the wire representation natural for each host architecture while maintaining identical instruction semantics.
Two flavors of unconditional jump (JA) exist: JMP class uses the 16-bit 'offset' field, while JMP32 class uses the 32-bit 'imm' field. This allows conditional jumps beyond 16 bits to be implemented as a short conditional jump plus a 32-bit unconditional JMP32.
The conformance group mechanism enables interoperability between runtimes and compilers: a runtime advertises supported conformance groups (e.g., base32, base64, atomic64), and compilers only emit instructions within those groups. Capability discovery can be manual or automated via tooling.
An implementation MUST support the base32 conformance group and MAY support additional conformance groups, where supporting a conformance group means it MUST support all instructions in that conformance group.
Legacy BPF packet access instructions (ABS and IND mode modifiers, LD class) are deprecated and SHOULD no longer be used. All such instructions belong to the 'packet' conformance group.
Signed modulo (SMOD) MUST use truncated division (where -13 % 3 == -1, as in C and Go), defined as: a % n = a - n * trunc(a / n). This resolves ambiguity between language implementations where Python/Ruby differ from C/Go/Java.
Unused fields in a BPF instruction SHALL be cleared to zero.
64-bit immediate instructions (IMM mode, DW size, LD class) use the wide encoding and repurpose the 'src_reg' field as an opcode subtype to encode: load 64-bit integer (0x0), map reference by file descriptor (0x1), map value address by fd (0x2), platform variable address (0x3), code address (0x4), map reference by index (0x5), and map value address by index (0x6).
Atomic operations (ATOMIC mode, STX class) operate on memory atomically, preventing interruption by other BPF programs. Simple operations (ADD=0x00, OR=0x40, AND=0x50, XOR=0xa0) are supported for 32-bit (atomic32 group) and 64-bit (atomic64 group). Complex operations include FETCH modifier (returns old value), XCHG (atomic exchange), and CMPXCHG (compare-and-exchange against R0). 8-bit and 16-bit atomic operations are not supported.
Byte swap instructions use the END code (0xd) with ALU or ALU64 class. ALU variants use the source bit to select little-endian (LE=0) or big-endian (BE=1) conversion between host and target byte order. ALU64 unconditionally swaps byte order regardless of host endianness. The 'imm' field encodes the width: 16, 32, or 64 bits.
For arithmetic and jump instructions, the 8-bit opcode is divided into 'code' (4 bits), 's' source bit (1 bit, K=0 for immediate or X=1 for register), and 'class' (3 bits). ALU uses 32-bit operands and ALU64 uses 64-bit operands for otherwise identical operations.
Helper functions allow BPF programs to call platform-provided functions, identified either by a static numeric ID in the 'imm' field (src_reg=0x0) or by a BTF (BPF Type Format) ID (src_reg=0x2). Program-local functions are called with src_reg=0x1 using a PC-relative offset in 'imm', and EXIT within a local function returns to the caller.
Jump instructions (JMP/JMP32) encode 14 operations including unconditional jump (JA), equality/inequality comparisons (JEQ, JNE), signed and unsigned ordered comparisons (JGT, JGE, JLT, JLE, JSGT, JSGE, JSLT, JSLE), bit-test (JSET), function call (CALL), and return (EXIT). The PC offset is in units of 64-bit instructions relative to the instruction after the jump.
Load and store instructions use a 3-part opcode: 'mode' (3 bits: IMM=0, ABS=1, IND=2, MEM=3, MEMSX=4, ATOMIC=6) and 'sz' (2 bits: W=0 for 4 bytes, H=1 for 2 bytes, B=2 for 1 byte, DW=3 for 8 bytes). Instructions using DW belong to the base64 conformance group.
Maps are shared memory regions accessible by BPF programs. A map can be referenced by file descriptor (map_by_fd) if the platform supports it, or by a program-load-time index (map_by_idx). The map_val() operation returns the address of the first value and is only defined for maps with a single contiguous memory region.
Regular load and store (MEM mode) operations transfer data between a register and a memory address computed as base register plus signed offset. STX stores from register, ST stores immediate, and LDX loads as unsigned. Sign-extension loads (MEMSX mode) load signed 8-, 16-, or 32-bit values and sign-extend to the register width.
Shift operations apply a mask to the shift amount: 0x3F (63) for 64-bit shifts and 0x1F (31) for 32-bit shifts, preventing undefined behavior from out-of-range shift counts. The MOVSX instruction (sign-extending move) is only defined for register source operands (X source), not immediate (K source).
The BPF arithmetic instruction set includes ADD, SUB, MUL, DIV/SDIV, OR, AND, LSH, RSH, NEG, MOD/SMOD, XOR, MOV/MOVSX, ARSH, and END, encoded in the 4-bit 'code' field. Division/multiplication/modulo belong to divmul32/divmul64 conformance groups; other instructions belong to base32/base64.
The three least significant bits of the 'opcode' field encode the instruction class: LD (0x0), LDX (0x1), ST (0x2), STX (0x3), ALU (0x4), JMP (0x5), JMP32 (0x6), ALU64 (0x7). ALU/JMP32 operate on 32-bit operands; ALU64/JMP on 64-bit.
IANA created the 'BPF Instruction Conformance Groups' registry (under the 'BPF Instructions' registry group) with seven initial entries: atomic32, atomic64, base32, base64, divmul32, divmul64 (all Permanent), and packet (Historical). Registration requires Standards Action or IESG Approval for Permanent entries, or Specification Required for Provisional/Historical.
IANA created the 'BPF Instruction Set' registry (under the 'BPF Instructions' registry group) with initial values provided in Appendix A, covering all BPF instructions with fields: opcode, src_reg, offset, imm, description, groups, change controller, and reference. New instructions require a new conformance group entry; deprecated instructions become Historical.
BPF execution environments should only run trusted and verified BPF programs, using sandboxing and privilege level separation to limit security impact. BPF verifiers (e.g., Linux kernel verifier, PREVAIL) enforce termination, safe memory access, API contract adherence, and absence of undefined-behavior instructions, often providing stronger assurance than general OS code.
BPF interpreters are a source of insecurity when used in the same memory address space as confidential data, because they are susceptible to side-channel attacks via speculative execution gadgets. Use of a JIT compiler is recommended instead; compilers must be carefully audited to ensure compilation of verified BPF programs does not introduce vulnerabilities.
Exposing functionality via BPF extends the interface between the executing and submitting components. Careful consideration of what functionality is exposed and its impact on desired security properties is required.
A basic BPF instruction is 64 bits wide, encoding four fields: 'opcode' (8 bits, split into 'specific' and 'class'), 'regs' (8 bits, encoding src_reg and dst_reg as 4-bit nibbles with byte-order-dependent nibble order), 'offset' (16-bit signed integer), and 'imm' (32-bit signed integer). Multi-byte fields use host byte order.
The wide instruction encoding is 128 bits: a standard 64-bit basic instruction followed by a 64-bit pseudo-instruction whose 'opcode', 'dst_reg', 'src_reg', and 'offset' are all zero, with the second 32-bit immediate value in the 'next_imm' field and a 'reserved' field set to zero.