Skip to content

[H-3] Unchecked arena index enables out-of-bounds reads #170

@this-vishalsingh

Description

@this-vishalsingh

Context: crates/backend/air/src/symbolic.rs

Description

The symbolic expression arena stores SymbolicNode<F> values inside a Vec<u8> and later retrieves them via read_unaligned using a caller-provided u32 index. Because SymbolicExpression::Operation(u32) is a public enum variant and get_node() is a safe, public function, any downstream code can construct an arbitrary Operation(idx) and trigger get_node(idx) directly, or indirectly via any evaluator to read out of bounds, causing undefined behavior crash, memory disclosure, or potentially code execution.
Separately, get_symbolic_constraints_and_bus_data_values() clears the arena before building, previously returned SymbolicExpression::Operation values from earlier calls become invalid once a later call clears the arena, enabling use-after-clear style UB if those expressions are used afterwards.
Also, alloc_node truncates usize offsets into u32, which can wrap on very large arenas and lead to incorrect reads.

Attack path

A plugin/consumer crate or any untrusted in-process code passes a crafted SymbolicExpression::Operation(large_idx) into code that calls get_node e.g., a constraint/codegen routine, resulting in an out-of-bounds read_unaligned from the thread-local arena buffer, triggering UB and possible memory disclosure or process compromise.

thread_local! {
    static ARENA: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}

fn alloc_node<F: Field>(node: SymbolicNode<F>) -> u32 {
    ARENA.with(|arena| {
        let mut bytes = arena.borrow_mut();
        let node_size = std::mem::size_of::<SymbolicNode<F>>();
        let idx = bytes.len();
        bytes.resize(idx + node_size, 0);
        unsafe {
            std::ptr::write_unaligned(bytes.as_mut_ptr().add(idx) as *mut SymbolicNode<F>, node);
        }
        idx as u32
    })
}

pub fn get_node<F: Field>(idx: u32) -> SymbolicNode<F> {
    ARENA.with(|arena| {
        let bytes = arena.borrow();
        unsafe { std::ptr::read_unaligned(bytes.as_ptr().add(idx as usize) as *const SymbolicNode<F>) }
    })
}

pub enum SymbolicExpression<F: Copy> {
    Variable(SymbolicVariable<F>),
    Constant(F),
    Operation(u32),
}

Recommendation

Make arena access memory-safe and non-forgeable:

  • Make SymbolicExpression::Operation and get_node non-public e.g., pub(crate) or make get_node unsafe with strict safety preconditions.
  • Store nodes in Vec<SymbolicNode<F>> typed arena rather than Vec<u8>, and perform bounds checks on indices.
  • Avoid truncating indices to u32 use usize, or hard-fail if the arena would exceed u32::MAX.
  • Prevent lifetime issues by tying expressions to an arena lifetime/generation so old expressions can’t be used after clear(), or never clearing/reusing the arena while expressions may be live.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions