镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

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

Open

Description

@coord-e

Summary

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

fn main() {
    let x = 1_i64;
    let y = 2_i64;
    let mut t = (&x, 0_i64);
    let r: &i64 = &*t.0;
    t = (&y, 1);          // accepted by NLL: r borrows *t.0, not t
    assert!(*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:

fn main() {
    let mut a = 1_i64;
    let mut c = 2_i64;
    {
        let mut b = Box::new(&mut a);
        let r: &mut i64 = *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

fn first<'a>(p: (&'a mut i64, i64)) -> &'a mut i64 { p.0 }
fn main() { let mut 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.

Activity

  1. 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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