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.
Mutable downcasting through context wrappers violates Rust's aliasing rules and causes indefined behavior.
context_chain_downcast/context_downcastderive the returned erased pointer from a shared field reference, butReport::downcast_mutlater turns that pointer into&mut T. Safe callers can trigger UB witherr.downcast_mut::<HighLevel>(),MidLevel, orLowLevelon 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 --releaseLanguage spec
The Rustonomicon has this to say about turning a
&into a&mut:Credits
The issue was discovered by GPT-5.5 xhigh, which also authored the PoC tests. I've manually verified the findings before reporting.