You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unsound: unelaborate_derefs substitutes every deref_copy destination by its source place, but at opt-level 0 that destination can be a long-lived user reference or _0. Reads after the source is reassigned see the new value (panicking programs verify as safe), and a returned p.0 crashes #333
unelaborate_derefs, together with extract_elaborated_deref (src/analyze/local_def.rs), handles every _t = deref_copy P the same way:
the statement becomes a Nop;
a body-wide replacement _t → P is recorded and applied with ReplacePlacesVisitor.
That is only valid for the short-lived temporaries that rustc's Derefer pass creates, where _t is used right away and P doesn't change in between. In optimized MIR at the default opt-level, however, rustc's own passes merge user bindings and the return place into these temporaries. The substitution then breaks in two ways.
1. Unsound: the source is reassigned while the reference is still live
fnmain(){let x = 1_i64;let y = 2_i64;letmut t = (&x,0_i64);let r:&i64 = &*t.0;
t = (&y,1);// accepted by NLL: r borrows *t.0, not tassert!(*r == 2);// actually *r == 1}
Thrust says safe, but the real program panics at the assert. With the correct == 1, Thrust reports Unsat, while assert!(false) is still Unsat, so the result isn't vacuous. The optimized MIR is:
_8 = deref_copy (_3.0: &i64); // debug r => _8
_3 = (copy _5, const 1_i64); // _3 reassigned while _8 is live
_6 = copy (*_8); // Thrust reads this as *(_3.0), i.e. y
Writes are redirected the same way:
fnmain(){letmut a = 1_i64;letmut c = 2_i64;{letmut b = Box::new(&mut a);let r:&muti64 = *b;
b = Box::new(&mut c);*r = 5;// writes a}assert!(a == 1 && c == 5);// Thrust: safe; at runtime a == 5, c == 2}
2. Crash: the destination is the return place
fnfirst<'a>(p:(&'amuti64,i64)) -> &'amuti64{ p.0}fnmain(){letmut a = 1i64;let r = first((&mut a,2));*r = 3;assert!(a == 3);}
The body is _0 = deref_copy (_1.0); return. The assignment to _0 is removed, but return reads _0 implicitly, so there is no operand to rewrite. _0 therefore becomes live at entry, and drop_bb_zst_params drops it from the entry type, which panics:
thread 'rustc' panicked at src/rty.rs:292:17:
variable $0 references the parameter being removed
&mut **p with p: &mut &mut i64, and getters like &mut *self.r, crash the same way. Spelling it as let r = p.0; r verifies fine.
This is different from #244. There, GVN at opt-level ≥ 1 rewrites deref_copy to copy and the pattern is missed. Here the pattern matches at opt-level 0 and the substitution itself is wrong.
Suggested fix
Substitute only when _t is a genuine short-lived temporary: it is not RETURN_PLACE, it is used before anything assigns P's base local, and in practice it is used within its own block. Otherwise keep _t as a real local:
_t = copy P when its type is &T (shared refs are modeled by value, so that is a correct snapshot);
a reborrow _t = &mut (*P) when its type is &mut T.
changed the title [-]ICE `variable $0 references the parameter being removed`: `unelaborate_derefs` deletes a `_0 = deref_copy ..` return assignment, so returning a `&mut` read through a projection (`p.0`, `&mut **p`, `&mut *self.r`) crashes[/-][+]Unsound: `unelaborate_derefs` substitutes every `deref_copy` destination by its source place, but at opt-level 0 that destination can be a long-lived user reference or `_0`. Reads after the source is reassigned see the new value (panicking programs verify as `safe`), and a returned `p.0` crashes[/+]on Oct 9, 2026
Activity
[-]ICE `variable $0 references the parameter being removed`: `unelaborate_derefs` deletes a `_0 = deref_copy ..` return assignment, so returning a `&mut` read through a projection (`p.0`, `&mut **p`, `&mut *self.r`) crashes[/-][+]Unsound: `unelaborate_derefs` substitutes every `deref_copy` destination by its source place, but at opt-level 0 that destination can be a long-lived user reference or `_0`. Reads after the source is reassigned see the new value (panicking programs verify as `safe`), and a returned `p.0` crashes[/+]