-
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
Leak and Destructor Guarantees #1085
Conversation
Very much in favor of this RFC. |
- `sync::mpsc::Sender` (possibly not, see unresolved questions) | ||
- Possibly other APIs. Please point any others out if you think of them. | ||
|
||
_Cause all panics in destructors to immediately abort the process._ |
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.
In the past I have personally been worried about the implementation details of a strategy such as this, so it would be nice to expand on how you expect this to be implemented.
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.
I would like to, but am not familiar enough with the internals of panicking to really say anything intelligent here. I included the details of this under unresolved questions, and would be happy to hash it out with someone more familiar and include their recommendation in the RFC.
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.
@alexcrichton Can you specifically elaborate on what concerns you? It seems to me that even a naive solution would only affect destructors that can panic, which I'm not convinced is anything like a majority of them.
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.
@pythonesque transitively a lot of code calls panic!
(e.g. internal asserts), even thought those code paths will literally never be taken for the code run by most destructors.
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.
@huonw The two most comment collections (Vec
and HashMap
) don't outside of debug mode, AFAICT. I agree that Rust in general generates lots of landing pads, but in destructors specifically I suspect it is less common.
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.
Having read some of the discussion, it sounds like the impact of replicating noexcept
semantics might not be too bad. I'd vote for attempting an implementation based on that and seeing what kind of performance impact it has in practice.
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.
A non-native and 0-overhead solution might look like something along the lines of running the stack unwinder and if a frame of a destructor is encountered then an abort happens, otherwise the panic happens normally. I don't know how this would be implemented, nor if it could be implemented reliably in the face of inlining.
Wouldn't this be as simple as inserting abort()
into the landing pads while generating Drop::drop()
?
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.
I hope this refers only to the libstd’s panic semantics and does not force it on the language as a whole. If it does, then a strong 👎 from me on this.
As for implementation, it is trivial without keeping any state in TLS or using exception tables:
- Introduce a new lang-item
drop_panic
(or adjustpanic
to receive boolean flag indicating it has been called from a destructor); - If
panic!
is called inside a destructor, calldrop_panic
language item instead ofpanic
; - In
libstd
implementdrop_panic
language item to abort the process.
Now, the nice thing about this scheme is:
- No additional runtime cost over current
panic!()
; - If you’re not using
libstd
, you get to choose behaviour of panic-in-destructor yourself.
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.
@nagisa This would not work, because:
(1) To be able to rely on this for memory safety it must be a language-wide guarantee.
(2) Destructors may call functions that are not destructors.
Panics within destructors can already cause your program to abort, and in fact will do so if they occur after another panic (Rust's exception implementation does not allow for safe handling of double panics). You cannot rely on them not to do this in current Rust. As a result, panics in destructors are already a bad idea and I strongly advise you not to rely on them being implemented in a particular way.
(This is not to say that Rust couldn't have something like C++'s terminate
, which would let you do other stuff before you aborted, but it wouldn't be able to unwind the stack).
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.
Panics within destructors can already cause your program to abort, and in fact will do so if they occur after another panic (Rust's exception implementation does not allow for safe handling of double panics). You cannot rely on them not to do this in current Rust. As a result, panics in destructors are already a bad idea and I strongly advise you not to rely on them being implemented in a particular way.
I am aware of that. However, given the fact that you cannot begin unwinding from a destructor anymore, even when not using libstd/core, seems like a big and unfortunate flexibility loss.
I'm also very much in favour of this. I have a couple of minor concerns:
As an example, I think it should be possible to make a RAII guard which just calls a lambda when it goes out of scope. (this is notably different from thread::scoped in that it's safe for the guard to be Leak). In this situation, if the lambda panics, it should propagate out normally, stopping the task rather than the whole process. |
@Diggsey I can't think of a reliable way for the compiler to know whether a |
Yeah, it could also be solved by at some point later adding the concept of "finalizers", which are called by rust when an instance of that type goes out of scope via normal execution, and thus are allowed to panic, but are not given any of the same guarantees as destructors. That would also be completely backwards compatible. |
I may have been in support of this if it had been proposed 6 or 9 months ago. As it is, I have several major concerns:
|
@sfackler I am personally fine with
Both of these seem independently useful to me. There are many APIs written under the assumption that RAII "works" for more than just memory (e.g. transactions). That Rust can "leak" these types is surprising. They can certainly be rewritten as closures, but it's not as ergonomic; it makes RAII a much less useful (and less reliable) feature. At least for me, it's not about this one API, it's about improving the predictability of Rust as a language. As far as your points about timing: those are mostly my concerns as well. But I'm still in favor of pushing forward with a trial implementation. I do want to highlight this comment:
This wouldn't be the end of the world, but what I fear is people eventually just mechanically adding Edit: Also, since I hadn't yet commented on this: I don't see any reason for such a bound on |
@sfackler brings up a good point that this still wouldn't let us bring the old I'm also concerned by the vagueness of how we'd implement abort-on-panic-in-destructors. Frankly, the old |
@sfackler is spot on. Perfect is the enemy of good, as they say. The current situation is somewhat unsatisfying but most of it can be fixed after 1.0 ( -1 from me. |
I would like to also say that I do think that the abort-on-panic-in-a-destructor behavior sounds desirable in its own right, and I could get behind a last-minute RFC to deliberately unspecify the current behavior such that we can experiment with such an idea for a future version of Rust. |
|
||
Change several `std` APIs to adjust for the guarantees now provided to types | ||
which do not implement `Leak`: | ||
|
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.
Hm, with this definition Leak
seems to be implemented for almost everything, including Vec<T>
for arbitrary T
. Does that mean that I can put JoinGuard
into a vector and then leak it freely?
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.
No, for the same reason Vec<T>
where T is not Send is not Send.
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.
I guess it means that Leak
is not an ordinary trait and has to be special-cased in the compiler like Send
, i.e. it has to be a lang item.
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.
Nope, we can define traits like this directly in Rust now, e.g. see Reflect
.
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.
Oops, it turns out all traits with default+negative impls behave like Send
in this regard, and it is just a part of OIBIT design. Sorry for the noise.
Huuuuge 👎 to me. The time for this has passed. |
So, I considered this sort of design briefly. I think that, even with OIBIT, the pain of adding a "default trait" is often underestimated. For example, if we added I think there is plenty of room to enable "guaranteed destruction" in the future. Closures are available today. If we want RAII-like APIs, I think there are good options there too, such as having some notation for a fn to return a pre-borrowed value (so the caller gets back a Now, we've long said that the semantics of panic during a destructor are explicitly unsettled. At this point, I favor making destructor bodies "catch" panics and simply abort, which I think has come up on this thread, though it took me some time to come around to this. But I think we can do this after 1.0. |
Yeah, I lean toward viewing the costs as outweighing the benefits here. It sounds like there are ways around the I am sympathetic to concerns that we shouldn't let 1.0 prevent us from making emergency changes if we need to (though the barrier is high, of course). But even notwithstanding 1.0, adding an extra built-in trait to the language has to be very beneficial in order to be worth the cognitive and complexity burden. Send/Sync/Copy are used all the time (at least in concurrent code), and they provide a lot of benefit. The question "Why do I need to worry about Send in Rust?" has a clear, convincing answer for anyone who has done threaded programming: it prevents data races. On the other hand, the question "Why do I need to worry about Leak in Rust?", would have the answer "well, there's this edge case in the |
I'm really in favor of implementing this RFC for 1.0. Why? I don't think of this RFC as introducing new feature, but rather as fixing a bug in Please also notice that while introducing So that's why we should implement this RFC, even if we aren't sure if it's a good idea, just not to close a possibility. |
I think that we should be clear that this design does not eliminate leaks in RC. It just allows the compiler to reason about "possibly leaking" data types. That's much less of a benefit. |
@krdln Most uses of RAII in other crates should be fine even without this RFC. The problem with "scoped" was not that it used RAII, but that it allowed the new thread to continue running after the lifetime of closed-over locals had expired. Any RAII guards which can be implemented safely are guaranteed to be OK. @pcwalton It eliminates leaks of types for which leaking would be unsafe, thereby allowing such types to be used in safe code, and at the same time does not restrict the use of Rc to create cycles. Seems like a fairly large benefit to me? @nikomatsakis How reasonable is the idea of making all traits extend "Leak" by default? To remove the dependency you'd write "Trait : ?Leak". If the Leak trait was added now as a "do nothing" trait which was impossible to opt-out of (for the moment) would it allow the correct behaviour to be implemented later in a backwards compatible way (since you're guaranteed that all pre-existing types and trait objects are Leak) |
This RFC doesn't fix a safety hole in current Rust. It fixes a safety hole in a hypothetical future version of Rust exposing functions that depend on not leaking for safety. But a simpler approach is to just not expose those kinds of functions. Given that closures exist today, it's not even clear that that hypothetical future Rust would have any benefit over current Rust other than (essentially) syntax. |
Closures are not a direct replacement: they prevent various control flow constructs which would otherwise be valid. Code which depends on that necessarily becomes more complicated without a RAII style API. |
While I agree with the goal of allowing reliable RAII guards, I disagree with the solution outlined in this RFC. Quoting from another post:
@pcwalton, closures are often much less convenient than guards. For example, you can't use |
Anyway, whether you want to call it syntax or not, the only benefit that is control flow-affecting constructs work better with It's better to make folks who want to use complex control flow with |
Agreed. However, I actually feel that for scoped threads, this is a benefit -- using
Agreed, I think this kind of thing could be very interesting to explore later on. |
By "using RAII this way" I meant using it in cases that would imply
I disagree here. There are things like database transaction handlers or eg. some kind of secret protection handlers which you can implement without |
By making such a RAII guard !Leak, you'd be disallowing perfectly desirable behaviour. Putting a RAII guard inside a not-cycle-proofed Rc is a completely legitimate thing to do: just because you may not want to do that in your particular use-case doesn't mean rust should forbid it, or that a library author writing such a RAII guard should try to impose that restriction on their users. Rust should only try to prevent things that are objectively wrong (eg. segfaults and other memory unsafety), not subjectively wrong like what you're describing. |
I'm a 👎 on this for all the reasons that have already been mentioned. The RFC would definitely need to be updated w/ an analysis of how libraries would need to be updated to factor in This change would introduce a non-negligible amount of cognitive overhead to users and library authors. Rust already has a steep learning curve. There needs to be a high bar to add more complexity and as pointed out by other commenters, the benefit of this change does not seem to meet this bar right now. |
An implementation of Leak, including std and compiler changes, can be found here: https://github.com/reem/rust/tree/leak Also updated the OP with the above link. |
See #1066 (comment) Code: https://gist.github.com/anonymous/6e4c9fe67283da121c97 (Note that it triggers a timeout on playpen for some reason, although it silently terminates on my machine) |
@theemathas it is sufficient to require The vast majority of breakage can be avoided by very careful placement of The remaining most problematic case is with trait objects, since By only requiring |
@reem Here is a variant of leaking that does not require |
@theemathas you're right, I hadn't considered forming a cycle just between the sender and receiver. I will adjust the sample implementation and measure the breakage. This is a good example of where we have to be careful and think quite hard about where Note that this example only occurred because I tried to be clever and use the unsafe constructors for |
I claim that for 99% of use cases of In other words, make Leak work like Sized, allow !Leak in Vec and maybe a small number of other types, and tell everyone else to suck it (and hope this sets a precedent not to include Edit: As for making destructors panic on unwind, isn't this equivalent to having a variable at the beginning of all destructors that panics in its own destructor? (And defusing it at the end.) For safety purposes, doesn't Rust need to get destructor order right even in the presence of unwinding and inlining? - so that should just work... (Perhaps this should only happen for destructors of objects containing |
I believe this is the right thing to do. Conscious that this is a breaking change but better to fix this now than live with an inferior solution indefinitely - that's what betas are for after all. Implementation has existed now for a few days and seems like fallout could be addressed before 1.0. 👍 |
I just thought of another kind of interesting use case for |
👎 I feel like everyone who's proposing changes to Rust beyond simply altering |
@kballard it's certainly not the only example, see, for instance, hlua's stack handling for an example from the community. This would be made quite a bit more onerous to use if the library instead had to use nested closures. |
The standard library is a tiny, tiny percentage of all rust code that will be written, and likely a small percentage of all unsafe rust code too. We have to look at this issue not from the lens of "how does this affect the std APIs" but "how does this affect Rust, and the sorts of APIs that can be written in it." |
@reem Looking over that linked post, it does not appear that the stack handling RAII values have anything to do with memory safety, they only need to have their destructors run in order to guarantee program correctness. Rust only cares about memory safety, not about whether your program has a bug in it. Furthermore, that Lua wrapper could actually be adjusted to work correctly even in the face of leaked destructors. It just requires reordering the internals slightly, but the API can remain the same. Adjust fn pop_stack_to(&mut self, size: i32) {
if self.size > size {
lua_pop(self.as_mut_lua(), self.size - size);
self.size = size;
}
} Then make it so that the creation of a |
That's a bit extreme. There are many ways in which Rust tries to help ensure correctness even where memory safety is not implicated ( |
That's not true at all, and you know it. That's why we have In this particular case I especially disagree. It would have been memory safe for Rust to just make deallocation unsafe. In fact, IIRC this is what Ada does. Rust didn't want to do that. Just because it's not an absolute rule that all destructors will run on stack unwind doesn't mean Rust can't guarantee that property in select cases. It's also not a guarantee that every API that can be "fixed" to work around this is going to have the same performance properties as the version that can just assume the destructor will be run; in fact, it seems almost inevitable to me that this will happen. Using |
I agree that Rust cares about bugs other than memory safety, but I wouldn't necessarily hold up A better example, IMO, would be match exhaustiveness. Even there, though, it's as much motivated by safety as it is for code size and control (not wanting the programmer to have to worry about the Rust compiler inserting panics everywhere). I think safety features are strongest when they double as performance features. Exhaustiveness checking, the lack of null pointers and zero values, and the mutability restrictions all function not only as safety features but also as optimization enablers. Those kinds of features have a much different cost-benefit ratio, in my mind, than features such as Leak which purely aid safety of relatively rare types of code. |
@pcwalton I'm curious; how does exhaustiveness checking enable better optimization? For the other examples you cited I can see the connection, but not for that one. |
I think he's saying that the panic that would otherwise have to be inserted for missing cases costs code size. (You can also save two instructions for the jump table dispatcher if you're totally sure the value is in range, but I doubt LLVM actually does that...) |
Hmm...
It puts the last case as the default case and omits it from the jump table, but it could have saved a jump (and saved one entire byte, net - and sacrificed safety) by removing the cmp/jbe and keeping it in the jumptable. |
In case anyone watching this is interested, I wrote up an alternative proposal: #1094 |
So, re-reading the thread here, I realize that I was so exhausted by writing this blog post, I failed to post it on all the relevant discussion threads. For the record, I wrote a blog post addressing my current feelings on this RFC and others: http://smallcultfollowing.com/babysteps/blog/2015/04/29/on-reference-counting-and-leaks/ |
Thanks @reem for the RFC, and everyone for the great discussion! Of course, this needs to be settled prior to the 1.0 release next week, and the core team met yesterday to come to a final decision on the matter. This is truly a thorny problem with multiple reasonable paths to take, but in the end the analysis of the tradeoffs presented in Niko's blog post and the follow up represents the core team's consensus, which emerged through the discussion on this thread and others. As such, we are going to close this RFC, to settle the matter for 1.0. There is still discussion to be had about what to do with the |
Add a new default unsafe trait,
Leak
. If a type does not implementLeak
it can bememory unsafe to fail to run the destructor of this type and continue execution of the
program past this types lifetime.
Additionally, cause all panics in destructors to immediately abort the process,
solving two other notable bugs that allow leaking arbitrary data.
Add a safe variant of
mem::forget
(e.g.mem::leak
) which requiresLeak
.The existing
mem::forget
remains unbounded, butunsafe
.This proposal also requires a slight breaking change to a few
std
APIs to addLeak
bounds where none exist currently. This is unfortunate so close to 1.0,but in the author's opinion is better than dedicating to a safe unbounded
mem::forget
forever.
This RFC is largely an alternative to RFC PR 1066, which makes an unbounded
mem::forget
safe.
EDIT: An implementation of Leak, including std and compiler changes, can be found here: https://github.com/reem/rust/tree/leak