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:
- Add explicit bounds checks before raw pointer arithmetic / raw reads.
- 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!
Hi!
We are a team of researchers studying memory safety in Rust. As part of our ongoing research, we tested
revm-interpreterversion35.0.1and 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 onrevm_interpreter::interpreter::ExtBytecodecan 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::ExtBytecoderevm_interpreter::interpreter_types::Jumpsrevm_interpreter::interpreter_types::ImmediatesRelevant source locations:
src/interpreter.rs:4src/interpreter.rs:14src/interpreter/ext_bytecode.rs:132-138src/interpreter/ext_bytecode.rs:167-178src/interpreter/ext_bytecode.rs:182-187src/interpreter_types.rs:9-39Shared Root Cause
ExtBytecodestores an internal raw instruction pointer, and several safe trait methods perform unchecked pointer arithmetic or unchecked raw reads:relative_jumpusesself.instruction_pointer.offset(offset)absolute_jumpusesself.base.bytes_ref().as_ptr().add(offset)read_u16usesread_u16(self.instruction_pointer)read_sliceusescore::slice::from_raw_parts(self.instruction_pointer, len)read_offset_u16offsets the raw pointer and then readsu16Because 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_jumpPoC:
Miri report:
Analysis:
ExtBytecode::default()starts with a default bytecode buffer.absolute_jumpis safe to call, but performs unchecked pointer arithmetic withadd(offset).offsetcan move the cursor past the backing allocation.Pattern 2:
Jumps::relative_jumpPoC:
Miri report:
Analysis:
relative_jumpis also a safe method.offseton the raw instruction pointer.Pattern 3:
Immediates::read_u16PoC:
Miri report:
Analysis:
read_u16is a safe method but assumes at least 2 readable bytes at the current cursor.Pattern 4:
Immediates::read_offset_u16/read_offset_i16PoC:
Miri report:
Analysis:
read_offset_u16andread_offset_i16are safe methods.Pattern 5:
Immediates::read_slicePoC:
Miri report:
Analysis:
read_sliceis safe but constructs a slice from a raw pointer and caller-provided length.from_raw_parts.Why We Believe This Is a Crate Issue
These examples do bypass the intended high-level
Interpreterexecution 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
unsafeprecondition on the caller side, not undefined behavior.Possible Fix Directions
Possible approaches that might address this:
Option/Resultwhere 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!