Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soundness regression with for<'lt> closure having an implied bound from return type #114936

Closed
danielhenrymantilla opened this issue Aug 17, 2023 · 6 comments · Fixed by #118882
Closed
Assignees
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-high High priority regression-from-stable-to-stable Performance or correctness regression from one stable version to another. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@danielhenrymantilla
Copy link
Contributor

danielhenrymantilla commented Aug 17, 2023

Bug originally found by @ast-ral, over Discord, which I've then reduced.

Code

I tried this code:

fn whoops(
    s: String,
    f: impl for<'s> FnOnce(&'s str) -> (&'static str, [&'static &'s (); 0]),
) -> &'static str
{
    f(&s).0
}

I expected to see this happen:

error[E0597]: `s` does not live long enough
 --> <source>:6:7
  |
6 |     f(&s).0
  |     --^^-
  |     | |
  |     | borrowed value does not live long enough
  |     argument requires that `s` is borrowed for `'static`
7 | }
  | - `s` dropped here while still borrowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0597`.

Instead, this happened: code compiles successfully

Version it soundly rejected this code:

1.64.0

Version with soundness regression

1.65.0 and onwards

Proof of unsoundness:

fn static_str_identity()
  -> impl for<'s> FnOnce(&'s str) -> (
        [&'static &'s (); 0],
        &'static str,
    )
{
    |s: &/* 's */ str| ( // <----------------+ inputs restricted to `'s`.
        [], // `: [&'static &'s (); 0]`,     |
            //   thus `'s : 'static` ->------+
            //            +-<----------------+
            //            |
            //            v
        s, // `: &'s str <: &'static str`
    )
}fn main()
{
    let f = static_str_identity();
    let local = String::from("...");
    let s: &'static str = f(&local).1; // <- should be rejected!
    drop(local);
    let _unrelated = String::from("UB!");
    dbg!(s); // <- compiles and prints `"UB!"`
}

@rustbot modify labels: +I-unsound +regression-from-stable-to-stable -regression-untriaged

Observations

  • This does not occur if the implied-lifetime-bounds array is in closure parameter position; so it looks like a bug w.r.t. not properly checking the closure output type implied bounds?

  • -Ztrait-solver=next does not help (as of 2023-08-16)

@danielhenrymantilla danielhenrymantilla added C-bug Category: This is a bug. regression-untriaged Untriaged performance or correctness regression. labels Aug 17, 2023
@rustbot rustbot added needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. I-prioritize Issue: Indicates that prioritization has been requested for this issue. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness regression-from-stable-to-stable Performance or correctness regression from one stable version to another. and removed regression-untriaged Untriaged performance or correctness regression. labels Aug 17, 2023
@apiraino
Copy link
Contributor

@lqd you beat me by one minute :)

cc @lcnr for #99217 being possible cause of this unsoundness?

@apiraino
Copy link
Contributor

WG-prioritization assigning priority (Zulip discussion).

@rustbot label -I-prioritize +P-high +T-compiler -needs-triage

@rustbot rustbot added P-high High priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Aug 17, 2023
@ast-ral
Copy link
Contributor

ast-ral commented Aug 17, 2023

Oh, neat it did turn out to be a new soundness bug instead of a dupe of #25860. Thanks for figuring out this was a regression, @danielhenrymantilla.

@lcnr
Copy link
Contributor

lcnr commented Sep 7, 2023

for

fn whoops<'a, F: for<'s> FnOnce(&'s str) -> (&'static str, [&'static &'s (); 0])>(
    s: &'a str,
    f: F,
) -> &'static str
{
    f(s).0
}

we have a terminator calling extern "rust-call" fn(F, (&'?4 str,)) -> <F as FnOnce<(&'?4 str,)>>::Output {<F as FnOnce<(&'?4 str,)>>::call_once}, i.e. a function which returns <F as FnOnce<(&'?4 str,)>>::Output.

This does not include the implied bounds. Checking that <F as FnOnce<(&'?4 str,)>>::Output is WF requires proving F: FnOnce<(&'?4 str,)> which succeeds without checking the usable implied bounds of the trait-ref. This is very related to other soundness bugs (TODO: link to them)edit: there's a bit more to this than I thought 😅

@BoxyUwU
Copy link
Member

BoxyUwU commented Sep 7, 2023

This soundness bug is effectively only reproducible via the Fn* traits because the impls for it are builtin and so a little screwy. An attempt to write an explicit impl for the builtin Fn* impls would require an explicit bound that 's: 'static holds which is not present on the builtin Fn* impl.

There is an assumption that all it takes for an assoc type to be wf is the where clauses on it and the impl hold, and that all arguments in the traitref are wf. The builtin Fn* impls violate this by not having the required 's: 'static implied bound on the impl so we end up with <F as Fn<...>>::Output being able to be proven wf without proving the implied bounds.

When calling f we check that the fn sig of <F as FnOnce<(&'?4 str,)>>::call_once is wf which is fn(F, (&'?4 str,)) -> <F as Fn<(&'?4 str,)>>::Output. Before #99217 we would check the normalized signature is wf so the return type would be -> (&'static str, [&'static &'s (); 0]) which would cause us to prove 's: 'static. Now that we check the unnormalized signature is wf we end up never seeing the normalized form with the &'static &'s () which introduces the 's: 'static implied bound.

It is generally an invariant of the type system that wf(ty) holding should imply wf(normalize(ty)) also holds. The fact that F::Output can be well formed but (&'static str, [&'static &'s (); 0]) is not, is concerning. In an ideal world proving the Fn* traits would require also proving all the implied bounds hold but right now that is not possible afaik.

A simple fix (albeit hacky until we make implied bounds desugared to actual bounds) would be to make us also check wf(normalized(sig)) not just wf(sig).

@lcnr lcnr added T-types Relevant to the types team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Oct 4, 2023
@spastorino spastorino assigned spastorino and unassigned spastorino Dec 11, 2023
bors added a commit to rust-lang-ci/rust that referenced this issue Dec 13, 2023
…=<try>

Check normalized call signature for WF in mir typeck

Unfortunately we don't check that the built-in implementations for `Fn*` traits are actually well-formed in the same way that we do for user-provided impls.

Essentially, when checking a call terminator, we end up with a signature that references an unnormalized `<[closure] as FnOnce<...>>::Output` in its output. That output type, due to the built-in impl, doesn't follow the expected rule that `WF(ty)` implies `WF(normalized(ty))`. We fix this by also checking the normalized signature here.

**See** boxy's detailed and useful explanation comment which explains this in more detail: rust-lang#114936 (comment)

Fixes rust-lang#114936
Fixes rust-lang#118876

r? types
cc `@BoxyUwU` `@lcnr`
bors added a commit to rust-lang-ci/rust that referenced this issue Jan 22, 2024
…=<try>

Check normalized call signature for WF in mir typeck

Unfortunately we don't check that the built-in implementations for `Fn*` traits are actually well-formed in the same way that we do for user-provided impls.

Essentially, when checking a call terminator, we end up with a signature that references an unnormalized `<[closure] as FnOnce<...>>::Output` in its output. That output type, due to the built-in impl, doesn't follow the expected rule that `WF(ty)` implies `WF(normalized(ty))`. We fix this by also checking the normalized signature here.

**See** boxy's detailed and useful explanation comment which explains this in more detail: rust-lang#114936 (comment)

Fixes rust-lang#114936
Fixes rust-lang#118876

r? types
cc `@BoxyUwU` `@lcnr`
bors added a commit to rust-lang-ci/rust that referenced this issue Jan 22, 2024
…=<try>

Check normalized call signature for WF in mir typeck

Unfortunately we don't check that the built-in implementations for `Fn*` traits are actually well-formed in the same way that we do for user-provided impls.

Essentially, when checking a call terminator, we end up with a signature that references an unnormalized `<[closure] as FnOnce<...>>::Output` in its output. That output type, due to the built-in impl, doesn't follow the expected rule that `WF(ty)` implies `WF(normalized(ty))`. We fix this by also checking the normalized signature here.

**See** boxy's detailed and useful explanation comment which explains this in more detail: rust-lang#114936 (comment)

Fixes rust-lang#114936
Fixes rust-lang#118876

r? types
cc `@BoxyUwU` `@lcnr`
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 13, 2024
… r=lcnr

Check normalized call signature for WF in mir typeck

Unfortunately we don't check that the built-in implementations for `Fn*` traits are actually well-formed in the same way that we do for user-provided impls.

Essentially, when checking a call terminator, we end up with a signature that references an unnormalized `<[closure] as FnOnce<...>>::Output` in its output. That output type, due to the built-in impl, doesn't follow the expected rule that `WF(ty)` implies `WF(normalized(ty))`. We fix this by also checking the normalized signature here.

**See** boxy's detailed and useful explanation comment which explains this in more detail: rust-lang#114936 (comment)

Fixes rust-lang#114936
Fixes rust-lang#118876

r? types
cc `@BoxyUwU` `@lcnr`
@bors bors closed this as completed in db9591c Feb 14, 2024
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Feb 14, 2024
Rollup merge of rust-lang#118882 - compiler-errors:normalized-sig-wf, r=lcnr

Check normalized call signature for WF in mir typeck

Unfortunately we don't check that the built-in implementations for `Fn*` traits are actually well-formed in the same way that we do for user-provided impls.

Essentially, when checking a call terminator, we end up with a signature that references an unnormalized `<[closure] as FnOnce<...>>::Output` in its output. That output type, due to the built-in impl, doesn't follow the expected rule that `WF(ty)` implies `WF(normalized(ty))`. We fix this by also checking the normalized signature here.

**See** boxy's detailed and useful explanation comment which explains this in more detail: rust-lang#114936 (comment)

Fixes rust-lang#114936
Fixes rust-lang#118876

r? types
cc ``@BoxyUwU`` ``@lcnr``
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-high High priority regression-from-stable-to-stable Performance or correctness regression from one stable version to another. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants