Skip to content

make closures act like MaybeDangling - #160745

Merged
rust-bors[bot] merged 4 commits into
rust-lang:mainfrom
RalfJung:closure-maybe-dangling
Sep 5, 2026
Merged

make closures act like MaybeDangling#160745
rust-bors[bot] merged 4 commits into
rust-lang:mainfrom
RalfJung:closure-maybe-dangling

Conversation

@RalfJung

@RalfJung RalfJung commented Aug 8, 2026

Copy link
Copy Markdown
Member

View all comments

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and Boxes passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

  • The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See Scoped threads violate 'dereferenceable for function call' requirement of references #101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
  • Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently unsound. The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:

fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}

Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to MaybeDangling. This also lets us simplify ManuallyDrop, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes #159443

@rustbot

rustbot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

miri is developed in its own repository. If the Miri part of this change can be broken out, consider making this change to rust-lang/miri instead. However, if Miri needs adjusting for rustc changes, just ignore this message.

cc @rust-lang/miri

Some changes occurred to the CTFE machinery

cc @oli-obk, @lcnr

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Aug 8, 2026
@rustbot

rustbot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

r? @clarfonthey

rustbot has assigned @clarfonthey.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: libs
  • libs expanded to 12 candidates
  • Random selection from JohnTitor, Mark-Simulacrum, clarfonthey, nia-e

@RalfJung

RalfJung commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

@rfcbot merge opsem

@rust-rfcbot

rust-rfcbot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

@RalfJung has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rust-rfcbot rust-rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Aug 8, 2026
@RalfJung RalfJung added the T-opsem Relevant to the opsem team label Aug 8, 2026
@rust-log-analyzer

This comment has been minimized.

@RalfJung
RalfJung force-pushed the closure-maybe-dangling branch from 75383ed to 932a86d Compare August 8, 2026 11:56
@rust-log-analyzer

This comment has been minimized.

@RalfJung
RalfJung force-pushed the closure-maybe-dangling branch from 932a86d to a13be5a Compare August 8, 2026 12:55
@rust-log-analyzer

This comment has been minimized.

@RalfJung
RalfJung force-pushed the closure-maybe-dangling branch 2 times, most recently from e3c49f3 to d919fe5 Compare August 8, 2026 15:00
@clarfonthey

Copy link
Copy Markdown
Contributor

r? compiler

This isn't really a libs change.

@rustbot rustbot assigned nnethercote and unassigned clarfonthey Aug 8, 2026
@RalfJung RalfJung added the needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. label Aug 8, 2026
@CAD97

CAD97 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

@rustbot reviewed

The unpin generators case could theoretically be solved by making generators specifically !UnsafeUnpin. Even if fix the opsem by giving all closures MaybeDangling semantics, this could arguably still be a correct (removal of) application of the autotrait.

FnOnce closures can't be used after they're consumed, so the "morally correct" fix is that usage sites that could logically move from impl FnOnce should be wrapping it in ManuallyDrop so the compiler can know that this is happening.

But in the face of std getting it wrong1, and of unpin generators hitting another very subtle case of not being logically pinned but still needing to be (logically) wrapped in UnsafePinned, I agree that giving all closures (fixed) ManuallyDrop (i.e. MaybeDangling) semantics

Furthermore, I strongly suspect that giving closures MaybeDangling semantics won't have an observable impact on performance. (Though this assumption should be tested.) Namely, because uses of closures will either be monomorphic and able to re-infer the stronger properties that hold; or polymorphic, need to be conservative, and the opportunity cost largely dominated by the indirect call anyway.

Footnotes

  1. And notably, IIRC, the version that got it wrong and was fixed with MaybeUninit didn't use ManuallyDrop despite it being stable, which is intended to have semantics that would also fix the soundness pitfall. (Although it didn't yet at the time.)

@CAD97

CAD97 commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

I almost forgot the strongest rationale: there's the really subtle case where you might think you're capturing a pointer but actually capture a (potentially mutable) reference instead because of how place capture rules work.

fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(|| {
        drop(unsafe { Box::from_raw(&raw mut *p) });
    });
}

Note: I could've sworn that having ptr: *mut i32 and using it as &mut *ptr would capture *ptr by-mut instead of capturing ptr by-move. That footgun was extremely dangerous, so I'm glad we fixed it already and I just forgot that we did.

The remaining instance of this style of footgun is much less potent than the one I recall existing previously, but combined with the other evidence that retagging closures' captures when retagging the impl FnOnce is fraught with subtle footguns that the most experienced Rust programmers did miss originally, the practical decision is to merge this change.

@RalfJung

RalfJung commented Aug 8, 2026

Copy link
Copy Markdown
Member Author

The unpin generators case could theoretically be solved by making generators specifically !UnsafeUnpin.

That would not help. The problem is the retag that happens when we move the generator. At that point it is not behind any kind of reference so no amount of UnsafeCell/UnsafeUnpin makes a difference.

Furthermore, I strongly suspect that giving closures MaybeDangling semantics won't have an observable impact on performance.

The example in the OP actually does get noalias currently which we'd lose.
But I doubt that's a common enough pattern to matter.

@nnethercote

Copy link
Copy Markdown
Contributor

I am not a good reviewer for this. Gonna guess a better one, please reassign if this is a bad choice:

r? @saethlin

@rustbot rustbot assigned saethlin and unassigned nnethercote Aug 9, 2026
@rust-rfcbot rust-rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels Aug 9, 2026
@rust-bors rust-bors Bot removed the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Sep 4, 2026
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Sep 4, 2026
…=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See rust-lang#101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](rust-lang#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes rust-lang#159443
rust-bors Bot pushed a commit that referenced this pull request Sep 4, 2026
Rollup of 25 pull requests

Successful merges:

 - #159074 ([PAC] FnAbi, llvm.ptrauth.resign and Session API change (2/8))
 - #159792 (A more readable debug map for IndexMaps)
 - #160745 (make closures act like MaybeDangling)
 - #161895 (std::sys::pal::sgx: fix mismatched alloc/free alignment)
 - #161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - #162072 (Add new Tier-3 target: `powerpc64-sony-ps3`)
 - #162179 (type system const items via direct rhs)
 - #162277 (Introduce `rustc_middle::middel::resolve`)
 - #162285 (box: fixup map/try_map deallocate calls)
 - #162286 (string: don't unwind prematurely)
 - #162289 (alloc: a bunch of safety comments)
 - #162292 (Update `askama` version to `0.16.1`)
 - #160509 (Remove `RegionExt`; move methods to `Region` in `rustc_type_ir`)
 - #160906 (Suggest usize instead of placeholder type for array length constants)
 - #160936 (traits: Represent live alias arguments as bitsets)
 - #161400 (Improve diagnostics for references to closures)
 - #161656 (Suggest mutable references for FnMut closure arguments)
 - #161711 (Add more splat fn type tests)
 - #161786 (Make `tcx.def_id_partial_cmp` public)
 - #161953 (sanitizers: Implicitly disable mutually exclusive sanitizers)
 - #162155 (add suggestion for `rustc_allowed_through_unstable_modules` attribute)
 - #162212 (Implement `Rng` for `Box`)
 - #162246 (Fix incorrect meta span)
 - #162266 (std: fix typo)
 - #162291 (Add regression test from 1.98.1)
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Sep 4, 2026
…=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See rust-lang#101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](rust-lang#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes rust-lang#159443
GuillaumeGomez added a commit to GuillaumeGomez/rust that referenced this pull request Sep 4, 2026
…=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See rust-lang#101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](rust-lang#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes rust-lang#159443
rust-bors Bot pushed a commit that referenced this pull request Sep 4, 2026
Rollup of 27 pull requests

Successful merges:

 - #159074 ([PAC] FnAbi, llvm.ptrauth.resign and Session API change (2/8))
 - #159792 (A more readable debug map for IndexMaps)
 - #160745 (make closures act like MaybeDangling)
 - #161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - #162030 (Prevent `--test` to be used in `rustdoc-html` testsuite)
 - #162072 (Add new Tier-3 target: `powerpc64-sony-ps3`)
 - #162179 (type system const items via direct rhs)
 - #162262 (Avoid manually instantiating some binders in error reporting with `-Znext-solver`)
 - #162277 (Introduce `rustc_middle::middel::resolve`)
 - #162285 (box: fixup map/try_map deallocate calls)
 - #162286 (string: don't unwind prematurely)
 - #162289 (alloc: a bunch of safety comments)
 - #162290 (abby test DSL: AliasTyOutlivesViaEnv)
 - #162292 (Update `askama` version to `0.16.1`)
 - #160509 (Remove `RegionExt`; move methods to `Region` in `rustc_type_ir`)
 - #160906 (Suggest usize instead of placeholder type for array length constants)
 - #160936 (traits: Represent live alias arguments as bitsets)
 - #161400 (Improve diagnostics for references to closures)
 - #161656 (Suggest mutable references for FnMut closure arguments)
 - #161711 (Add more splat fn type tests)
 - #161786 (Make `tcx.def_id_partial_cmp` public)
 - #161953 (sanitizers: Implicitly disable mutually exclusive sanitizers)
 - #162155 (add suggestion for `rustc_allowed_through_unstable_modules` attribute)
 - #162212 (Implement `Rng` for `Box`)
 - #162246 (Fix incorrect meta span)
 - #162266 (std: fix typo)
 - #162291 (Add regression test from 1.98.1)
Zalathar added a commit to Zalathar/rust that referenced this pull request Sep 5, 2026
…=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See rust-lang#101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](rust-lang#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes rust-lang#159443
JonathanBrouwer added a commit to JonathanBrouwer/rust that referenced this pull request Sep 5, 2026
…=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See rust-lang#101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](rust-lang#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes rust-lang#159443
rust-bors Bot pushed a commit that referenced this pull request Sep 5, 2026
Rollup of 14 pull requests

Successful merges:

 - #162324 (miri subtree update)
 - #162170 (bootstrap: use target's LLVM libdir when cross-compiling)
 - #158312 (Adds support for AArch64 SVE to inline assembly)
 - #159792 (A more readable debug map for IndexMaps)
 - #160745 (make closures act like MaybeDangling)
 - #161263 (break rustc_expand-rustc_middle dependency)
 - #161895 (std::sys::pal::sgx: fix mismatched alloc/free alignment)
 - #161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - #161397 (coverage: Tidy tests and add some new ones)
 - #161616 (Report precondition violation for `<usize as SliceIndex>::get_unchecked` in const-eval)
 - #162248 (Add regression test for unsized const parameter default ICE)
 - #162250 (Fix hashing of span end columns in incremental compilation)
 - #162265 (cargotest: add lockfiles)
 - #162318 (bootstrap: Fix broken path for `./x doc compiler/rustc --open`)
@rust-bors
rust-bors Bot merged commit 0157777 into rust-lang:main Sep 5, 2026
13 checks passed
@rustbot rustbot added this to the 1.100.0 milestone Sep 5, 2026
rust-bors Bot pushed a commit that referenced this pull request Sep 5, 2026
Rollup merge of #160745 - RalfJung:closure-maybe-dangling, r=WaffleLapkin

make closures act like MaybeDangling

This makes closures (and types like them: coroutines and coroutine closures) act like MaybeDangling. This means that the aliasing model will entirely ignore references and `Box`es passed around as closure captures, removing a pretty subtle footgun that has already caused multiple soundness issues:

- The standard library thread spawning logic was unsound because it passed around arbitrary user data in a closure capture. See #101983 for details. I doubt that this is the only such unsoundness in the ecosystem, this is just very hard to find -- you need to not only run your code in Miri but also pass very specific types through your API to trigger the UB.
- Movable (unpinned) generators can contain mutable references that are reborrowed from other references stored in the same generator. This is currently [unsound](#159443). The only way this is sound is if the reborrowed-from references are inside MaybeDangling; without this, moving the generator (which retags its contents) invalidates the reborrowed reference. So at least for generators, we have to do this change anyway one way or another.

Here's an example of code that no longer has UB under this PR:
```rust
fn invoke(f: impl FnOnce()) {
    f()
}

fn main() {
    let p = Box::leak(Box::new(0i32));
    invoke(move || {
        drop(unsafe { Box::from_raw(p) });
    });
}
```
Basically, what we are establishing here is that immediately invoking a closure should be (almost) equivalent to just inlining its body. (There is still a caveat here in that if you capture things that violate their validity invariant, the inlined body might not care but immediately invoking the closure will. But at least for all the subtle questions around aliasing, the two will be equivalent under this PR.)

Overall I think the fact that moving a closure / generator will alter its contents (by retagging) is just a bit too subtle. It's already subtle for "normal" types but there at least one can see the type with its fields. For closures, that's all entirely implicit. At the same time, the benefit we get from this at the moment is tiny -- we can only actually tell LLVM about these references if the closure/generator has scalar / scalar-pair representation, which can only happen when it captures at most 2 scalar values.

This PR just implements the semantics without updating any docs. I am not sure where we'd document this, given our general lack of documentation around the aliasing model. Still we should t-opsem FCP this PR to ensure we have team consensus for not retagging or requiring reference dereferenceability inside closoures and closure-like types (and then we can involve lang if/when we start making official promises about this).

On the implementation side, I realized this by introducing the notion of "maybe-dangling-like" types, so that the semantics is not hard-coded specifically to `MaybeDangling`. This also lets us simplify `ManuallyDrop`, reducing its field nesting a bit, which should help with some of the query limit issues people encountered when we added the extra field nesting. It also means generators get the desired semantics without increasing their field nesting. Cc @WaffleLapkin

Fixes #159443
@rust-timer

Copy link
Copy Markdown
Collaborator

Note

This PR was benchmarked as part of triage of its containing rollup: triage URL.

Finished benchmarking commit (bb0edf2): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Our benchmarks found a performance regression caused by this PR.
This might be an actual regression, but it can also be just noise.

Next Steps:

  • If the regression was expected or you think it can be justified,
    please write a comment with sufficient written justification, and add
    @rustbot label: +perf-regression-triaged to it, to mark the regression as triaged.
  • If you think that you know of a way to resolve the regression, try to create
    a new PR with a fix for the regression.
  • If you do not understand the regression or you think that it is just noise,
    you can ask the @rust-lang/wg-compiler-performance working group for help (members of this group
    were already notified of this PR).

@rustbot label: +perf-regression
cc @rust-lang/wg-compiler-performance

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
0.4% [0.1%, 0.7%] 71
Regressions ❌
(secondary)
0.3% [0.2%, 0.5%] 26
Improvements ✅
(primary)
-0.3% [-0.4%, -0.2%] 6
Improvements ✅
(secondary)
-0.3% [-0.4%, -0.2%] 9
All ❌✅ (primary) 0.3% [-0.4%, 0.7%] 77

Max RSS (memory usage)

Results (primary 0.9%, secondary 1.5%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
3.1% [1.7%, 4.6%] 2
Regressions ❌
(secondary)
4.5% [2.1%, 7.2%] 6
Improvements ✅
(primary)
-3.6% [-3.6%, -3.6%] 1
Improvements ✅
(secondary)
-7.3% [-7.4%, -7.2%] 2
All ❌✅ (primary) 0.9% [-3.6%, 4.6%] 3

Cycles

Results (secondary 1.2%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
2.4% [2.1%, 2.8%] 3
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
-2.4% [-2.4%, -2.4%] 1
All ❌✅ (primary) - - 0

Binary size

Results (primary -0.2%, secondary -0.0%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
0.1% [0.0%, 0.6%] 10
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
-0.4% [-0.8%, -0.0%] 28
Improvements ✅
(secondary)
-0.0% [-0.0%, -0.0%] 3
All ❌✅ (primary) -0.2% [-0.8%, 0.6%] 38

Bootstrap: missing data
Artifact size: 403.33 MiB -> 404.11 MiB (0.19%)

@rustbot rustbot added the perf-regression Performance regression. label Sep 6, 2026
@RalfJung

RalfJung commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Oh wow. I did not see that coming.

This has to be the ty_and_layout_pointee_info_at change, right? But new is_like_maybe_dangling shouldn't be that expensive...

@RalfJung
RalfJung deleted the closure-maybe-dangling branch September 6, 2026 08:01
@RalfJung

RalfJung commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

It is also, of course, possible that the compiler actually benefited from the noalias/dereferenceable annotations of the closure argument in FnOnce closures.

pull Bot pushed a commit to LeeeeeeM/miri that referenced this pull request Sep 6, 2026
Rollup of 14 pull requests

Successful merges:

 - rust-lang/rust#162324 (miri subtree update)
 - rust-lang/rust#162170 (bootstrap: use target's LLVM libdir when cross-compiling)
 - rust-lang/rust#158312 (Adds support for AArch64 SVE to inline assembly)
 - rust-lang/rust#159792 (A more readable debug map for IndexMaps)
 - rust-lang/rust#160745 (make closures act like MaybeDangling)
 - rust-lang/rust#161263 (break rustc_expand-rustc_middle dependency)
 - rust-lang/rust#161895 (std::sys::pal::sgx: fix mismatched alloc/free alignment)
 - rust-lang/rust#161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - rust-lang/rust#161397 (coverage: Tidy tests and add some new ones)
 - rust-lang/rust#161616 (Report precondition violation for `<usize as SliceIndex>::get_unchecked` in const-eval)
 - rust-lang/rust#162248 (Add regression test for unsized const parameter default ICE)
 - rust-lang/rust#162250 (Fix hashing of span end columns in incremental compilation)
 - rust-lang/rust#162265 (cargotest: add lockfiles)
 - rust-lang/rust#162318 (bootstrap: Fix broken path for `./x doc compiler/rustc --open`)
@RalfJung

RalfJung commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Yeah looks like that is mostly what is happening: #162361.
So, @rust-lang/opsem ... what do we do with this knowledge? Does knowing how it affects the compiler change our mind about treating closures like MaybeDangling?

@RalfJung RalfJung added the I-opsem-nominated Nominated for discussion by the opsem team label Sep 6, 2026
renovate-bot pushed a commit to renovate-bot/rust-lang-_-compiler-builtins that referenced this pull request Sep 7, 2026
Rollup of 14 pull requests

Successful merges:

 - rust-lang/rust#162324 (miri subtree update)
 - rust-lang/rust#162170 (bootstrap: use target's LLVM libdir when cross-compiling)
 - rust-lang/rust#158312 (Adds support for AArch64 SVE to inline assembly)
 - rust-lang/rust#159792 (A more readable debug map for IndexMaps)
 - rust-lang/rust#160745 (make closures act like MaybeDangling)
 - rust-lang/rust#161263 (break rustc_expand-rustc_middle dependency)
 - rust-lang/rust#161895 (std::sys::pal::sgx: fix mismatched alloc/free alignment)
 - rust-lang/rust#161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - rust-lang/rust#161397 (coverage: Tidy tests and add some new ones)
 - rust-lang/rust#161616 (Report precondition violation for `<usize as SliceIndex>::get_unchecked` in const-eval)
 - rust-lang/rust#162248 (Add regression test for unsized const parameter default ICE)
 - rust-lang/rust#162250 (Fix hashing of span end columns in incremental compilation)
 - rust-lang/rust#162265 (cargotest: add lockfiles)
 - rust-lang/rust#162318 (bootstrap: Fix broken path for `./x doc compiler/rustc --open`)
asukaminato0721 pushed a commit to asukaminato0721/rust-analyzer that referenced this pull request Sep 7, 2026
Rollup of 14 pull requests

Successful merges:

 - rust-lang/rust#162324 (miri subtree update)
 - rust-lang/rust#162170 (bootstrap: use target's LLVM libdir when cross-compiling)
 - rust-lang/rust#158312 (Adds support for AArch64 SVE to inline assembly)
 - rust-lang/rust#159792 (A more readable debug map for IndexMaps)
 - rust-lang/rust#160745 (make closures act like MaybeDangling)
 - rust-lang/rust#161263 (break rustc_expand-rustc_middle dependency)
 - rust-lang/rust#161895 (std::sys::pal::sgx: fix mismatched alloc/free alignment)
 - rust-lang/rust#161940 (Promote `wasm32-wasip3` to a tier 2 target)
 - rust-lang/rust#161397 (coverage: Tidy tests and add some new ones)
 - rust-lang/rust#161616 (Report precondition violation for `<usize as SliceIndex>::get_unchecked` in const-eval)
 - rust-lang/rust#162248 (Add regression test for unsized const parameter default ICE)
 - rust-lang/rust#162250 (Fix hashing of span end columns in incremental compilation)
 - rust-lang/rust#162265 (cargotest: add lockfiles)
 - rust-lang/rust#162318 (bootstrap: Fix broken path for `./x doc compiler/rustc --open`)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this PR / Issue. I-opsem-nominated Nominated for discussion by the opsem team perf-regression Performance regression. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. T-opsem Relevant to the opsem team to-announce Announce this issue on triage meeting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undefined behavior when moving a coroutine that reborrows from itself