Skip to content

WF checks on closure arguments.#151510

Open
LorrensP-2158466 wants to merge 4 commits intorust-lang:mainfrom
LorrensP-2158466:wf-closure-arg
Open

WF checks on closure arguments.#151510
LorrensP-2158466 wants to merge 4 commits intorust-lang:mainfrom
LorrensP-2158466:wf-closure-arg

Conversation

@LorrensP-2158466
Copy link
Contributor

@LorrensP-2158466 LorrensP-2158466 commented Jan 22, 2026

Fixes #104478.

Enables WF checks on the arguments of a closure.

The now removed function ascribe_user_type_skip_wf mentioned that skipping WF was done due to backwards compatibility reasons.

FCP: WF checks on closure arguments

Summary

On stable, we only perform WF-checks on closure arguments when that argument is actually used. This causes non WF code to still compile.

This was necessary as properly checking them for WF caused breakage due to #104477, see #104478. As we've fixed #104477 in #148329 we can now always check closure arguments for well-formedness while borrowchecking the closure body.

This causes a compilation error when a used type is not WF. This is an intentional breakaging change.

Example

Consider the example from the linked issue of this PR.

struct MyTy<T: Trait>(T);
trait Trait {}
impl Trait for &'static str {}
fn wf<T>(_: T) {}

fn test() {
    let _: for<'x> fn(MyTy<&'x str>) = |_| {};
    
    let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
}

Currently, on stable, this fails only because of the second closure:

error[E0521]: borrowed data escapes outside of closure
 --> src/lib.rs:9:44
  |
9 |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
  |                                         -  ^^^^^
  |                                         |  |
  |                                         |  `x` escapes the closure body here
  |                                         |  argument requires that `'1` must outlive `'static`
  |                                         `x` is a reference that is only valid in the closure body
  |                                         has type `MyTy<&'1 str>`

We don't emit an error for the first closure because the argument is not used in the closure body, even though it is also not WF.

With this PR we now error for both closures:

error: lifetime may not live long enough
  --> src/lib.rs:7:44
   |
LL |     let _: for<'x> fn(MyTy<&'x str>) = |_| {};
   |                                         ^
   |                                         |
   |                                         has type `MyTy<&'1 str>`
   |                                         requires that `'1` must outlive `'static`

error: lifetime may not live long enough
  --> src/lib.rs:9:44
   |
LL |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
   |                                         ^
   |                                         |
   |                                         has type `MyTy<&'1 str>`
   |                                         requires that `'1` must outlive `'static`

Design

The specifics of WF checking is not documented in the reference.

Coverage

See the tests added in this PR for things which now don't compile.

Outstanding bugs

There is a related unsoundness in #151637, which is not fixed by this PR. This is due to closures using implied bounds only involving external regions and needs to be fixed separately. See the WIP work in #153027.

Breaking changes

This change is intentionally breaking, affecting jobsteal. See the crater triage by @theemathas #151510 (comment)

History

WF checks on closure arguments was left as future work in #101947 and tracked in #104478.

r? @lcnr

@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. labels Jan 22, 2026
Comment on lines +10 to +19
error: lifetime may not live long enough
--> $DIR/check-wf-of-closure-args.rs:13:41
|
LL | let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x);
| ^
| |
| has type `MyTy<&'1 str>`
| requires that `'1` must outlive `'static`

error: aborting due to 2 previous errors
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that the error message is a bit worse now, compared to the old:

error[E0521]: borrowed data escapes outside of closure
  --> src/lib.rs:10:44
   |
10 |     let _: for<'x> fn(MyTy<&'x str>) = |x| wf(x); // FAIL
   |                                         -  ^^^^^
   |                                         |  |
   |                                         |  `x` escapes the closure body here
   |                                         |  argument requires that `'1` must outlive `'static`
   |                                         `x` is a reference that is only valid in the closure body
   |                                         has type `MyTy<&'1 str>`

| - - has type `&'1 i32`
| |
| has type `&Cell<&'2 i32>`
| has type `&'2 Cell<&i32>`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also looks wrong, is it due to the removed normalization?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have for<'2, '1, '_> fn(&'2 Cell<&'_ i32>, &'1 i32)

we have the requirements '_: '2 and '1: '_ which does imply '1: '2 :3

not a good error though xd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the name right now, so TODO.

Also, this test, when run with current nightly, indeeds performs a use-after-free. But now we error on it, it can thus be removed to this:

use std::sync::OnceLock;

type Payload = Box<i32>;

static STORAGE: OnceLock<& ()> = OnceLock::new();

trait Store {}
impl Store for &'static Payload {}

struct MyTy<T: Store>(T);

trait IsFn {}
impl IsFn for for<'x> fn(&'x Payload) -> MyTy<&'x Payload> {}

fn bar<F: IsFn>(_: F) {}

fn main() {
    bar::<for<'a> fn(&'a Payload) -> MyTy<&'a Payload>>(|x| unsafe {
        std::mem::transmute::<&Payload, MyTy<&Payload>>(x)
    });
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether we need this test at all 🤷 imo you could remove it...

If you keep it, please keep the full repro, could call it sth like check-wf-of-closure-args-exploit.rs or sth 🤷

@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@LorrensP-2158466
Copy link
Contributor Author

CI x86_64-gnu-tools failed with an error I don't understand:

..............................F................... (50/146)
.................................................. (100/146)
..............................................     (146/146)
======== tests/rustdoc-gui/globals.goml ========
[ERROR] line 14: The following errors happened: [Property named `"searchIndex"` doesn't exist]: for command `assert-window-property-false: {"searchIndex": null}`
    at <file:///checkout/obj/build/x86_64-unknown-linux-gnu/test/rustdoc-gui/doc/test_docs/index.html?search=Foo>
======== tests/rustdoc-gui/search-result-display.goml ========
[WARNING] line 39: Delta is 0 for "x", maybe try to use `compare-elements-position` instead?

@lcnr
Copy link
Contributor

lcnr commented Jan 24, 2026

@bors try

@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Jan 24, 2026
@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 25, 2026

☀️ Try build successful (CI)
Build commit: 1baf923 (1baf923b9c3a455162afe43e18647f494c1a4b73, parent: 021fc25b7a48f6051bee1e1f06c7a277e4de1cc9)

@lcnr
Copy link
Contributor

lcnr commented Jan 26, 2026

@craterbot check

@craterbot
Copy link
Collaborator

👌 Experiment pr-151510 created and queued.
🤖 Automatically detected try build 1baf923
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot added S-waiting-on-crater Status: Waiting on a crater run to be completed. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 26, 2026
@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 27, 2026

☔ The latest upstream changes (presumably #151701) made this pull request unmergeable. Please resolve the merge conflicts.

@craterbot
Copy link
Collaborator

🚧 Experiment pr-151510 is now running

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot
Copy link
Collaborator

🎉 Experiment pr-151510 is completed!
📊 17 regressed and 6 fixed (795068 total)
📊 2202 spurious results on the retry-regressed-list.txt, consider a retry1 if this is a significant amount.
📰 Open the summary report.

⚠️ If you notice any spurious failure please add them to the denylist!
ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

Footnotes

  1. re-run the experiment with crates=https://crater-reports.s3.amazonaws.com/pr-151510/retry-regressed-list.txt

@craterbot craterbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-crater Status: Waiting on a crater run to be completed. labels Feb 3, 2026
@theemathas
Copy link
Contributor

@theemathas
Copy link
Contributor

theemathas commented Feb 3, 2026

Crater results:

The jobsteal regression seems to not directly involve closure arguments, which is strange. And the errors for the cw-storage-plus regression mention a lifetime '1 without pointing out where that lifetime is, which is confusing.

@lcnr
Copy link
Contributor

lcnr commented Feb 10, 2026

@LorrensP-2158466 can you look into how to fix jobsteal without opening a PR yet. Also, I would like to FCP merge this change, are you interested in writing the FCP proposal here?

@LorrensP-2158466
Copy link
Contributor Author

@lcnr Will do, both!

@@ -0,0 +1,48 @@
// The same as check-wf-of-closure-args-complex, but this example causes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "the same as check-wf-of-closure-args-complex mean"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether we need this test at all 🤷 imo you could remove it...

If you keep it, please keep the full repro, could call it sth like check-wf-of-closure-args-exploit.rs or sth 🤷

@lcnr

This comment was marked as outdated.

@lcnr lcnr added the T-types Relevant to the types team, which will review and decide on the PR/issue. label Mar 4, 2026
@lcnr
Copy link
Contributor

lcnr commented Mar 4, 2026

@rfcbot fcp merge types

see PR description

@rust-rfcbot
Copy link
Collaborator

rust-rfcbot commented Mar 4, 2026

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

Concerns:

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 Mar 4, 2026
@lcnr
Copy link
Contributor

lcnr commented Mar 4, 2026

Thank you @LorrensP-2158466 for writing the PR and most of the FCP and @theemathas for the crater triage

@rfcbot concern jobsteal crater regression fix

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. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. 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-types Relevant to the types team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

closures accept ill-formed inputs overly restrictive closure borrow-checking

7 participants