Skip to content

UB inconsistency when derefing a place in a closure #2121

Open

Description

@Nadrieril

This was initially reported as a Miri bug: rust-lang/miri#4258

Under Miri, these two uses of **r report inconsistent UB:

use std::mem::transmute;

fn main() {
    let r: &&u32 = unsafe {
        let x = 42;
        transmute(&&x)
    };

    // no UB detected
    let f = || { let _ = **r; };
    f();
    
    // UB due to the inner deref
    let _ = **r;
}

The closure capture rules consider the whole of **r to be a capture path, and says that "Closures only capture data that needs to be read". Given the rest of that section, it's fair to interpret the usage above as not reading data hence not requiring a capture. This is what the compiler does today.

But in the actual MIR lowering of the second case, we consider that the double-deref does constitute a read of the inner &u32, which triggers UB.

There's a tension here: the read of the inner &u32 doesn't matter in safe code, so I see why closure capture rules would consider that it doesn't matter. But this does introduce a surprising behavior difference. Can we resolve this somehow?

Activity

  1. changed the title [-]UB inconsistency when mentioning a place in a closure[/-] [+]UB inconsistency when derefing a place in a closure[/+] on Dec 19, 2025
  2. RalfJung commented on Dec 19, 2025

    @RalfJung
    Member

    @rustbot label +I-lang-nominated

    FWIW I would argue that let _ = **r "needs" to read *r to even be able to create the place **r, which is then discarded without being read from.

  3. jackh726 commented on Jan 21, 2026

    @jackh726
    Member

    Without knowing the actual impl details here, I could imagine that there is likely some very small change that would cause **r to be treated as captured r for closure captures - but that has implications outside of if this is considered UB or not. There may be a different (possible more in-depth) change that we could do where it's like a "fake" capture or something.

    Either way, if we change something here we probably want a crater run.

  4. added
    I-lang-radarItems that are on lang's radar and will need eventual work or consideration.
    on Jan 21, 2026
  5. tmandry commented on Jan 21, 2026

    @tmandry
    Member

    I think the two should be consistent but I don't have an opinion yet on which behavior to pick.

    @RalfJung It sounds like you're saying we could choose to make neither of them UB?

  6. theemathas commented on Jan 22, 2026

    @theemathas
    Contributor

    This code currently doesn't compile. Should it?

    fn main() {
        let a = Box::new(Box::new(String::new()));
        let _a = **a;
        let _closure = || { let _ = a; };
    }
  7. RalfJung commented on Jan 22, 2026

    @RalfJung
    Member

    @RalfJung It sounds like you're saying we could choose to make neither of them UB?

    I think I misunderstood the code...

    The let _ = **r; errors because it is a PlaceMention of **r and we said that PlaceMention requires the place to be non-dangling. It seems that somehow the same code in a closure does not generate a PlaceMention?

  8. Nadrieril commented on Jan 24, 2026

    @Nadrieril
    MemberAuthor

    @RalfJung The closure case indeed doesn't generate a PlaceMention because the closure capture rules say that there's nothing to capture in this case. I think when we try to emit the PlaceMention that let _ = ... should entail, we observe that no place was captured and therefore do nothing, same as if we had let _ = a; .

    @theemathas I think that's different; minimized:

    fn main() {
        let a = String::new();
        drop(a);
        let _closure = || { let _ = a; }; // ERROR value used here after move
    }

    This isn't about PlaceMention since a plain let _ = a; outside the closure would be fine. Instead, it's a general property that it's an error for a closure to mention a non-initialized place, so the compiler inserts FakeReads before closure creation to prevent that. I guess it's not very consistent given that let (_x, _) = a; would be fine with a.1 being uninitialized 🤔 EDIT: actually no, it doesn't like that either (play)

  9. theemathas commented on Jan 24, 2026

    @theemathas
    Contributor

    @Nadrieril This compiles in edition 2021+ though:

    fn main() {
        let a = (String::new(), String::new());
        drop(a.0);
        let _closure = || { let _ = a.1; };
    }
  10. Nadrieril commented on Jan 24, 2026

    @Nadrieril
    MemberAuthor

    @theemathas mind opening a new issue to discuss that?

  11. RalfJung commented on Jan 24, 2026

    @RalfJung
    Member

    The let _ = **r; errors because it is a PlaceMention of **r and we said that PlaceMention requires the place to be non-dangling. It seems that somehow the same code in a closure does not generate a PlaceMention?

    This is wrong of course, I misremembered... we said that PlaceMention leads to the place being computed, which has the same requirements as constructing a raw pointer. For **r that means that the first deref needs to happen (what you call the "outer" deref, though it is the inner of the two *, which is why I got confused initially) to determine the address of the place that is being mentioned.

    The closure case indeed doesn't generate a PlaceMention because the closure capture rules say that there's nothing to capture in this case. I think when we try to emit the PlaceMention that let _ = ... should entail, we observe that no place was captured and therefore do nothing, same as if we had let _ = a; .

    That seems quite inconsistent that we generate PlaceMention outside closures but not inside closures. Or more fundamentally, it is very surprising to me that this closure does not capture r at all. I would argue r is clearly being read -- it is being read to construct a place (that we then do not read from).

  12. digama0 commented on Jan 29, 2026

    @digama0

    Relatively hot take during t-opsem meeting: the **r in the closure implies it should be captured by-reference, and the fact that it isn't looks like a bug in closure capture.

  13. RalfJung commented on Jan 29, 2026

    @RalfJung
    Member

    Yeah the general vibes in the opsem meeting was that "of course" this should behave the same inside and outside a closure.

    This becomes more interesting with the following example:

    use std::mem::MaybeUninit;
    
    fn main() { unsafe {
        let mut r = &0i32;
        // Make `r` uninitialized.
        (&raw mut r).cast::<MaybeUninit<*const i32>>().write(MaybeUninit::uninit());
        
        // Miri accepts this 
        let f = || { let _ = *r; };
        f();
        // and this
        let f = move || { let _ = *r; };
        f();
    
        // But this is UB
        let _ = *r;
    } }

    The requirement for r in let_ = *r; is that it actually holds a pointer that satisfies the language invariant, i.e., it cannot be uninitialized. This requirement gets lost via the closure. So how should r get captured to properly reflect this?

  14. added
    P-lang-drag-2Lang team prioritization drag level 2.
    and removed
    P-lang-drag-1Lang team prioritization drag level 1.
    on Feb 4, 2026
  15. added
    P-lang-drag-3Lang team prioritization drag level 3.
    and removed
    P-lang-drag-2Lang team prioritization drag level 2.
    on May 6, 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

    I-lang-nominatedNominated for discussion during a lang team meeting.I-lang-radarItems that are on lang's radar and will need eventual work or consideration.P-lang-drag-3Lang team prioritization drag level 3.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions