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
UB inconsistency when derefing a place in a closure #2121
Under Miri, these two uses of **r report inconsistent UB:
use std::mem::transmute;fnmain(){let r:&&u32 = unsafe{let x = 42;transmute(&&x)};// no UB detectedlet f = || {let _ = **r;};f();// UB due to the inner dereflet _ = **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?
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
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.
@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?
@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; .
fnmain(){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)
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).
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.
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;fnmain(){unsafe{letmut r = &0i32;// Make `r` uninitialized.(&rawmut r).cast::<MaybeUninit<*consti32>>().write(MaybeUninit::uninit());// Miri accepts this let f = || {let _ = *r;};f();// and thislet f = move || {let _ = *r;};f();// But this is UBlet _ = *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?
Activity
[-]UB inconsistency when mentioning a place in a closure[/-][+]UB inconsistency when derefing a place in a closure[/+]RalfJung commentedon Dec 19, 2025
@rustbot label +I-lang-nominated
FWIW I would argue that
let _ = **r"needs" to read*rto even be able to create the place**r, which is then discarded without being read from.jackh726 commentedon Jan 21, 2026
Without knowing the actual impl details here, I could imagine that there is likely some very small change that would cause
**rto be treated as capturedrfor 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.
tmandry commentedon Jan 21, 2026
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?
theemathas commentedon Jan 22, 2026
This code currently doesn't compile. Should it?
RalfJung commentedon Jan 22, 2026
I think I misunderstood the code...
The
let _ = **r;errors because it is a PlaceMention of**rand 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?Nadrieril commentedon Jan 24, 2026
@RalfJung The closure case indeed doesn't generate a
PlaceMentionbecause the closure capture rules say that there's nothing to capture in this case. I think when we try to emit thePlaceMentionthatlet _ = ...should entail, we observe that no place was captured and therefore do nothing, same as if we hadlet _ = a;.@theemathas I think that's different; minimized:
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 insertsFakeReads before closure creation to prevent that. I guess it's not very consistent given thatlet (_x, _) = a;would be fine witha.1being uninitialized 🤔 EDIT: actually no, it doesn't like that either (play)theemathas commentedon Jan 24, 2026
@Nadrieril This compiles in edition 2021+ though:
Nadrieril commentedon Jan 24, 2026
@theemathas mind opening a new issue to discuss that?
RalfJung commentedon Jan 24, 2026
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
**rthat 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.That seems quite inconsistent that we generate
PlaceMentionoutside closures but not inside closures. Or more fundamentally, it is very surprising to me that this closure does not capturerat all. I would argueris clearly being read -- it is being read to construct a place (that we then do not read from).digama0 commentedon Jan 29, 2026
Relatively hot take during t-opsem meeting: the
**rin the closure implies it should be captured by-reference, and the fact that it isn't looks like a bug in closure capture.RalfJung commentedon Jan 29, 2026
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:
The requirement for
rinlet_ = *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 shouldrget captured to properly reflect this?