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 upLeak and Destructor Guarantees #1085
Conversation
reem
added some commits
Apr 23, 2015
This comment has been minimized.
This comment has been minimized.
|
Very much in favor of this RFC. |
alexcrichton
reviewed
Apr 23, 2015
| - `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._ |
This comment has been minimized.
This comment has been minimized.
alexcrichton
Apr 23, 2015
Member
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.
This comment has been minimized.
This comment has been minimized.
reem
Apr 23, 2015
Author
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.
This comment has been minimized.
This comment has been minimized.
pythonesque
Apr 23, 2015
Contributor
@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.
This comment has been minimized.
This comment has been minimized.
huonw
Apr 23, 2015
Member
@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.
This comment has been minimized.
This comment has been minimized.
pythonesque
Apr 23, 2015
Contributor
@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.
This comment has been minimized.
This comment has been minimized.
pythonesque
Apr 23, 2015
Contributor
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.
This comment has been minimized.
This comment has been minimized.
vadimcn
Apr 23, 2015
Contributor
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()?
This comment has been minimized.
This comment has been minimized.
nagisa
Apr 23, 2015
Contributor
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
As for implementation, it is trivial without keeping any state in TLS or using exception tables:
- Introduce a new lang-item
drop_panic(or adjustpanicto receive boolean flag indicating it has been called from a destructor); - If
panic!is called inside a destructor, calldrop_paniclanguage item instead ofpanic; - In
libstdimplementdrop_paniclanguage 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.
This comment has been minimized.
This comment has been minimized.
pythonesque
Apr 23, 2015
Contributor
@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).
This comment has been minimized.
This comment has been minimized.
nagisa
Apr 23, 2015
Contributor
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.
alexcrichton
reviewed
Apr 23, 2015
| If a way to leak in the entirely safe subset of rust + `mem::swap`, then this | ||
| proposal is basically dead on arrival. We should investigate if this is | ||
| possible, but the author's slightly educated guess is that it is not possible. | ||
|
|
This comment has been minimized.
This comment has been minimized.
alexcrichton
Apr 23, 2015
Member
It may be worth also mentioning that this would likely be a large breaking change due to the addition of bounds on the type parameters for the primitives listed above. (and the short time frame that would have to be reconciled in).
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@Diggsey I can't think of a reliable way for the compiler to know whether a |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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:
|
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
petrochenkov
reviewed
Apr 23, 2015
|
|
||
| Change several `std` APIs to adjust for the guarantees now provided to types | ||
| which do not implement `Leak`: | ||
|
|
This comment has been minimized.
This comment has been minimized.
petrochenkov
Apr 23, 2015
Contributor
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?
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
petrochenkov
Apr 23, 2015
Contributor
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.
This comment has been minimized.
This comment has been minimized.
pythonesque
Apr 23, 2015
Contributor
Nope, we can define traits like this directly in Rust now, e.g. see Reflect.
This comment has been minimized.
This comment has been minimized.
petrochenkov
Apr 23, 2015
Contributor
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.
nagisa
reviewed
Apr 23, 2015
| Unlike with `Sized`, the vast majority of code actually does want what would be | ||
| `T: ?Leak`, meaning that over time we would either see lots of unnecessarily | ||
| restrictive bounds or the proliferation of `T: ?Leak` all over pretty much all | ||
| generic code. |
This comment has been minimized.
This comment has been minimized.
nagisa
Apr 23, 2015
Contributor
If only we had negative generic constraints…
impl<'a, T> Unleakable for ::std::thread::JoinGuard<'a, T> { }
pub fn leak<T: !Unleakable>(x: T) {
unsafe { forget(x) }
}
// etc
This comment has been minimized.
This comment has been minimized.
arielb1
Apr 24, 2015
Contributor
This won't work - traits are coinductive. Every recursive type would be Unleakable.
This comment has been minimized.
This comment has been minimized.
Huuuuge |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
@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 comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
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 |
This comment has been minimized.
This comment has been minimized.
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. |
nikomatsakis
self-assigned this
Apr 23, 2015
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
|
I'm a 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. |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
theemathas
commented
Apr 25, 2015
|
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) |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
theemathas
commented
Apr 25, 2015
|
@reem Here is a variant of leaking that does not require |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
comex
commented
Apr 25, 2015
|
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 |
This comment has been minimized.
This comment has been minimized.
imccrum
commented
Apr 27, 2015
|
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. |
This comment has been minimized.
This comment has been minimized.
|
I just thought of another kind of interesting use case for |
This comment has been minimized.
This comment has been minimized.
|
I feel like everyone who's proposing changes to Rust beyond simply altering |
This comment has been minimized.
This comment has been minimized.
|
@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. |
This comment has been minimized.
This comment has been minimized.
|
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." |
This comment has been minimized.
This comment has been minimized.
|
@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 |
This comment has been minimized.
This comment has been minimized.
That's a bit extreme. There are many ways in which Rust tries to help ensure correctness even where memory safety is not implicated ( |
This comment has been minimized.
This comment has been minimized.
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 |
This comment has been minimized.
This comment has been minimized.
|
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. |
This comment has been minimized.
This comment has been minimized.
Valloric
commented
Apr 28, 2015
|
@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. |
This comment has been minimized.
This comment has been minimized.
comex
commented
Apr 28, 2015
|
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...) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
comex
commented
Apr 28, 2015
|
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. |
This comment has been minimized.
This comment has been minimized.
|
In case anyone watching this is interested, I wrote up an alternative proposal: #1094 |
This comment has been minimized.
This comment has been minimized.
|
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/ |
This comment has been minimized.
This comment has been minimized.
|
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 |
reem commentedApr 23, 2015
Add a new default unsafe trait,
Leak. If a type does not implementLeakit 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::forgetremains unbounded, butunsafe.This proposal also requires a slight breaking change to a few
stdAPIs to addLeakbounds 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::forgetforever.
This RFC is largely an alternative to RFC PR 1066, which makes an unbounded
mem::forgetsafe.
EDIT: An implementation of Leak, including std and compiler changes, can be found here: https://github.com/reem/rust/tree/leak