Skip to content

Commit c816b61

Browse files
committed
Guaranteed non-static destructors
Guarantee that the destructor for data that contains a borrow is run before any code after the borrow’s lifetime is executed.
1 parent 0790192 commit c816b61

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
- Feature Name: Guaranteed non-static destructors
2+
- Start Date: 2015-04-27
3+
- RFC PR: (leave this empty)
4+
- Rust Issue: (leave this empty)
5+
6+
# Summary
7+
8+
Guarantee that the destructor for data that contains a borrow is run before any
9+
code after the borrow’s lifetime is executed.
10+
11+
# Motivation
12+
13+
Rust is currently not guaranteed to run destructors on an object before
14+
execution moves beyond its bounding-lifetime. This is surprising,
15+
unintuitive, and leads to soundness mistakes. This is evidenced by two recently
16+
approved API designs: `thread::scopped` and `drain_range`, both of which were
17+
found to be unsound because they mistakenly assumed that their destructor would
18+
run before any code outside of their bounding lifetimes could be executed. Even
19+
if both are fixed, it is very likely that similar errors will arise in the
20+
future.
21+
22+
While we can try to continuously hammer home that leaks are not considered
23+
`unsafe`, we'll never be able to prevent all such mistakes. It would be better
24+
if we could actually provide the intuitive guarantee, which is what this RFC
25+
attempts to accomplish.
26+
27+
Note that this RFC is explicitly not attempting to solve all leaks, nor even to
28+
ensure that all stack-anchored objects are destructed. Rust is primarily
29+
concerned with memory safety, so this RFC tries to formulate a general rule
30+
that addresses the issue of incorrect assumptions about destructors leading to
31+
memory unsafety. It does not attempt to prevent the leaking of other resources,
32+
as leaking a `'static` object is never unsafe. (`'static` objects are
33+
conceptually safe to keep around “forever”, so any unsoundness that can be
34+
obtained by a leak could also be obtained through other means).
35+
36+
# Detailed design
37+
38+
In addition to current guarantees, the following property can be relied upon:
39+
40+
Given an object `A` implementing `Drop` whose lifetime is restricted to `'a`
41+
(that is, the object may not live longer than `'a`), in absence of unsafe code,
42+
the destructor for `A` is guaranteed to run before any code after the end of
43+
`'a` is executed.
44+
45+
This is already true at the language level. However, certain library constructs
46+
currently allow safe code to break it.
47+
48+
This has the following implications:
49+
50+
* It is perfectly acceptable to forget, leak, et cetera any object as long as
51+
that object is valid for the duration of `'static`. Intuitively, this makes
52+
sense, as forgetting an object is the same as having it live forever.
53+
* A resource that is never freed due to and endless loop, a deadlock, or
54+
program termination is valid because any code that comes after the end of
55+
`'a` is never executed.
56+
* Code would be allowed to rely on the guarantee for soundness. This means
57+
that patterns such as `thread::scoped` and the initially-proposed
58+
`Vec::drain_range` would be sound.
59+
* Unsafe code is free to forget objects as needed in cases where the
60+
programmer guarantees it is sound. However, it is not allowed to expose a
61+
safe interface that would allow the above guarantee to be violated.
62+
63+
As noted, this guarantee is already true at the core language level. However,
64+
it can be violated in safe code when using the standard library. There are
65+
two known ways in which this can happen: when a destructor panics in certain
66+
situations (e.g., in a `Vec`), and when a reference cycle is created using `Rc`
67+
and friends.
68+
69+
This RFC proposes the following solutions:
70+
71+
* Specify that any panic that occurs while a destructor is in progress results
72+
in an abort. Panicking in a destructor is generally a bad idea with many
73+
edge cases, so this is probably desirable, anyway. It should be possible to
74+
implement this efficiently in a similar manner to C++’s `noexcept`.
75+
* Restrict the basic `new` operation of existing reference-counted types to
76+
types valid for `'static`, which are always safe to leak. Additionally,
77+
introduce a scoped cycle collector that can be used to safely create `Rc`s
78+
with a shorter lifetime, and research the possibility of a reference-counted
79+
type that statically disallows cycles.
80+
81+
Specifically, the scoped cycle collector would operate as follows:
82+
83+
* There `RcGuard` type that can be instantiated with a given lifetime `'a`.
84+
* Safe code can use the guard object to safely create `Rc`s with any type that
85+
outlives `'a`.
86+
* When the `RcGuard` is dropped, it drops the data of all `Rc's` created with
87+
it. This would free any cycles.
88+
* Attempting to dereference any `Rc` whose content had already been dropped
89+
(e.g., during cycle clean-up) would cause a panic (and thus an abort if it
90+
happened during RcGuard's destructor).
91+
92+
The guard technique can also be applied to channels to ensure that they are
93+
properly cleaned up even if the user does something like send the receiver and
94+
sender into their own channel.
95+
96+
# Drawbacks
97+
98+
* Using `Rc`s for non-`'static` types will be slightly less convenient. The
99+
user will either have to use the cycle guard, an acyclic Rc type, or use
100+
`unsafe` and manually verify that no leaks are possible. (In the compiler,
101+
for instance, the guard would need to be somewhere outlived by the type
102+
arenas (perhaps in the `ctxt` object)).
103+
* Probably not enough time to implement all of this by 1.0. This can be
104+
mitigated by implementing the minimum necessary to make this
105+
backward-compatible. Since the behavior of panicking destructors are already
106+
unspecified and subject to change, this would mean restricting the safe
107+
creation of reference-counted objects to containing `'static` values and
108+
adding an `unsafe` way to use shorter lifetimes. This would make using `Rc`s
109+
for non-`'static` data impossible without `unsafe` in 1.0, which is not
110+
ideal.
111+
112+
# Alternatives
113+
114+
* Don't consider failing to destruct non-`'static` data unsafe.
115+
116+
This is more or less the status quo, barring certain adjustments such as
117+
removing `unsafe` from `mem::forget`.
118+
119+
This would be unfortunate, as it makes a very common RAII pattern unusable
120+
for anything memory-safety related. Furthermore, it breaks expectations. It
121+
seems like using an RAII guard should be memory safe, to the point that two
122+
new APIs were recently designed that relied on it for soundness, despite the
123+
fact that leaks have not been considered unsafe for a long time.
124+
125+
It is very likely that people will continue to make this mistake (even if
126+
the core team doesn't, third party developers almost certainly will). Rust’s
127+
strong lifetime ownership semantics make it seem like something that should
128+
be reliable. This is compounded by the fact that it is true of the core
129+
language, and “true-enough” in practice that developers will continue to
130+
assume that it can be relied upon.
131+
132+
Even if the programmer is aware of the limitation, taking it into account
133+
can lead to more difficult and convoluted API design to ensure soundness.
134+
Given that they are *usually* reliable, a programmer my opt to go with an
135+
RAII design anyway to save time, reducing the value of Rust’s safety
136+
guarantees.
137+
138+
* Leave `Rc` and friends. Add a `Leak` trait or similar. All APIs that can
139+
potentially leak (such as `Rc`) would have a `Leak` bound, and programmers
140+
who want to rely on their destructor being called before their lifetime ends
141+
would have to add a `Leak` bound to opt out of being usable with `Rc`s,
142+
channels, et cetera.
143+
144+
While `!Leak` would technically only be needed for types can that lead to
145+
unsoundness if their destructor were skipped, many would probably use it for
146+
any guard-like type whose destructor “should” run, preventing their use in
147+
certain contexts unnecessarily.
148+
149+
Additionally, it is likely that there will be types with *do* need the
150+
guarantees for memory safety that otherwise would be useful to keep in an
151+
`Rc` or send over a channel, which would not be possible.
152+
153+
Instead of defining a single, simple rule for all types, the `Leak`
154+
trait would have to be specifically dealt with all over, e.g., by adding
155+
`+Leak` to the bounds of anything that you want to put in an `Rc`. Also, if
156+
leaking `drain_range` or something similar is made to leak the referenced
157+
values instead of leading to unsoundness, it needs to be `Leak` if and only
158+
if the contained type is. This adds complexity and mental burden.
159+
160+
Finally, while some of the solutions proposed in this RFC for handling
161+
non-`'static` data could be applied to `!Leak` data, the added complexity
162+
added by a `Leak` trait would be even less worth it if the complexity to
163+
work around it had to be added to `Rc`, channels, et cetera, anyway.
164+
165+
* Guarantee that all stack-anchored objects have their destructor run if the
166+
program exits normally.
167+
168+
This is much more challenging, would require sweeping changes in many areas,
169+
and is not even that useful: you can always move a `'static` object off the
170+
stack into a static location. Also, since `'static` objects can conceptually
171+
live forever, there seems little benefit to attempting to enforce otherwise.
172+
173+
In addition, there several benefits to being able to being able to safely
174+
forget static data, such as forgetting a `File` to keep the file from being
175+
closed once you have transferred the underlying file descriptor.
176+
177+
# Unresolved questions
178+
179+
What are the best designs for safely allowing channels and `Rc`s to safely work
180+
with non-`'static` data?

0 commit comments

Comments
 (0)