Skip to content

UB found by miri #3776

Description

@yilin0518

Hi!

We are a team of researchers studying memory safety in Rust. As part of our ongoing research, we tested revm-interpreter version 35.0.1 and found several cases where public safe APIs appear to allow undefined behavior under Miri.

All examples below are written in safe Rust. They do not use user-side unsafe. The common pattern is that publicly reachable low-level bytecode cursor APIs on revm_interpreter::interpreter::ExtBytecode can be called directly with unchecked offsets or lengths, which leads to raw-pointer UB instead of a panic or checked failure.

Affected API Surface

The following public items are involved:

  • revm_interpreter::interpreter::ext_bytecode::ExtBytecode
  • revm_interpreter::interpreter_types::Jumps
  • revm_interpreter::interpreter_types::Immediates

Relevant source locations:

  • src/interpreter.rs:4
  • src/interpreter.rs:14
  • src/interpreter/ext_bytecode.rs:132-138
  • src/interpreter/ext_bytecode.rs:167-178
  • src/interpreter/ext_bytecode.rs:182-187
  • src/interpreter_types.rs:9-39

Shared Root Cause

ExtBytecode stores an internal raw instruction pointer, and several safe trait methods perform unchecked pointer arithmetic or unchecked raw reads:

  • relative_jump uses self.instruction_pointer.offset(offset)
  • absolute_jump uses self.base.bytes_ref().as_ptr().add(offset)
  • read_u16 uses read_u16(self.instruction_pointer)
  • read_slice uses core::slice::from_raw_parts(self.instruction_pointer, len)
  • read_offset_u16 offsets the raw pointer and then reads u16

Because these methods are safe and publicly reachable, safe callers can violate the internal cursor invariants and trigger UB directly.

In other words, even if these APIs are intended as low-level interpreter internals, the current public safe interface seems unsound: misuse by safe Rust should not be able to cause UB.

Pattern 1: Jumps::absolute_jump

PoC:

#![feature(allocator_api)]
extern crate alloc;
use revm_interpreter::*;
fn main() {
    let mut v22 = <interpreter::ext_bytecode::ExtBytecode as bytecode::bitvec::macros::internal::core::prelude::v1::Default>::default();
    let v23: &'_ mut interpreter::ext_bytecode::ExtBytecode = &mut v22;
    let v24 = 45usize;
    <interpreter::ext_bytecode::ExtBytecode as interpreter_types::Jumps>::absolute_jump(v23, v24);
}

Miri report:

error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by 45 bytes, but got alloc531 which is only 1 byte from the end of the allocation
   --> /home/rose/projects/lifesonar-tests1/new_crates/analyze-poc/revm-interpreter-35.0.1/src/interpreter/ext_bytecode.rs:138:45
    |
138 |         self.instruction_pointer = unsafe { self.base.bytes_ref().as_ptr().add(offset) };
    |                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
...
    = note: inside `<revm_interpreter::interpreter::ExtBytecode as revm_interpreter::interpreter_types::Jumps>::absolute_jump`
note: inside `main`
   --> src/main.rs:8:5

Analysis:

  • ExtBytecode::default() starts with a default bytecode buffer.
  • absolute_jump is safe to call, but performs unchecked pointer arithmetic with add(offset).
  • An arbitrary caller-controlled offset can move the cursor past the backing allocation.

Pattern 2: Jumps::relative_jump

PoC:

#![feature(allocator_api)]
extern crate alloc;
use revm_interpreter::*;
fn main() {
    let mut v14 = <interpreter::ext_bytecode::ExtBytecode as bytecode::bitvec::macros::internal::core::prelude::v1::Default>::default();
    let v15: &'_ mut interpreter::ext_bytecode::ExtBytecode = &mut v14;
    let v16 = -80isize;
    <interpreter::ext_bytecode::ExtBytecode as interpreter_types::Jumps>::relative_jump(v15, v16);
}

Miri report:

error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by -80 bytes, but got alloc531 which is at the beginning of the allocation
   --> /home/rose/projects/lifesonar-tests1/new_crates/analyze-poc/revm-interpreter-35.0.1/src/interpreter/ext_bytecode.rs:133:45
    |
133 |         self.instruction_pointer = unsafe { self.instruction_pointer.offset(offset) };
    |                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
...
    = note: inside `<revm_interpreter::interpreter::ExtBytecode as revm_interpreter::interpreter_types::Jumps>::relative_jump`
note: inside `main`
   --> src/main.rs:8:5

Analysis:

  • relative_jump is also a safe method.
  • It uses unchecked offset on the raw instruction pointer.
  • Negative or oversized relative offsets immediately move the pointer outside the allocation.

Pattern 3: Immediates::read_u16

PoC:

#![feature(allocator_api)]
extern crate alloc;
use revm_interpreter::*;
fn main() {
    let mut v1 = <interpreter::ext_bytecode::ExtBytecode as bytecode::bitvec::macros::internal::core::prelude::v1::Default>::default();
    let v15: &'_ interpreter::ext_bytecode::ExtBytecode = &v1;
    let v16 = <interpreter::ext_bytecode::ExtBytecode as interpreter_types::Immediates>::read_u16(v15);
}

Miri report:

error: Undefined Behavior: memory access failed: attempting to access 2 bytes, but got alloc531 which is only 1 byte from the end of the allocation
  --> /home/rose/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/revm-bytecode-10.0.0/src/utils.rs:20:33
   |
20 |     u16::from_be_bytes(unsafe { ptr.cast::<[u8; 2]>().read() })
   |                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
...
   = note: inside `revm_interpreter::revm_bytecode::utils::read_u16`
   = note: inside `<revm_interpreter::interpreter::ExtBytecode as revm_interpreter::interpreter_types::Immediates>::read_u16`
note: inside `main`
  --> src/main.rs:7:15

Analysis:

  • read_u16 is a safe method but assumes at least 2 readable bytes at the current cursor.
  • That precondition is not encoded in the type system or API.
  • Calling it on the default bytecode cursor causes an unchecked 2-byte raw read across the allocation boundary.

Pattern 4: Immediates::read_offset_u16 / read_offset_i16

PoC:

#![feature(allocator_api)]
extern crate alloc;
use revm_interpreter::*;
fn main() {
    let mut v15 = <interpreter::ext_bytecode::ExtBytecode as bytecode::bitvec::macros::internal::core::prelude::v1::Default>::default();
    let v16: &'_ interpreter::ext_bytecode::ExtBytecode = &v15;
    let v17 = -68isize;
    let v18 = <interpreter::ext_bytecode::ExtBytecode as interpreter_types::Immediates>::read_offset_u16(v16, v17);
}

Miri report:

error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by -68 bytes, but got alloc531 which is at the beginning of the allocation
   --> /home/rose/projects/lifesonar-tests1/new_crates/analyze-poc/revm-interpreter-35.0.1/src/interpreter/ext_bytecode.rs:185:17
    |
185 | /                 self.instruction_pointer
186 | |                     // Offset for max_index that is one byte
187 | |                     .offset(offset),
    | |___________________________________^ Undefined Behavior occurred here
...
    = note: inside `<revm_interpreter::interpreter::ExtBytecode as revm_interpreter::interpreter_types::Immediates>::read_offset_u16`
note: inside `main`
   --> src/main.rs:8:15

Analysis:

  • read_offset_u16 and read_offset_i16 are safe methods.
  • They first move the raw cursor by a signed offset, then read 2 bytes.
  • Arbitrary signed offsets can take the raw pointer out of bounds before the read even happens.

Pattern 5: Immediates::read_slice

PoC:

#![feature(allocator_api)]
extern crate alloc;
use revm_interpreter::*;
fn main() {
    let v1 = <interpreter::ext_bytecode::ExtBytecode as bytecode::bitvec::macros::internal::core::prelude::v1::Default>::default();
    let v2: &'_ interpreter::ext_bytecode::ExtBytecode = &v1;
    let v3 = 130usize;
    let v4 = <interpreter::ext_bytecode::ExtBytecode as interpreter_types::Immediates>::read_slice(v2, v3);
}

Miri report:

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 130 bytes, but got alloc531 which is only 1 byte from the end of the allocation
   --> /home/rose/projects/lifesonar-tests1/new_crates/analyze-poc/revm-interpreter-35.0.1/src/interpreter/ext_bytecode.rs:178:18
    |
178 |         unsafe { core::slice::from_raw_parts(self.instruction_pointer, len) }
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
...
    = note: inside `<revm_interpreter::interpreter::ExtBytecode as revm_interpreter::interpreter_types::Immediates>::read_slice`
note: inside `main`
   --> src/main.rs:8:14

Analysis:

  • read_slice is safe but constructs a slice from a raw pointer and caller-provided length.
  • There is no bounds validation before from_raw_parts.
  • This allows safe callers to request a slice larger than the remaining bytecode and trigger UB.

Why We Believe This Is a Crate Issue

These examples do bypass the intended high-level Interpreter execution path, but they still use public safe APIs only.

If these low-level APIs are intended for internal use only, making them public and safe still creates a soundness problem, as it seems that safe misuse should not cause UB. And a public safe API must uphold Rust’s safety guarantees even when called with bad inputs. It is the better design that invalid inputs leads to a checked failure, panic, or an unsafe precondition on the caller side, not undefined behavior.

Possible Fix Directions

Possible approaches that might address this:

  1. Add explicit bounds checks before raw pointer arithmetic / raw reads.
  2. Replace unchecked operations with checked APIs returning Option/Result where appropriate.

We would appreciate it if you could take a look and confirm whether this behavior indicates a real issue, or if it is a false positive / expected limitation of Miri.

Thank you very much for your time and for maintaining this great project!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions