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

Implied bounds on nested references + variance = soundness hole #25860

Open
aturon opened this issue May 28, 2015 · 40 comments
Open

Implied bounds on nested references + variance = soundness hole #25860

aturon opened this issue May 28, 2015 · 40 comments
Assignees
Labels
A-typesystem C-bug I-unsound P-medium T-lang

Comments

@aturon
Copy link
Member

@aturon aturon commented May 28, 2015

The combination of variance and implied bounds for nested references opens a hole in the current type system:

static UNIT: &'static &'static () = &&();

fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }

fn bad<'a, T>(x: &'a T) -> &'static T {
    let f: fn(&'static &'a (), &'a T) -> &'static T = foo;
    f(UNIT, x)
}

This can likely be resolved by checking well-formedness of the instantiated fn type.


Update from @pnkfelix :

While the test as written above is rejected by Rust today (with the error message for line 6 saying "in type &'static &'a (), reference has a longer lifetime than the data it references"), that is just an artifact of the original source code (with its explicit type signature) running up against one new WF-check.

The fundamental issue persists, since one can today write instead:

static UNIT: &'static &'static () = &&();

fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }

fn bad<'a, T>(x: &'a T) -> &'static T {
    let f: fn(_, &'a T) -> &'static T = foo;
    f(UNIT, x)
}

(and this way, still get the bad behaving fn bad, by just side-stepping one of the explicit type declarations.)

@aturon aturon added A-typesystem I-nominated labels May 28, 2015
@aturon
Copy link
Member Author

@aturon aturon commented May 28, 2015

What's going on here is that foo gets to assume that 'b: 'a, but this isn't actually checked when producing f.

This assumption was thought to be valid because any nested reference type &'a &'b T has to guarantee it for well-formedness. But variance currently allows you to switch around the lifetimes before actually passing in the witnessing argument.

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented May 28, 2015

One solution is to be more aggressive about checking WFedness, but there are other options to consider.

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Jun 2, 2015

triage: P-high T-lang

@rust-highfive rust-highfive added P-high and removed I-nominated labels Jun 2, 2015
@nikomatsakis nikomatsakis self-assigned this Jun 2, 2015
@nikomatsakis nikomatsakis added the T-lang label Jun 2, 2015
@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Jun 2, 2015

I've been working on some proposal(s) that address this bug (among others), so assigning to me.

nikomatsakis added a commit to nikomatsakis/rust that referenced this issue Jun 5, 2015
invariance, this restores soundness to implied bounds (I think). :)

Fixes rust-lang#25860.
nikomatsakis added a commit to nikomatsakis/rust that referenced this issue Jun 8, 2015
invariance, this restores soundness to implied bounds (I think). :)

Fixes rust-lang#25860.
nikomatsakis added a commit to nikomatsakis/rust that referenced this issue Jun 9, 2015
invariance, this restores soundness to implied bounds (I think). :)

Fixes rust-lang#25860.
@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Jun 15, 2015

There is a related problem that doesn't require variance on argument types. The current codebase doesn't check that the expected argument type is well-formed, only the provided one, which is a subtype of the expected one. This is insufficient (but easily rectified).

nikomatsakis added a commit to nikomatsakis/rust that referenced this issue Jul 6, 2015
invariance, this restores soundness to implied bounds (I think). :)

Fixes rust-lang#25860.
@Stebalien
Copy link
Contributor

@Stebalien Stebalien commented Sep 30, 2015

@nikomatsakis I believe you meant to close this.

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Sep 30, 2015

The work needed to close this has not yet landed. It's in the queue though, once we finish up rust-lang/rfcs#1214.

@Stebalien
Copy link
Contributor

@Stebalien Stebalien commented Sep 30, 2015

Sorry, I saw the commit but didn't notice that it hadn't been merged.

@arielb1 arielb1 added the I-unsound label Oct 1, 2015
@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Jan 21, 2016

One interesting thing to note, in light of the new WF checks that have landed with the preliminary implementation of rust-lang/rfcs#1214, is that if we change the definition of fn foo like this (where we rewrite the implied lifetime bounds to be explicitly stated in a where-clause :

fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T where 'b: 'a { v }

then it seems like the code is rejected properly.

I am currently trying to puzzle through whether this kind of "switch from implicit to explicit", assuming it were done as a kind of desugaring by the compiler, if that would be effectively the same as "remove support for contravariance" from the language, or if it represents some other path...

@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Jan 22, 2016

Here's a variation on the original example that retains explicit types (rather than resorting to _ as a type like I did in the description):

fn foo<'a, 'b, T>(_false_witness: Option<&'a &'b ()>, v: &'b T) -> &'a T { v }

fn bad<'c, 'd, T>(x: &'c T) -> &'d T {
    // below is using contravariance to assign `foo` to `f`,
    // side-stepping the obligation to prove `'c: 'd`
    // implicit in the original `fn foo`.
    let f: fn(Option<&'d &'d ()>, &'c T) -> &'d T = foo;
    f(None, x)
}

fn main() {
    fn inner() -> &'static String {
        bad(&format!("hello"))
    }

    let x = inner();
    println!("x: {}", x);
}

@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Jan 22, 2016

I am currently trying to puzzle through whether this kind of "switch from implicit to explicit", assuming it were done as a kind of desugaring by the compiler, if that would be effectively the same as "remove support for contravariance" from the language

After some reflection, I think this does represent a (perhaps appropriately) weakened variation on "remove contravariance from the language"

In particular, if we did the desugaring right (where implied lifetime bounds from a fn-signature would get transformed into where clauses on the fn), then we could still have useful contravariance for fn's that have no such implied bounds.

@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Jan 22, 2016

as an aside, part of me does think that it would be better if we also added some way to write the proposed implied where-clauses explicitly as part of the fn-type. I.e. if you consider the following example:

fn bad<'a, 'b>(g: fn (&'a &'b i32) -> i32) {
    let _f: fn(x: &'b &'b i32) -> i32 = g;
}

under my imagined new system, the above assignment of g to _f would be illegal, due to the implied bounds attached to g (that are not part of the type of _f).

But it might be nice if we could actually write:

fn explicit_types<'a, 'b>(callback: fn (&'a i32, &'b i32) -> i32 where 'a: 'b) {
    ...
}

(note that the where clause there is part of the type of callback)

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Jan 22, 2016

I take it from your comments that removing or restricting the variance is actually considered as a solution to this?

That's surprising me. I cannot see variance at fault here. The observation underlying the issue is that implicit bounds are not properly preserved under variance. The most obvious solution (for me, anyways) would be to make the implicit bounds explicit: If "fn foo<'a, 'b, T>(&'a &'b (), &'b T) -> &'a T" would be considered mere syntactic sugar for "fn foo<'a, 'b, T>(&'a &'b (), &'b T) -> &'a T where 'b: 'a, 'a: 'fn, T: 'b" (using 'fn for the lifetime of the function), and from then on the checks are preserved and adapted appropriately when applying variance or specializing lifetimes, wouldn't that also catch the trouble? (Making implied bounds explicit was also discussed in rust-lang/rfcs#1327, since implied bounds are also a trouble for drop safety.)

@pnkfelix
Copy link
Member

@pnkfelix pnkfelix commented Jan 22, 2016

@RalfJung

I cannot see variance at fault here.

I've been saying the similar things to @nikomatsakis

But the first step for me was to encode the test case in a way that made made it apparent (at least to me) that contravariance is (part of) why we are seeing this happen.

I think that #25860 (comment) is very much the same as what you are describing: 1. Ensure the implicit bounds are actually part of the fn-type itself (even if most users will not write the associated where clause explicitly) and are checked before allowing any calls to a value of a given fn-type, and then 2. fix the subtyping relation between fn-types to ensure that such where-clauses are preserved.

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Jan 22, 2016

I think that #25860 (comment) is very much the same as what you are describing: 1. Ensure the implicit bounds are actually part of the fn-type itself (even if most users will not write the associated where clause explicitly) and are checked before allowing any calls to a value of a given fn-type, and then 2. fix the subtyping relation between fn-types to ensure that such wshere-clauses are preserved.

Yes, that sounds like we're talking about the same idea.

@nikomatsakis
Copy link
Contributor

@nikomatsakis nikomatsakis commented Jan 22, 2016

I do not consider the problem to be fundamentally about contravariance -- but I do consider removing contravariance from fn arguments to be a pragmatic way to solve the problem, at least in the short term. In one of the drafts for RFC rust-lang/rfcs#1214, I wrote the section pasted below, which I think is a good explanation of the problem as I understand it:

Appendix: for/where, an alternative view

The natural generalization of our current fn types is adding the ability to attach arbitrary where clauses to higher-ranked types. That is, a type like for<'a,'b> fn(&'a &'b T) might be written out more explicitly by adding the implied bounds as explicit where-clauses attached to the for:

for<'a,'b> where<'b:'a, T:'b> fn(&'a &'b T)

These where-clauses must be discharged before the fn can be called. They can also be discharged through subtyping, if no higher-ranked regions are involved: that is, there might be a typing rule that allows a where clause to be dropped from the type so long as it can be proven in the current environment (similarly, having fewer where clauses would be a subtype of having more.)

You can view the current notion of implied bounds as being a more limited form of this formalism where the where clauses are exactly the implied bounds of the argument types. However, making the where clauses explicit has some advantages, because it means that one can vary the types of the arguments (via contravariance) while leaving the where clauses intact.

For example, if you had a function:

fn foo<'a,'b,T>(&'a &'b T) { ... }

Under this RFC, the type of this function is:

for<'a,'b> fn(&'a &'b T)

Under the for/where scheme, the full type would be:

for<'a,'b> where<'b:'a, T:'b> fn(&'a &'b T)

Now, if we upcast this type to only accept static data as argument, the where clauses are unaffected:

for<'a,'b> where<'b:'a, T:'b> fn(&'static &'static T)

Viewed this way, we can see why the current fn types (in which one cannot write where clauses explicitly) are invariant: changing the argument types is fine, but it also changes the where clauses, and the new where clauses are not a superset of the old ones, so the subtyping relation does not hold. That is, if we write out the implicit where clauses that result implicitly, we can see why variance on fns causes problems:

for<'a,'b> where<'b:'a, T:'b> fn(&'a &'b T, &'b T) -> &'a T 
<:
for<'a,'b> fn(&'static &'static T, &'b T) -> &'a T
? (today yes, under this RFC no)

Clearly, this subtype relationship should not hold, because the where clauses in the subtype are not implied by the supertype.

@arielb1
Copy link
Contributor

@arielb1 arielb1 commented Jan 22, 2016

To make the point clearer, the principal part of the issue involves higher-ranked types. The rank 0 issue should not be that hard to solve (by requiring WF when instantiating a function - I am a bit surprised we don't do so already).

An example of the problem path (I am intentionally making the problem binder separate from the problem where-clause-container):

fn foo<'a, 'b, T>() -> fn(Option<&'a &'b ()>, &'b T) -> &'a T {
    fn foo_inner<'a, 'b, T>(_witness: Option<&'a &'b ()>, v: &'b T) -> &'a T {
        v
    }
    foo_inner
}

fn bad<'c, 'd, T>(x: &'c T) -> &'d T {
    // instantiate `foo`
    let foo1: for<'a, 'b> fn() -> fn(Option<&'a &'b ()>, &'b T) -> &'a T = foo;
    // subtyping: instantiate `'b <- 'c`
    let foo2: for<'a> fn() -> fn(Option<&'a &'c ()>, &'c T) -> &'a T = foo1;
    // subtyping: contravariantly 'c becomes 'static
    let foo3: for<'a> fn() -> fn(Option<&'a &'static ()>, &'c T) -> &'a T = foo2;
    // subtyping: instantiate `'a <- 'd`
    let foo4: fn() -> fn(Option<&'d &'static ()>, &'c T) -> &'d T = foo3;
    // boom!
    foo4()(None, x)
}

fn main() {
    fn inner() -> &'static String {
        bad(&format!("hello"))
    }

    let x = inner();
    println!("x: {}", x);
}

@XX
Copy link

@XX XX commented Apr 18, 2021

What is the current status of fixing this bug? Rust-haters use this hole to demonstrate that the language is not safe.

@ssokolow
Copy link

@ssokolow ssokolow commented Apr 18, 2021

@XX This one specifically? As someone with no background in programming language theory, I'm curious what makes this one so special.

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Apr 18, 2021

I don't think there was any progress, unfortunately.

That said, haters gonna hate -- I don't think that is a healthy reason to do anything.^^ There are much better reasons.

As someone with no background in programming language theory, I'm curious what makes this one so special.

As someone with background in PL theory, I don't know. ;) I assume it's just because it's one of the oldest still-unresolved soundness bugs.

@stanislav-tkach
Copy link
Contributor

@stanislav-tkach stanislav-tkach commented Apr 21, 2021

I have also seen code snippets exploiting this issue as a response for "safe rust guaranties..." claims more than once.

I assume it's just because it's one of the oldest still-unresolved soundness bugs.

I guess this is true. Personally, I also think that having this issue open for five years is kind of disturbing. 🙃

@Cerber-Ursi
Copy link
Contributor

@Cerber-Ursi Cerber-Ursi commented Jan 17, 2022

Bumping an issue, since it was found (link to the corresponding URLO post) that this problem allows one to easily write transmute in safe code (without any IO, like totally_safe_transmute does, only by exploiting the soundness hole).

@lcnr
Copy link
Contributor

@lcnr lcnr commented Feb 5, 2022

wrote a blog post summarizing this issue and adding an example why I believe removing contravariance is not enough to fix this bug.

https://lcnr.de/blog/diving-deep-implied-bounds-and-variance/

@crlf0710
Copy link
Member

@crlf0710 crlf0710 commented Feb 6, 2022

I took a brief look into this issue, and it seems rust language contravariance rules and borrow checker can do the needed detection.

Here's my inspection, i'm using arielb's example code as starting point. It's obvious that the first incorrect cast is the foo2 to foo3 conversion.

fn bad<'c, 'd, T>(x: &'c T) -> &'d T {
    // instantiate `foo`
    let foo1: for<'a, 'b> fn() -> fn(Option<&'a &'b ()>, &'b T) -> &'a T = foo;
    // subtyping: instantiate `'b <- 'c`
    let foo2: for<'a> fn() -> fn(Option<&'a &'c ()>, &'c T) -> &'a T = foo1;
    // subtyping: contravariantly 'c becomes 'static
    let foo3: for<'a> fn() -> fn(Option<&'a &'static ()>, &'c T) -> &'a T = foo2;
    // subtyping: instantiate `'a <- 'd`
    let foo4: fn() -> fn(Option<&'d &'static ()>, &'c T) -> &'d T = foo3;
    // boom!
    foo4()(None, x)
}

So, i implement my fake function pointer type, my own fake FnOnce trait(for simplicity, i removed Output), and separated the early and late generics parameters and bounds manually. And I use &dyn FnOnce to demostrate. The code is something like this:

use std::marker::PhantomData;

struct Foo<T>(PhantomData<T>);
trait MyFnOnce<Args> {}
impl<'a, 'c, T: 'c> MyFnOnce<(&'a &'c (), &'c T)> for Foo<T> {}

// this compiles
fn good<'c, T: 'c>() {
    let foo2: &dyn for<'a> MyFnOnce<(&'a &'c (), &'c T)> = &Foo::<T>(PhantomData);
}

// this doesn't compile
// fn bad<'c, T: 'c>() {
//    let foo3: &dyn for<'a> MyFnOnce<(&'a &'static (), &'c T)> = &Foo::<T>(PhantomData);
// }

The borrow checker can properly detect the invalid cast within bad function. So i think this should be just an implementation issue. I'm not very familar with the code, but i doubt the problem lives within this function:

https://github.com/rust-lang/rust/blob/master/compiler/rustc_middle/src/ty/relate.rs#L190-L214

If it can type check similar to my example above, i think the original code will be rejected as expected.

EDIT: In case there's extra magic with the builtin FnOnce trait, here's a version with std FnOnce.

#![feature(fn_traits)]
#![feature(unboxed_closures)]
// nightly only
use std::marker::PhantomData;

struct Foo<T>(PhantomData<T>);

impl<'a, 'c, T: 'c> FnOnce<(&'a &'c (), &'c T)> for Foo<T> {
    type Output = ();
    extern "rust-call" 
    fn call_once(self, _: (&'a &'c (), &'c T)) -> Self::Output { todo!() }
}

// this compiles
fn bad1<'c, T: 'c>() {
    let x: &dyn for<'a> FnOnce(&'a &'c (), &'c T) = &Foo::<T>(PhantomData);
}

// this doesn't
// fn bad2<'c, T: 'c>() {
//    let x: &dyn for<'a> FnOnce(&'a &'static (), &'c T) = &Foo::<T>(PhantomData);
// }

And for reference, this is the return type example occurred in lcnr's blog.

// borrow checker properly handles this too.
// does not compile
// fn my<T: 'static>() {
//    let foo1: &dyn for<'a, 'b> FnOnce(&'a (), &'b T) -> (&'a &'b (), &'a T) = todo!();
//    let foo2: &dyn for<'b> FnOnce(&'static (), &'b T) -> (&'static &'b (), &'static T) = foo1;
//    let foo3: &dyn for<'b> FnOnce(&'static (), &'b T) -> (&'b &'b (), &'static T) = foo2;
//}

@SkiFire13
Copy link
Contributor

@SkiFire13 SkiFire13 commented Feb 6, 2022

So, i implement my fake function pointer type, my own fake FnOnce trait

EDIT: In case there's extra magic with the builtin FnOnce trait, here's a version with std FnOnce.

I believe this happens because traits objects are always invariant over their generic arguments. If MyFnOnce and FnOnce were contravariant over their generic arguments then both these examples would compile, but they don't:

fn should_compile1<'a, 'f>(f: &'f dyn FnOnce(&'a ())) -> &'f dyn FnOnce(&'static ()) {
    f
}

fn should_compile2<'c, F: for<'a, 'b> FnOnce(&'a &'b ())>(f: F) {
    let x: &dyn for<'a> FnOnce(&'a &'c ()) = &f;
    let y: &dyn for<'a> FnOnce(&'a &'static ()) = x;
}

@crlf0710
Copy link
Member

@crlf0710 crlf0710 commented Feb 7, 2022

@SkiFire13 You're correct that, dyn values doesn't suffer this issue because it doesn't use contravariance here. However it seems to me that my trait objects examples still shows that this issue can be solved by removing contravariance, in contradictionary to @lcnr's belief?

PS: Personally I'd love to see more aligning between fn () -> () and Box<dyn Fn() -> ()>. I hope the later can act as "stateful callable" in any case, without missing functionality compared to the former...

@lcnr
Copy link
Contributor

@lcnr lcnr commented Feb 7, 2022

@crlf0710

in my post i have an implementation of bad which does not rely on contravariance anywhere, but instead uses covariance in the function return type.

// we again have an implied `'input: 'out` bound, this time because of the return type
fn foo<'out, 'input, T>(_dummy: &'out (), value: &'input T) -> (&'out &'input (), &'out T) {
    (&&(), value)
}

fn bad<'short, T>(x: &'short T) -> &'static T {
    // instantiate `foo`
    let foo1: for<'out, 'input> fn(&'out (), &'input T) -> (&'out &'input (), &'out T) = foo;

    // instantiate 'out as 'static
    let foo2: for<'input> fn(&'static (), &'input T) -> (&'static &'input (), &'static T) = foo1;

    // function return types are covariant,
    // go from 'static to 'input
    let foo3: for<'input> fn(&'static (), &'input T) -> (&'input &'input (), &'static T) = foo2;

    // instantiate 'input as 'short
    let foo4: fn(&'static (), &'short T) -> (&'short &'short (), &'static T) = foo3;

    // boom!
    foo4(&(), x).1
}

While completely removing variance inside of binders would fix this bug, that seems neither desirable nor feasible, considering the resulting breakage and loss of expressiveness.

@GuillaumeDIDIER
Copy link

@GuillaumeDIDIER GuillaumeDIDIER commented Feb 7, 2022

@SkiFire13
Copy link
Contributor

@SkiFire13 SkiFire13 commented Feb 7, 2022

@GuillaumeDIDIER that RFC already mentions this issue (in this section), saying it will be addressed in a separate RFC.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 8, 2022
add fut/back compat tests for implied trait bounds

the `guard` test was tested to cause a segfault with `-Zchalk`, very nice

cc `@nikomatsakis` rust-lang#44491 rust-lang#25860
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Feb 8, 2022
add fut/back compat tests for implied trait bounds

the `guard` test was tested to cause a segfault with `-Zchalk`, very nice

cc ``@nikomatsakis`` rust-lang#44491 rust-lang#25860
@LHolten
Copy link

@LHolten LHolten commented Feb 12, 2022

To me it looks like function declarations assume all instances of the same lifetime in the signature to be identical.
But when sub-typing functions, you don't have to substitute all instances of a generic lifetime at the same time (thus breaking the assumption).
Meanwhile for functions generic over types (instead of lifetimes), all instances of the type have to be substituted at the same time.
So why not apply the same restriction to substituting generic function lifetimes?

nikomatsakis added a commit to nikomatsakis/a-mir-formality that referenced this issue Apr 16, 2022
show that the buggy implied bounds path is fixed
@Cassy343
Copy link

@Cassy343 Cassy343 commented Jul 6, 2022

I feel like the issue here isn't fundamental to variance or implied bounds. Let's look at the two more recent examples of this problem.

From @arielb1

fn foo<'a, 'b, T>() -> fn(Option<&'a &'b ()>, &'b T) -> &'a T {
    fn foo_inner<'a, 'b, T>(_witness: Option<&'a &'b ()>, v: &'b T) -> &'a T {
        v
    }
    foo_inner
}

fn bad<'c, 'd, T>(x: &'c T) -> &'d T {
    // instantiate `foo`
    let foo1: for<'a, 'b> fn() -> fn(Option<&'a &'b ()>, &'b T) -> &'a T = foo;
    // subtyping: instantiate `'b <- 'c`
    let foo2: for<'a> fn() -> fn(Option<&'a &'c ()>, &'c T) -> &'a T = foo1;
    // subtyping: contravariantly 'c becomes 'static
    let foo3: for<'a> fn() -> fn(Option<&'a &'static ()>, &'c T) -> &'a T = foo2;
    // subtyping: instantiate `'a <- 'd`
    let foo4: fn() -> fn(Option<&'d &'static ()>, &'c T) -> &'d T = foo3;
    // boom!
    foo4()(None, x)
}

And from @lcnr

// we again have an implied `'input: 'out` bound, this time because of the return type
fn foo<'out, 'input, T>(_dummy: &'out (), value: &'input T) -> (&'out &'input (), &'out T) {
    (&&(), value)
}

fn bad<'short, T>(x: &'short T) -> &'static T {
    // instantiate `foo`
    let foo1: for<'out, 'input> fn(&'out (), &'input T) -> (&'out &'input (), &'out T) = foo;

    // instantiate 'out as 'static
    let foo2: for<'input> fn(&'static (), &'input T) -> (&'static &'input (), &'static T) = foo1;

    // function return types are covariant,
    // go from 'static to 'input
    let foo3: for<'input> fn(&'static (), &'input T) -> (&'input &'input (), &'static T) = foo2;

    // instantiate 'input as 'short
    let foo4: fn(&'static (), &'short T) -> (&'short &'short (), &'static T) = foo3;

    // boom!
    foo4(&(), x).1
}

In both examples, the invalid cast is in the transition from foo2 to foo3. The thing that both these examples have in common is that a guarantee given by the implied bound is erased by the cast. In the first example, foo2 has the bound 'c: 'a, but the cast erases this bound. This is clearly invalid: the function behind that pointer was allowed to rely on that bound but the caller may no longer respect it. Similarly in the second example, in foo2 we have 'input: 'static, but that bound is then erased. It seems like what's needed here is some kind of check for bound erasure when applying sub-typing.

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Jul 6, 2022

The thing that both these examples have in common is that a guarantee given by the implied bound is erased by the cast.

Indeed. So implied bounds are clearly fundamental to the problem.
And what's happening here isn't a cast, it's subtyping, hence the relation to variance.

@Cassy343
Copy link

@Cassy343 Cassy343 commented Jul 6, 2022

The point I was trying to get across was that it wasn't an insurmountable problem with implied bounds and variance, rather I think it is fixable by incorporating implied bounds into variance in a way. I'm spitballing here because I have no formal experience with this stuff, but is it not possible to add a check along the lines of "T is a subtype of U only if the implied bounds associated with T imply those associated with U." So like with for<'input> fn(&'static (), &'input T) -> (&'static &'input (), &'static T) we have "for all 'input, 'input: 'static," but for for<'input> fn(&'static (), &'input T) -> (&'input &'input (), &'static T), we just have "for all 'input, 'input: 'input," and the latter does not imply the former. Is such a condition not sufficient/not well-defined/not computable?

@RalfJung
Copy link
Member

@RalfJung RalfJung commented Jul 6, 2022

The point I was trying to get across was that it wasn't an insurmountable problem with implied bounds and variance, rather I think it is fixable by incorporating implied bounds into variance in a way.

Oh, sure. The question is just how to actually implement that. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-typesystem C-bug I-unsound P-medium T-lang
Projects
None yet
Development

No branches or pull requests