-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
(It turned out to be Un-)sound Generic Drop #769
(It turned out to be Un-)sound Generic Drop #769
Conversation
I guess that However, doing this directly would be problematic, as it would prevent types within an arena from referencing other such types. I think that we could give the |
@arielb1 it does not suffice to just use But the semantics here require one be able to express (anyway, as you point out, such a requirement would be a real drag, since it would indeed prevent a number of useful patterns from being used there.) I'm not entirely sure I follow how adding a lifetime parameter to |
If we had negative bounds, could we represent ‘ |
It seems to me that
So the definition would become |
I haven't read through all the alternatives and appendices, but a quick skim didn't reveal to me why we can't just parse:
as
implying truly distinct lifetimes, and thus resolving the example problems? Are there any problem cases that aren't just a consequence of considering everything in the same scope to have the same lifetime? Although I'm pretty fuzzy on lifetime unification, so maybe this would Break Everything -- or I'm misunderstanding what the problem actually is. |
Ah, I see the appendix suggests exactly this. Than all the other rules introduced are just to support cyclic structures, then? |
The point is that the lifetime parameter of the |
@arielb1 hmm, you know, I thought I actually tried an experiment like that (that is, putting a lifetime parameter on Okay, now I understand what you are suggesting with the lifetime parameter on Anyway, in my opinion, these are potential future extensions to the language that, while potentially very convenient, need not block landing this RFC. (i.e. I think they can be added backwards compatibly post 1.0 ... the only exception would be if we anticipate needing to change existing stable API's in non-backwards compatible ways, but I am not aware of an example of that.) |
@gankro to what "other rules" are you referring? Do you mean the other conditions in the Drop-Check rule? (In which case I would say, yes, for the most part, things like Condition B. were largely added to support cyclic structure that we found used in practice, in particular in |
@gankro also, regarding |
Added an "unresolved question" alluding to the potential importance of better handling of covariance (and thus variance in general).
Ok, that sounds reasonable to me. The rules necessary to handle cycles are a bit hairy, but if we have usecases today I suppose we need a solution. |
@P1start T strictly outlives It would be better to use a new symbol like |
Follow-up: My previous experiment was flawed because I did not add an appropriate marker for the variance when I made the version of It now seems to me like one can indeed make a safe version of (Also, it might make sense to continue providing the current API under the name I will update the RFC accordingly. @arielb1 : thanks for pushing back on this point. |
Wow, this is really fantastic. I realize the result is complex, but it seems to address pretty much all of my concerns with the previous variants of the outlives rules. Great work! |
* (A.) the `Drop impl` for `D` instantiates `D` at `'a` | ||
directly, i.e. `D<'a>`, or, | ||
|
||
* (B.) the `Drop impl` for `D` has some type parameter with a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is relying on parametric polymorphism, right? Is that OK in the presence of Any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, many types have an implicit X: Drop
bound, e.g. Vec
impl<T> Drop for Vec<T> {
fn drop(&mut self) {
// This is (and should always remain) a no-op if the fields are
// zeroed (when moving out, because of #[unsafe_no_drop_flag]).
if self.cap != 0 {
unsafe {
for x in self.iter() {
ptr::read(x);
}
dealloc(*self.ptr, self.cap)
}
}
}
}
The ptr::read(x);
line is basically a x.drop()
call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I didn't spell this out in the RFC, but in general all type parameters must be assumed to have a destructor attached, which is not quite the same as having an explicit Drop
bound, but the point is, the analysis does not treat the implicit attached destructor as cause for Condition B to fire (since that would basically mean that condition B would always fire, and defeat its purpose).
In fact, your example of the implicit x.drop()
calls within the <Vec as Drop>::drop
implementation is the reason that we need the PhantomData<T>
: That is the magic sauce that tells the analysis, during its recursive traversal of the Vec
struct, that it needs to analyze the drop behavior of T
when it is analyzing Vec<T>
.
This approach is indeed relying on a kind of parametricity, but only in that it is using the lack of bounds as proof that the drop
implementation will never access borrowed data via a method call (except for potentially via implicit calls to drop
on values owned by the type -- but then the analysis catches those during its recursive application to the substructure of the type).
I admit that I did not explicitly consider Any
when I was thinking about this. The docs say that:
Every type with no non-
'static
references implements Any, so Any can be used as a trait object to emulate the effects dynamic typing.
So if a type has references to non-'static
data, it does not get the implicit implementation of Any
. Can one implement Any
explicitly for other types that do have references to non-'static
data? If so, then this scheme might have a problem. (But it also seems like maybe we should not be allowing for such types to implement Any
?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay so I poked around a little bit more; from the source code, it seem like any type implementing Any
is forced to outlive 'static
. So one should not be able to hide borrowed data behind the Any
trait, and therefore it is okay for the analysis to treat Any
like a black box whose destructor is safe to run (at least with respect to not accessing borrowed data).
Namely, now officially propose that `Arena` be changed to `Arena<'a>`. (As an aside, suggest that we *could* also provide the old API with an `UnsafeArena` type; but that is not part of the proposal proper).
Props to huonw for pointing this out, since it forced me to think through the cases where the near-parametricity of `T` matters. Notably, it does not suffice to focus solely on the case where `v` owns data of type `T`; one must also consider the scenario where the destructor for `v` constructs a `T` on the fly.
This RFC is accepted. The tracking issue is rust-lang/rust#8861. |
This is a resurrection and heavy revision/expansion of a PR that pcwalton did to resolve #8861. The most relevant, user-visible semantic change is this: #[unsafe_destructor] is gone. Instead, if a type expression for some value has a destructor, then any lifetimes referenced within that type expression must strictly outlive the scope of the value. See discussion on rust-lang/rfcs#769
A passing thought The Drop-Check rule currently implies that traits with no methods are safe to use as bounds. (the current implementation takes a short-cut and is not so general: it only allows the known builtin bounds as bounds, not arbitrary method-less traits.) So the question: Is there some hack one could use with an associated type to get around that restriction, i.e. something like:
The above seems like a case that the drop-check rule would need to cover; the rule as currently written would not force lifetimes in the type substituted for (This is probably fixable by generalizing the text from saying "some type parameter with a trait bound |
(still, probably best in the short term to amend the RFC with something to fix this believed hole, even if its a really conservative solution like "where |
(I'd personally be satisfied with the really conservative sol'n for now) |
A small thing I noticed, I'm not sure if this has any practical significance: The |
Hmm, yes. I am now trying to construct an example where this yields unsoundness ... it is a little tricky, but ... oh I have some new ideas, will report back later ... (In any case, it is probably safest to just treat this as another case where "the type of v owns data of type T", although at that point I might reword the term it as "the type of v can destroy data of type T" ...)
Update: A tricky part of trying to expose brokenness here is that any destructor that actually writes to a contained
That is the basic difficulty I am facing in trying to use the observation to actually construct an example where one could use this to break soundness. Here is some example code illustrating the kind of issue I am thinking about: https://is.gd/fDfLPX |
So my current thought is that I need to amend the "Noncopy types" subsection with an explanation along the lines of the one presented in my previous comment.
It might also be good to note that calls to destructors for values extracted via unsafe pointers (e.g. |
Yes, I admit I don't understand the rules well enough to tell whether this could actually violate safety. Just thought it was worth noticing. |
Hey, I do know the rules, and you still managed to give me quite a scare, so I'd say it is definitely worth noticing/noting. :) |
Remove
#[unsafe_destructor]
from the Rust language. Make it safe for developers to implementDrop
on type- and lifetime-parameterized structs and enum (i.e. "Generic Drop") by imposing new rules on code where such types occur, to ensure that the drop implementation cannot possibly read or write data via a reference of type&'a Data
where'a
could have possibly expired before the drop code runs.(rendered
text/
)Spawned from rust-lang/rust#8861 (which is also the tracking issue)