Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upImplied bounds on nested references + variance = soundness hole #25860
Comments
aturon
added
A-typesystem
I-nominated
labels
May 28, 2015
This comment has been minimized.
This comment has been minimized.
|
What's going on here is that This assumption was thought to be valid because any nested reference type |
This comment has been minimized.
This comment has been minimized.
|
One solution is to be more aggressive about checking WFedness, but there are other options to consider. |
This comment has been minimized.
This comment has been minimized.
|
triage: P-high T-lang |
rust-highfive
added
P-high
and removed
I-nominated
labels
Jun 2, 2015
nikomatsakis
self-assigned this
Jun 2, 2015
nikomatsakis
added
the
T-lang
label
Jun 2, 2015
This comment has been minimized.
This comment has been minimized.
|
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
nikomatsakis
added a commit
to nikomatsakis/rust
that referenced
this issue
Jun 8, 2015
nikomatsakis
added a commit
to nikomatsakis/rust
that referenced
this issue
Jun 9, 2015
This comment has been minimized.
This comment has been minimized.
|
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
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis I believe you meant to close this. |
This comment has been minimized.
This comment has been minimized.
|
The work needed to close this has not yet landed. It's in the queue though, once we finish up rust-lang/rfcs#1214. |
This comment has been minimized.
This comment has been minimized.
|
Sorry, I saw the commit but didn't notice that it hadn't been merged. |
arielb1
added
the
I-unsound 💥
label
Oct 1, 2015
nikomatsakis
referenced this issue
Oct 8, 2015
Closed
Nightly regression: lifetime error with autoderef #28854
This comment has been minimized.
This comment has been minimized.
|
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<'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... |
This comment has been minimized.
This comment has been minimized.
|
Here's a variation on the original example that retains explicit types (rather than resorting to 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);
} |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
|
as an aside, part of me does think that it would be better if we also added some way to write the proposed implied 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 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 |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
Yes, that sounds like we're talking about the same idea. |
This comment has been minimized.
This comment has been minimized.
|
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 viewThe natural generalization of our current
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 For example, if you had a function:
Under this RFC, the type of this function is:
Under the for/where scheme, the full type would be:
Now, if we upcast this type to only accept static data as argument, the where clauses are unaffected:
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:
Clearly, this subtype relationship should not hold, because the where clauses in the subtype are not implied by the supertype. |
This comment has been minimized.
This comment has been minimized.
|
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);
} |
nikomatsakis
referenced this issue
Feb 24, 2016
Open
"Outlived by" bound for higher-ranked lifetimes #1409
nikomatsakis
referenced this issue
Mar 18, 2016
Closed
Unsound projection when late-bound-region appears only in return type #32330
This comment has been minimized.
This comment has been minimized.
|
@rust-lang/lang no activity in a while. Still P-high? Anybody working on it or can we lower it? |
brson
added
the
I-nominated
label
Jun 23, 2016
This comment has been minimized.
This comment has been minimized.
|
(this issue really wants input from @nikomatsakis ) |
nikomatsakis
referenced this issue
Jun 29, 2016
Merged
Generic associated types (associated type constructors) #1598
This comment has been minimized.
This comment has been minimized.
|
I think we need to make progress on this issue, but I've been saying that for a year and we haven't. I would put this on the list of "important soundness issues" to address but I'm not sure how to prioritize that work in general relative to other tasks. As far as the range of possible fixes I think nothing has changed from this previous comment:
From a purely theoretical point-of-view, I would prefer the latter but I am wary of trying to implement it. It seems obvious that it will be hard to do in a complete way if we are talking about arbitrary sets of bounds. On the other hand, it may be that if we are just talking about implied bounds, we can implement the checks in a pretty simple way, since the bounds in question will have particular forms. I've been meaning to take some time and think about this more -- I think that some of the work being done for lazy normalization may help here -- but I've not really done so. As far as the user-visible language changes that would result, removing contravariance for fn args is a nice simplification, but it is almost certainly more backwards incompatible than something based on making |
aturon
added
P-medium
and removed
I-nominated
P-high
labels
Jul 7, 2016
This comment has been minimized.
This comment has been minimized.
|
The lang team met and determined that we should move to P-medium; no one is going to close this in the near future, but we want to keep it on the radar. |
This comment has been minimized.
This comment has been minimized.
|
After some further thought -- and in particular working more on chalk -- I have started to have some hope that we would be able to close this using the more principled approach described here. Basically the idea would be that |
aturon commentedMay 28, 2015
The combination of variance and implied bounds for nested references opens a hole in the current type system:
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:
(and this way, still get the bad behaving
fn bad, by just side-stepping one of the explicit type declarations.)