RFC: Alter mem::forget to be safe #1066
Conversation
Alter the signature of the `std::mem::forget` function to remove `unsafe` Explicitly state that it is not considered unsafe behavior to not run destructors.
This effectively breaks any RAII pattern that depends on a destructor. What's the safe replacement that is guaranteed to run? |
|
||
Primarily, the `unsafe` annotation on the `mem::forget` function will be | ||
removed, allowing it to be called from safe Rust. This transition will be made | ||
possible by stating that destructors **may not run** in all circumstances (from |
nagisa
Apr 16, 2015
Contributor
nit: “may not” sounds wrong here. “might not” would be better, I think.
nit: “may not” sounds wrong here. “might not” would be better, I think.
# Alternatives | ||
|
||
The main alternative this proposal is to provide the guarantee that a destructor | ||
for a type is always run and that it is memory unsafe to not do so. This would |
nagisa
Apr 16, 2015
Contributor
This is a very, very hard thing to do, because there’s process::exit
, intrinsics::abort
, stack overflow etc which circumvent unwinding the stack and terminate the process immediately.
This is a very, very hard thing to do, because there’s process::exit
, intrinsics::abort
, stack overflow etc which circumvent unwinding the stack and terminate the process immediately.
glaebhoerl
Apr 16, 2015
Contributor
I think a useful distinction can be made between "the destructor will always be run" and "the destructor will always be run if the program itself continues to run". I.e., what we'd be interested in preventing is Rust code being able to observe the fact of a destructor having not run when it "should have".1 Put another way, actual Rust code shouldn't need to guard against the possibility of a destructor having not run. Of course if the process aborts (the world ends), then that possibility is avoided (and in fact, ending the world can be a reasonable strategy for avoiding it). As language designers it's also unfortunately not within our power to prevent the power going out or a sudden gamma ray burst destroying all life and computation on Earth, but that's also not what we're concerned with.
1 If an invariant is violated in the forest but nobody can observe it, might demons fly out of your nose? (No - they mightn't.)
A tricky question, though, is to what extent panics can be considered to end the world to an adequate degree. From the perspective of the code that thread was executing, the world has indeed ended - but other threads and (notably!) destructors can still potentially observe what the end was like.
I think a useful distinction can be made between "the destructor will always be run" and "the destructor will always be run if the program itself continues to run". I.e., what we'd be interested in preventing is Rust code being able to observe the fact of a destructor having not run when it "should have".1 Put another way, actual Rust code shouldn't need to guard against the possibility of a destructor having not run. Of course if the process aborts (the world ends), then that possibility is avoided (and in fact, ending the world can be a reasonable strategy for avoiding it). As language designers it's also unfortunately not within our power to prevent the power going out or a sudden gamma ray burst destroying all life and computation on Earth, but that's also not what we're concerned with.
1 If an invariant is violated in the forest but nobody can observe it, might demons fly out of your nose? (No - they mightn't.)
A tricky question, though, is to what extent panics can be considered to end the world to an adequate degree. From the perspective of the code that thread was executing, the world has indeed ended - but other threads and (notably!) destructors can still potentially observe what the end was like.
nagisa
Apr 16, 2015
Contributor
what we'd be interested in preventing is Rust code being able to observe the fact of a destructor having not run when it "should have".
This is pretty easy, actually, unless you declare destructors to be “pure”. A nice example would be not running destructor for PID-like files that are only supposed to exist for the lifetime of process. Killing and running the same Rust code again could somewhat reliably tell you that the destructor was not run and PID-like file was not deleted.
What I’m trying to say here: this alternative would require marking all of these prematurely-kills-process functions unsafe as well. I believe this makes the alternative unviable.
what we'd be interested in preventing is Rust code being able to observe the fact of a destructor having not run when it "should have".
This is pretty easy, actually, unless you declare destructors to be “pure”. A nice example would be not running destructor for PID-like files that are only supposed to exist for the lifetime of process. Killing and running the same Rust code again could somewhat reliably tell you that the destructor was not run and PID-like file was not deleted.
What I’m trying to say here: this alternative would require marking all of these prematurely-kills-process functions unsafe as well. I believe this makes the alternative unviable.
glaebhoerl
Apr 16, 2015
Contributor
Point noted. I'm not quite sure of the correct way to formulate the guarantee we'd want (if we'd want it, which this RFC says we wouldn't), but I still very much feel that ruling out abort()
(or loop { }
) shouldn't have to be a part of it. The motivation for the guarantee would be to be able to depend on it for memory safety. Things external to the process such as PID files do not impact on memory safety. This is similar to how Rust prevents you from having two &mut
references to the same object but not from doing rm -rf /
.
Point noted. I'm not quite sure of the correct way to formulate the guarantee we'd want (if we'd want it, which this RFC says we wouldn't), but I still very much feel that ruling out abort()
(or loop { }
) shouldn't have to be a part of it. The motivation for the guarantee would be to be able to depend on it for memory safety. Things external to the process such as PID files do not impact on memory safety. This is similar to how Rust prevents you from having two &mut
references to the same object but not from doing rm -rf /
.
@joshtriplett I believe our destructors are still guaranteed to run, unless you:
This is actually how I’d want to see the “destructors might not run” part defined, so it puts people at ease, @alexcrichton. We’re not Java after all, which runs finalizers at its own discretion. Otherwise, I’m undecided about this RFC. While, indeed, |
One thing the RFC doesn't mention is that it's not hard to work around this change in APIs like That said, we will at some point need to spell out some minimal circumstances in which you can rely on a dtor being run, if we want to fully justify a |
Your destructor running is just control-flow integrity. |
As @nagisa mentioned, this RFC doesn't mean that destructors won't be run, it just means that they are not guaranteed to run. All "normal circumstances" will still have destructors run. The current "replacement" for a destructor that's guaranteed to run is to construct a situation which you know avoids the pitfalls where the compiler does not run destructors. For example this means avoiding panicking from a destructor, avoiding
I do agree that the statement "destructors may not be run" may be a little weak, but I'm also somewhat wary of trying to exhaustively list either all location where leaks are possible or all locations where leaks are not possible. Perhaps we could try to list a subset of scenarios where leaks are bugs? For example this code should always run the destructor for fn foo<T>(x: T, f: fn(&X)) {
foo(&x)
}
This is actually true today as well I believe. Due to the fact that a panicking destructor or an |
It's not quite that simple today: rust-lang/rust#24292 (comment) |
Destructors not running if a panic occurs in a destructor is a very annoying bug. On the other hand, destructors of locals always run on a panic (because a double-panic = abort), and you can explicitly drop the local in other cases to handle it assuming control-flow integrity. If you don't put your struct inside of a |
@arielb1 I think we're in total agreement. All I'm saying is that, as with many other things, we will at some point need to write clear guidelines about what you can rely on and need to guarantee when writing unsafe code. The policy can't simply be "you cannot rely on destructors running". |
Once we release 1.0 with the ability to write a safe We can still add support for RAII guards post-1.0 by adding the Yes, there'll probably be many places that won't be updated to use |
Well, that depends on what you mean by "depends". :) If the destructor is cleaning up resources, then it continues to work fine. If the destructor is modifying state to move something from inaccessible to accessible (as with mutexes and RefCell, for example), that's fine too. It's just when the value will be accessible anyway but in an inconsistent state that you have a problem and want to use a closure (or perhaps some other mechanism, if we add one in the future). In general, the intention of this RFC is not to say that you can't rely on destructors to run (though there are some important caveats to consider...) but rather that when you relinquish ownership of a value outside of your control, it may get leaked and not run, so you have to consider that. |
Hi! I'm new here, so sorry if this is a rather uniformed point of view. One of the things I really liked about Rust is how it married C++-style RAII, a functional type system, and memory safety. This proposal essentially kills RAII! I think the alternative, guarantee that destructors run, is a much better option. This RFC renders guards of all sorts prone to leaking. I understand that "unsafe" means only "memory unsafe", but when writing real code, you care about other sorts of correctness, too. Guards are a very useful design pattern. I would argue they are more useful than RC! The RFC mentions some outstanding bugs that can lead to unrun destructors. As a user, I expect bugs, even in a release version. It's only 1.0! A vain attempt to rid Rust of all bugs in time for release seems a very poor justification for fundamentally changing (for the worse) the semantics of Drop. Bugs can always be fixed later. You will never get RAII back once you declare that Drop cannot be depended upon. I think that a solution to this problem should be aimed at fixing RC. RC is in many ways antithetical to the philosophy that Rust espouses and drew me to your project. [A]RC is essentially giving up on finding an owner. Most times I've seen reference counting used in C++, it was due to developer laziness. Please don't ditch a very useful design pattern, familiar to every C++ programmer, in the name of RC! |
@terpstra C++ has the same ceveats as we do. If you somehow leak an object (via a loop in refcounted pointer, for example) in C++ it won’t run the destructor either. |
To be clear, this RFC is not proposing any changes to any of the existing
Note that this will require an extensive audit as well to ensure that there are no other forms of leakage. It is also quite difficult (and may have a performance impact) to fix the existing bugs mentioned in this RFC. |
No. C++ will never move an object from the stack to the heap, in the Rust sense. The object on the stack is guaranteed to run its destructor, barring the sorts of things like your program dies. So, guards remain safe to use. A memory leak in the heap is something else entirely. C++ programmers are well aware that heap objects might live forever. |
"To be clear, this RFC is not proposing any changes to any of the existing #[stable] RAII patterns in the standard library. For example the lock() method on a Mutex will still return an RAII guard." That just makes this RFC even worse! Memory safety is not the only safety. Leaking a lock could be even more deadly than reading invalid memory. A straw-man of this proposal is: In other words: this proposal is willing to cause any other form of breakage, as long as it is not memory breakage. |
Rust already allows for many kinds of breakage that aren't memory-unsafety. |
+1 for restricting what we mean by safety, -1 for marking In the future if we come up with a design that fixes the issue with I think it should be fine to be restrictive about what is considered |
@terpstra First, C++ can move objects from the stack to the heap just fine. It does call the destructor on the stack thing, but since it was moved from, it won't close whatever resource we're talking about (assuming the type is properly written). Example: http://ideone.com/gmCR7I Second, deadlocks are possible without leaking guards/not calling destructors (e.g., by acquire the guards in an inconsistent order). Likewise, many other wrong things can be done without it being unsafe, not least because there is no clear, usable set of rules that would disallow all wrong uses while still allowing most of the correct ones. A programming language that catches all bugs is a programming language that can't do anything. |
@rkruppe Typically you make a Guard without a copy constructor. And C++ never "moves" (in the rust sense) anything other than plain-old-data. Classes always copy construct + destruct the old value. Obviously deadlocks and other bugs are outside of Rust's control. That's not the point. The point is that RAII is a useful design pattern and discipline used in avoiding bugs. This RFC proposes to take away its teeth, rending the design pattern useless. |
A completely made up example, but: say you're writing a library that communicates over the network. Your Yes, you can just write the API another way, but this is the kind of thing that users will get wrong. Having such an easy way to shoot yourself in the foot seems like a bad idea. |
As for the RFC itself: I am very sad about this whole affair, and I don't think marking Marking Alternative: If we get at least some restricted form of negative bounds, there is a backwards-compatible way forward for guaranteed destruction: An opt-in trait (basically the complement of the |
@terpstra Anyway, I still don't quite like making |
@rkruppe: it wouldn't be backwards compatible to add negative Unless we do some pretty significant breaking changes pre-1.0, the default for unbounded generics will always have to be "safe to leak". |
I think, for example, Bluss' comment from 13 days ago (lines 55-ish) would have made a very helpful clarification on what "may not run" means. If you saw the language from the RFC in a Java spec, you would probably reach a different conclusion about intended behavior, so I don't agree that the language is already clear enough. Connecting drop and ownership makes lots of sense and is very clear. Please fix. For the record, I think it is great that |
FWIW, I totally agree that we should lay out these guarantees. I replied very early on to this effect. But I don't think it's crucial to do so in this RFC, which is primarily about how to resolve the It's worth remembering that the RFC text is not "final" -- things are frequently changed during implementation, for example, as more details emerge. What matters is what actually goes into the APIs and documentation. BTW, the effect of this RFC on the current spec is basically nil, since we already had text about leaks and safey. |
I wish the RFC was this specific! Proposed doc for mem::forget doesn't have this information either. rust-lang/rust#25187
For the record I'm completely fine with that too. |
Personally, I'm curious what legitimate use mem::forget has that doesn't involve unsafe code. Every use case I know of involves FFI. |
I would like the ability to leak data that I know will live forever so that I can create cycles arbitrarily with |
@pcwalton Can you do that with |
It needs |
I have Generally, I think the fewer methods marked |
Dont forget to mark |
|
@bombless |
|
This enables joining multiple threads simultaneously, restoring the `thread::scope` functionality removed right before 1.0, referenced in rust-lang#24292 and rust-lang/rfcs#1066. This PR brings in the `scoped` functionality from crossbeam, restoring the functionality we once had. Thanks!
Alter the signature of the
std::mem::forget
function to removeunsafe
Explicitly state that it is not considered unsafe behavior to not run
destructors.
Rendered