Skip to content

pulley: ExtendedOpcode::MAX off-by-one causes UB in safe new() / Disassembler #12813

@gaynor-anthropic

Description

@gaynor-anthropic

ExtendedOpcode::MAX is computed as the variant count (N), not the max discriminant (N-1). new() checks bytes <= MAX, so new(N) passes validation and reaches transmute(N) on an invalid discriminant . Reachable via Disassembler::disassemble_all on 3 crafted bytes; Miri confirms. Niche optimisation currently masks it (Some(transmute(N)) bit-aliases None), but that's a layout coincidence.

Minimal PoC:

$ cat src/main.rs
//! `ExtendedOpcode::MAX` is the variant *count* (310), not the max
//! discriminant (309). `new()` checks `<= MAX`, so `new(310)` reaches
//! `transmute(310)` — UB. Miri catches it; natively it niche-collapses
//! to `None` by coincidence.
//!
//!     cargo +nightly miri run
#![forbid(unsafe_code)]

use pulley_interpreter::disas::Disassembler;
use pulley_interpreter::opcode::{ExtendedOpcode, Opcode};

fn main() {
    // Safe public disassembler → SafeBytecodeStream → ExtendedOpcode::new(MAX)
    let bytes = [
        Opcode::ExtendedOp as u8,
        ExtendedOpcode::MAX as u8,
        (ExtendedOpcode::MAX >> 8) as u8,
    ];
    let _ = Disassembler::disassemble_all(&bytes);
}
$ cargo miri run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `/root/.rustup/toolchains/nightly-2025-12-06-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/poc_bug_029`
error: Undefined Behavior: constructing invalid value at .<enum-tag>: encountered 0x0136, but expected a valid enum tag
   --> /root/home/opensrc/wasmtime/pulley/src/opcode.rs:104:18
    |
104 |         unsafe { core::mem::transmute(byte) }
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
    = note: BACKTRACE:
    = note: inside `pulley_interpreter::ExtendedOpcode::unchecked_new` at /root/home/opensrc/wasmtime/pulley/src/opcode.rs:104:18: 104:44
    = note: inside `pulley_interpreter::ExtendedOpcode::new` at /root/home/opensrc/wasmtime/pulley/src/opcode.rs:91:27: 91:53
    = note: inside `pulley_interpreter::decode::decode_one_extended::<pulley_interpreter::disas::Disassembler<'_>>` at /root/home/opensrc/wasmtime/pulley/src/decode.rs:707:26: 707:51
    = note: inside `pulley_interpreter::decode::Decoder::decode_one::<pulley_interpreter::disas::Disassembler<'_>>` at /root/home/opensrc/wasmtime/pulley/src/decode.rs:601:25: 601:53
    = note: inside `pulley_interpreter::decode::Decoder::decode_all::<'_, pulley_interpreter::disas::Disassembler<'_>>` at /root/home/opensrc/wasmtime/pulley/src/decode.rs:528:26: 528:53
    = note: inside `pulley_interpreter::disas::Disassembler::<'_>::disassemble_all` at /root/home/opensrc/wasmtime/pulley/src/disas.rs:32:9: 32:40
note: inside `main`
   --> src/main.rs:19:13
    |
 19 |     let _ = Disassembler::disassemble_all(&bytes);
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Not a security issue because pully is a tier 2 feature.

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