Skip to content

Missing boundary check for load/store in JIT compiler #435

Open

Description

@pcy190

The current JIT compiler does not emit boundary checks for the load/store memory opcode. Hence, any program can perform an invalid memory operation.

e.g., the following PoC program can trigger invalid memory write:

mov %r0, 0
stxdw [%r0+96], %r0

Running it under JIT mode, we can get:

==173819==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x0000000000f8 (pc 0x7fd973a5e023 bp 0x7ffd906bec70 sp 0x7ffd906bea60 T173819)
==173819==The signal is caused by a WRITE memory access.

Activity

  1. Alan-Jowett commented on May 7, 2024

    @Alan-Jowett
    Collaborator

    @pcy190 bounds checking is not implemented at all for JIT.

  2. Alan-Jowett commented on Jan 27, 2026

    @Alan-Jowett
    Collaborator

    Root Cause Analysis & Implementation Details

    Thanks for the report @pcy190! Here's a comprehensive analysis for implementers.


    Current Status

    As noted by @Alan-Jowett, bounds checking is not implemented in either the x86_64 or ARM64 JIT compilers. The interpreter has full bounds checking, but the JIT path skips these checks entirely.


    Root Cause Analysis

    The Vulnerability

    The JIT compiler emits raw memory access instructions without any validation:

    x86_64 JIT (ubpf_jit_x86_64.c lines 1927-1974):

    case EBPF_OP_LDXW:
        emit_load(state, S32, src, dst, inst.offset);  // No bounds check!
        break;
    case EBPF_OP_STXW:
        emit_store(state, S32, src, dst, inst.offset); // No bounds check!
        break;

    ARM64 JIT (ubpf_jit_arm64.c):
    Same pattern - emit_loadstore() is called directly without validation.

    Interpreter (ubpf_vm.c lines 1127-1130):

    case EBPF_OP_LDXW:
        BOUNDS_CHECK_LOAD(4);  // ← Bounds check HERE
        reg[inst.dst] = ubpf_mem_load(reg[inst.src] + inst.offset, 4);
        break;

    Attack Scenario

    BPF Program:
      mov %r0, 0
      stxdw [%r0+96], %r0    // Write 8 bytes to address 0x60
    
    Interpreter path:
      → bounds_check() called → returns false → execution stops
    
    JIT path:
      → mov [rax+96], rbx generated → executed → SEGV or arbitrary write!
    

    Proposed Implementation

    There are two main approaches:

    Option A: Runtime Bounds Check Calls (Simpler, Slower)

    Emit a call to the existing bounds_check() function before each load/store:

    // Before each load/store in JIT:
    static void
    emit_bounds_check_call(struct jit_state* state, int addr_reg, int32_t offset, int size, const char* type)
    {
        // Save volatile registers
        // Set up arguments for bounds_check():
        //   arg1 (RDI): vm pointer (stored in non-volatile register)
        //   arg2 (RSI): computed address (addr_reg + offset)  
        //   arg3 (RDX): size
        //   arg4 (RCX): type string pointer
        //   arg5 (R8):  cur_pc
        //   arg6-9:     mem, mem_len, stack, stack_len (from context)
        
        // Call bounds_check via function pointer
        emit_call(state, bounds_check_wrapper);
        
        // Check return value (RAX), jump to error handler if false
        emit_cmp_imm32(state, RAX, 0);
        emit_jcc(state, JNE, error_label);
        
        // Restore volatile registers
    }

    Pros: Reuses existing bounds_check() logic, supports custom bounds check callbacks
    Cons: ~50+ bytes of code per load/store, significant performance impact

    Option B: Inline Range Checks (Complex, Faster)

    Generate inline comparison code for common cases (stack/mem access):

    static void
    emit_inline_bounds_check(struct jit_state* state, int addr_reg, int32_t offset, int size)
    {
        // Compute: access_addr = addr_reg + offset
        emit_lea(state, RAX, addr_reg, offset);
        
        // Check: access_addr >= mem_start && access_addr + size <= mem_end
        emit_cmp_reg64(state, RAX, mem_start_reg);
        emit_jcc(state, JB, check_stack);  // Below mem, try stack
        
        emit_lea(state, RCX, RAX, size);   // RCX = access_end
        emit_cmp_reg64(state, RCX, mem_end_reg);
        emit_jcc(state, JBE, bounds_ok);   // Within mem bounds
        
    check_stack:
        // Similar check against stack_start/stack_end
        emit_cmp_reg64(state, RAX, stack_start_reg);
        emit_jcc(state, JB, bounds_fail);
        
        emit_lea(state, RCX, RAX, size);
        emit_cmp_reg64(state, RCX, stack_end_reg);
        emit_jcc(state, JA, bounds_fail);
        
    bounds_ok:
        // Continue with actual load/store
        
    bounds_fail:
        // Jump to error exit
    }

    Pros: Much faster (inline comparisons vs function call), ~20-30 bytes per check
    Cons: Doesn't support custom bounds_check_function callback, complex implementation

    Option C: Hybrid Approach (Recommended)

    1. Add inline checks for stack/mem (covers 99% of cases)
    2. Fall back to calling bounds_check() only when inline check fails (handles custom callbacks)
    // Inline fast path
    if (access in mem_range || access in stack_range) {
        // proceed with load/store
    } else {
        // Slow path: call bounds_check() for custom regions
        if (!bounds_check(...)) {
            goto error;
        }
    }

    Implementation Requirements

    1. Reserve registers for bounds info (or load from context struct):

      • mem_start, mem_end
      • stack_start, stack_end (stack is R10 in BPF, already tracked)
    2. Add error exit path in JIT prologue/epilogue:

      • Set return value to -1
      • Clean up stack
      • Return to caller
    3. Respect bounds_check_enabled flag:

      • Check vm->bounds_check_enabled at JIT compile time
      • Optionally: check at runtime in prologue, skip all checks if disabled
    4. Handle all load/store opcodes:

      • EBPF_OP_LDX{B,H,W,DW} and sign-extended variants
      • EBPF_OP_ST{B,H,W,DW} (immediate stores)
      • EBPF_OP_STX{B,H,W,DW} (register stores)
      • EBPF_OP_ATOMIC_STORE variants

    Affected Files

    File Changes Needed
    vm/ubpf_jit_x86_64.c Add bounds check emit functions, modify all load/store cases
    vm/ubpf_jit_arm64.c Same as x86_64 for ARM64
    vm/ubpf_jit_support.h Add shared helper declarations
    tests/ Add JIT-specific bounds check tests

    Acceptance Criteria

    • JIT-compiled code performs bounds checking equivalent to interpreter
    • Bounds checking can be disabled via ubpf_toggle_bounds_check(vm, false)
    • Custom bounds_check_function callbacks are invoked when set
    • All existing tests pass with JIT
    • New tests verify OOB access is caught in JIT mode
    • Performance impact documented (expected: 10-30% for bounds-heavy programs)

    Test Cases

    // Test: OOB write to NULL caught
    uint8_t code[] = {
        0xb7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r0, 0
        0x7b, 0x0a, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, // stxdw [r0+96], r0
        0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit
    };
    // Expected: JIT execution returns -1 (bounds check failed)
    
    // Test: Valid stack access succeeds  
    uint8_t code[] = {
        0x7b, 0x1a, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00, // stxdw [r10-8], r1
        0x79, 0x0a, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00, // ldxdw r0, [r10-8]
        0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // exit
    };
    // Expected: JIT execution succeeds, returns input value

    Severity

    Critical - This allows arbitrary memory read/write from JIT-compiled BPF programs, bypassing all safety guarantees.


    References

    • Linux kernel BPF JIT (implements similar checks): arch/x86/net/bpf_jit_comp.c
    • Interpreter bounds check implementation: ubpf_vm.c lines 1089-1125
  3. Alan-Jowett commented on Jan 28, 2026

    @Alan-Jowett
    Collaborator

    Implemented for x64. Will file a separate issue for ARM64 support

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions