]> git.lizzy.rs Git - rust.git/blob - src/doc/tarpl/leaking.md
dcb03b1c8b6a0df9ba93235f2ac3e3fe7fead53c
[rust.git] / src / doc / tarpl / leaking.md
1 % Leaking
2
3 Ownership-based resource management is intended to simplify composition. You
4 acquire resources when you create the object, and you release the resources when
5 it gets destroyed. Since destruction is handled for you, it means you can't
6 forget to release the resources, and it happens as soon as possible! Surely this
7 is perfect and all of our problems are solved.
8
9 Everything is terrible and we have new and exotic problems to try to solve.
10
11 Many people like to believe that Rust eliminates resource leaks. In practice,
12 this is basically true. You would be surprised to see a Safe Rust program
13 leak resources in an uncontrolled way.
14
15 However from a theoretical perspective this is absolutely not the case, no
16 matter how you look at it. In the strictest sense, "leaking" is so abstract as
17 to be unpreventable. It's quite trivial to initialize a collection at the start
18 of a program, fill it with tons of objects with destructors, and then enter an
19 infinite event loop that never refers to it. The collection will sit around
20 uselessly, holding on to its precious resources until the program terminates (at
21 which point all those resources would have been reclaimed by the OS anyway).
22
23 We may consider a more restricted form of leak: failing to drop a value that is
24 unreachable. Rust also doesn't prevent this. In fact Rust has a *function for
25 doing this*: `mem::forget`. This function consumes the value it is passed *and
26 then doesn't run its destructor*.
27
28 In the past `mem::forget` was marked as unsafe as a sort of lint against using
29 it, since failing to call a destructor is generally not a well-behaved thing to
30 do (though useful for some special unsafe code). However this was generally
31 determined to be an untenable stance to take: there are *many* ways to fail to
32 call a destructor in safe code. The most famous example is creating a cycle of
33 reference-counted pointers using interior mutability.
34
35 It is reasonable for safe code to assume that destructor leaks do not happen, as
36 any program that leaks destructors is probably wrong. However *unsafe* code
37 cannot rely on destructors to be run to be *safe*. For most types this doesn't
38 matter: if you leak the destructor then the type is *by definition*
39 inaccessible, so it doesn't matter, right? For instance, if you leak a `Box<u8>`
40 then you waste some memory but that's hardly going to violate memory-safety.
41
42 However where we must be careful with destructor leaks are *proxy* types. These
43 are types which manage access to a distinct object, but don't actually own it.
44 Proxy objects are quite rare. Proxy objects you'll need to care about are even
45 rarer. However we'll focus on three interesting examples in the standard
46 library:
47
48 * `vec::Drain`
49 * `Rc`
50 * `thread::scoped::JoinGuard`
51
52
53
54 ## Drain
55
56 `drain` is a collections API that moves data out of the container without
57 consuming the container. This enables us to reuse the allocation of a `Vec`
58 after claiming ownership over all of its contents. It produces an iterator
59 (Drain) that returns the contents of the Vec by-value.
60
61 Now, consider Drain in the middle of iteration: some values have been moved out,
62 and others haven't. This means that part of the Vec is now full of logically
63 uninitialized data! We could backshift all the elements in the Vec every time we
64 remove a value, but this would have pretty catastrophic performance
65 consequences.
66
67 Instead, we would like Drain to *fix* the Vec's backing storage when it is
68 dropped. It should run itself to completion, backshift any elements that weren't
69 removed (drain supports subranges), and then fix Vec's `len`. It's even
70 unwinding-safe! Easy!
71
72 Now consider the following:
73
74 ```rust,ignore
75 let mut vec = vec![Box::new(0); 4];
76
77 {
78     // start draining, vec can no longer be accessed
79     let mut drainer = vec.drain(..);
80
81     // pull out two elements and immediately drop them
82     drainer.next();
83     drainer.next();
84
85     // get rid of drainer, but don't call its destructor
86     mem::forget(drainer);
87 }
88
89 // Oops, vec[0] was dropped, we're reading a pointer into free'd memory!
90 println!("{}", vec[0]);
91 ```
92
93 This is pretty clearly Not Good. Unfortunately, we're kind've stuck between a
94 rock and a hard place: maintaining consistent state at every step has an
95 enormous cost (and would negate any benefits of the API). Failing to maintain
96 consistent state gives us Undefined Behaviour in safe code (making the API
97 unsound).
98
99 So what can we do? Well, we can pick a trivially consistent state: set the Vec's
100 len to be 0 when we *start* the iteration, and fix it up if necessary in the
101 destructor. That way, if everything executes like normal we get the desired
102 behaviour with minimal overhead. But if someone has the *audacity* to
103 mem::forget us in the middle of the iteration, all that does is *leak even more*
104 (and possibly leave the Vec in an *unexpected* but consistent state). Since
105 we've accepted that mem::forget is safe, this is definitely safe. We call leaks
106 causing more leaks a *leak amplification*.
107
108
109
110
111 ## Rc
112
113 Rc is an interesting case because at first glance it doesn't appear to be a
114 proxy value at all. After all, it manages the data it points to, and dropping
115 all the Rcs for a value will drop that value. Leaking an Rc doesn't seem like it
116 would be particularly dangerous. It will leave the refcount permanently
117 incremented and prevent the data from being freed or dropped, but that seems
118 just like Box, right?
119
120 Nope.
121
122 Let's consider a simplified implementation of Rc:
123
124 ```rust,ignore
125 struct Rc<T> {
126     ptr: *mut RcBox<T>,
127 }
128
129 struct RcBox<T> {
130     data: T,
131     ref_count: usize,
132 }
133
134 impl<T> Rc<T> {
135     fn new(data: T) -> Self {
136         unsafe {
137             // Wouldn't it be nice if heap::allocate worked like this?
138             let ptr = heap::allocate<RcBox<T>>();
139             ptr::write(ptr, RcBox {
140                 data: data,
141                 ref_count: 1,
142             });
143             Rc { ptr: ptr }
144         }
145     }
146
147     fn clone(&self) -> Self {
148         unsafe {
149             (*self.ptr).ref_count += 1;
150         }
151         Rc { ptr: self.ptr }
152     }
153 }
154
155 impl<T> Drop for Rc<T> {
156     fn drop(&mut self) {
157         unsafe {
158             let inner = &mut ;
159             (*self.ptr).ref_count -= 1;
160             if (*self.ptr).ref_count == 0 {
161                 // drop the data and then free it
162                 ptr::read(self.ptr);
163                 heap::deallocate(self.ptr);
164             }
165         }
166     }
167 }
168 ```
169
170 This code contains an implicit and subtle assumption: ref_count can fit in a
171 `usize`, because there can't be more than `usize::MAX` Rcs in memory. However
172 this itself assumes that the ref_count accurately reflects the number of Rcs
173 in memory, which we know is false with mem::forget. Using mem::forget we can
174 overflow the ref_count, and then get it down to 0 with outstanding Rcs. Then we
175 can happily use-after-free the inner data. Bad Bad Not Good.
176
177 This can be solved by *saturating* the ref_count, which is sound because
178 decreasing the refcount by `n` still requires `n` Rcs simultaneously living
179 in memory.
180
181
182
183
184 ## thread::scoped::JoinGuard
185
186 The thread::scoped API intends to allow threads to be spawned that reference
187 data on their parent's stack without any synchronization over that data by
188 ensuring the parent joins the thread before any of the shared data goes out
189 of scope.
190
191 ```rust
192 pub fn scoped<'a, F>(f: F) -> JoinGuard<'a>
193     where F: FnOnce() + Send + 'a
194 ```
195
196 Here `f` is some closure for the other thread to execute. Saying that
197 `F: Send +'a` is saying that it closes over data that lives for `'a`, and it
198 either owns that data or the data was Sync (implying `&data` is Send).
199
200 Because JoinGuard has a lifetime, it keeps all the data it closes over
201 borrowed in the parent thread. This means the JoinGuard can't outlive
202 the data that the other thread is working on. When the JoinGuard *does* get
203 dropped it blocks the parent thread, ensuring the child terminates before any
204 of the closed-over data goes out of scope in the parent.
205
206 Usage looked like:
207
208 ```rust,ignore
209 let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
210 {
211     let guards = vec![];
212     for x in &mut data {
213         // Move the mutable reference into the closure, and execute
214         // it on a different thread. The closure has a lifetime bound
215         // by the lifetime of the mutable reference `x` we store in it.
216         // The guard that is returned is in turn assigned the lifetime
217         // of the closure, so it also mutably borrows `data` as `x` did.
218         // This means we cannot access `data` until the guard goes away.
219         let guard = thread::scoped(move || {
220             *x *= 2;
221         });
222         // store the thread's guard for later
223         guards.push(guard);
224     }
225     // All guards are dropped here, forcing the threads to join
226     // (this thread blocks here until the others terminate).
227     // Once the threads join, the borrow expires and the data becomes
228     // accessible again in this thread.
229 }
230 // data is definitely mutated here.
231 ```
232
233 In principle, this totally works! Rust's ownership system perfectly ensures it!
234 ...except it relies on a destructor being called to be safe.
235
236 ```rust,ignore
237 let mut data = Box::new(0);
238 {
239     let guard = thread::scoped(|| {
240         // This is at best a data race. At worst, it's *also* a use-after-free.
241         *data += 1;
242     });
243     // Because the guard is forgotten, expiring the loan without blocking this
244     // thread.
245     mem::forget(guard);
246 }
247 // So the Box is dropped here while the scoped thread may or may not be trying
248 // to access it.
249 ```
250
251 Dang. Here the destructor running was pretty fundamental to the API, and it had
252 to be scrapped in favour of a completely different design.