Skip to content

downcast_mut() derives a &mut from &, which is UB #469

Description

@Shnatsel

Mutable downcasting through context wrappers violates Rust's aliasing rules and causes indefined behavior.

context_chain_downcast / context_downcast derive the returned erased pointer from a shared field reference, but Report::downcast_mut later turns that pointer into &mut T. Safe callers can trigger UB with err.downcast_mut::<HighLevel>(), MidLevel, or LowLevel on a wrapped report. Miri Tree Borrows flags the subsequent safe write as a write through a pointer derived from a frozen shared tag.

Steps to reproduce

miri complains about all 3 of these tests, even under the more permissive Tree Borrows model:

MIRIFLAGS=-Zmiri-tree-borrows cargo +nightly miri test --release

#[test]
fn test_downcast_mut_context_message() {
    let (mut err, dropped) = make_chain();

    // Memory-safety issue: `context_chain_downcast` finds context messages by
    // taking a shared reference to the field, while `Report::downcast_mut`
    // turns the returned erased pointer into `&mut D`. Miri with Tree Borrows
    // rejects the write through this safe mutable downcast because the pointer
    // is derived from a frozen shared tag.
    let high = err.downcast_mut::<HighLevel>().unwrap();
    high.message = "mutated high-level context";
    assert_eq!(high.to_string(), "mutated high-level context");

    assert!(dropped.none());
    drop(err);
    assert!(dropped.all());
}

#[test]
fn test_downcast_mut_inner_context_message() {
    let (mut err, dropped) = make_chain();

    // Memory-safety issue: this asks for a mutable reference to a context
    // message below the outermost wrapper. The lookup recurses through
    // `context_chain_downcast` into `context_downcast`, which returns a pointer
    // derived from a shared field reference before `Report::downcast_mut`
    // creates `&mut MidLevel`. Miri flags the subsequent safe write as UB.
    let mid = err.downcast_mut::<MidLevel>().unwrap();
    mid.message = "mutated mid-level context";
    assert_eq!(mid.to_string(), "mutated mid-level context");

    assert!(dropped.none());
    drop(err);
    assert!(dropped.all());
}

#[test]
fn test_downcast_mut_inner_error() {
    let (mut err, dropped) = make_chain();

    // Memory-safety issue: mutable downcast to the innermost diagnostic also
    // reuses the recursive context-chain lookup. `context_downcast` returns a
    // pointer derived from a shared field reference, then `Report::downcast_mut`
    // creates `&mut LowLevel`; Miri flags the safe write through it as UB.
    let low = err.downcast_mut::<LowLevel>().unwrap();
    low.message = "mutated low-level error";
    assert_eq!(low.to_string(), "mutated low-level error");

    assert!(dropped.none());
    drop(err);
    assert!(dropped.all());
}

Language spec

The Rustonomicon has this to say about turning a & into a &mut:

Transmuting an & to &mut is Undefined Behavior. While certain usages may appear safe, note that the Rust optimizer is free to assume that a shared reference won’t change through its lifetime and thus such transmutation will run afoul of those assumptions. So:

  • Transmuting an & to &mut is always Undefined Behavior.
  • No you can’t do it.
  • No you’re not special.

Credits

The issue was discovered by GPT-5.5 xhigh, which also authored the PoC tests. I've manually verified the findings before reporting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions