Hello, I'm a rust newbie. I was going through the book and I saw the following code block:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
drop(v);
handle.join().unwrap();
}
If we run we'll get:
Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v);
| ^ value used here after move
|
help: consider cloning the value before moving it into the closure
|
6 ~ let value = v.clone();
7 ~ let handle = thread::spawn(move || {
8 ~ println!("Here's a vector: {value:?}");
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
The error says ^ value used here after move and If understand correctly its the same as Use-After-Free. but shouldn't it be Double-free because its dropped after when the second thread ends and also with this drop(v);
I think I can't clearly find the difference between those two undefined behaviors.
when rust moves a value it just memcopies the whole vaule and just ignores the old (or might optimize and resuse the location) any access to the old variable is invalid. and once the value is moved it's the resoisbility of the new owner to drop it.
if you were to run drop on the old value it could be still holding the old data and cause double free in some cases
A plain print!() call instead of the drop(v) call in the main thread would not compile also, because when a value is moved into a closure, we just can't use it any longer outside. So I think that it is more a use-after-move issue. A double-free is prevented by the Rust compiler, so never can occur in safe Rust. My personal view is, that discussing code that is not accepted by the compiler for a good reason, as sometimes done in the book of Brown University, makes not too much sense -- it is just wrong and invalid.
I think this error message is more about ownership errors than use-after-free or double-free.
(The error is saying that you cannot use variable v anymore because its ownership is already transferred to the closure with the move keyword.)
Following ownership rules will prevent both use-after-free and double-free errors. The compiler enforces them, so normally, as long as your program compiles, you don't have to care about them (unless e.g. you write unsafe code, which doesn't have such memory safety guarantees).
As far as I know, the compiler doesn't make a distinction between use-after-free and double-free, except that you might occasionally see these words in some specially-written error messages (so far I haven't).
nitpick : consider using "try to run", or better "try to compile" instead.
because indeed, you did not run the code, you can't run the run the code because it fails to compile.
in rust, as in many compiled languages, the re is a strong difference between runtime and compile time, so it's better to be explicit on what exactly is runtime or compile.
in particular, compile time errros vs runtime errors are extremely different.
this would indeed lead to a double free, but destruction is a kind of "use".
this is the general pattern of use after move which may or may not include freeing or even destruction.
consider
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
v
});
println!("trying_to_print_it : {v :?}"); // won't compile, as the vector isn't there : it got sent to another thread
let v_again = handle.join().unwrap();
println!("Here it is again : {v_again :?}");
}
here it is not destroyed at all, so definitely no free, but it still has been moved and thus can't be used
Double-free is also use-after-free, since freeing is also a kind of use. So, it doesn't matter which kind of use-after-free would be happening here - it's UB in any case.
I think you are right about the double free. The key piece you are missing is that this is a COMPILE TIME error. So you basically can't have a double-free error here, because the compiler catches it before the second free occurs during compilation, due to Rust's move semantics (where moving a value to closure ensures that ownership is transferred to that closure and the closure cleans up the allocated memory on its own).
In more precise terms, Rust doesn't need to analyze "will this cause double-free?" it simply enforces that once ownership moves, the original variable is invalid. The move itself is what Rust checks, and that's sufficient to prevent both use-after-free and double-free at compile time.
Thanks for all of your explanations.
I'm kinda confused if its because:
Ownership error
they are both the same
or, It's not double free because it didn't freed again
they all seem correct to me, is there like a reference or something about that? (I mean about a precise definition of each error, I already did rustc --explain E0382).
Thanks!
This is an ownership error. It's neither user-after-free, nor double-free, because the code was never even compiled, let alone ran.
If you somehow got rid of ownership tracking, it would be both: you need to read already deinitialised contents of v to get a pointer that can be free'd.
Edit: It is actually impossible to say it is both, because there's no guarantee that you will attempt to free the same pointer that's been deallocated, or some random place in memory that never existed in the first place, since reading deinitialized v is already UB.
PS:
I feel like you might be a little confused about what drop() is. It is literally just an empty function, it doesn't do any special "freeing".
I think it's plenty useful to discuss such programs, just keep in mind that they don't have one fixed meaning and thus they don't have one, fixed reason for why they are invalid.
It's like in court: there may be many way to prove your innocence and they all can be fulfilled simultaneously.
Just compile it with mrustc? But then you couldn't run it with Miri so it wouldn't help you much…
Why the program doesn't compile: ownership error (uses a value after moving it away)
If it compiles and runs, does it count as a use-after-free or double-free?: I think it can both be called a use-after-free and a double-free, though I think the use-after-free error is broader because you cannot free something without using it
error[E0382]: use of moved value: `v`
--> src/main.rs:5:14
|
3 | let v: Vec<()> = Vec::new();
| - move occurs because `v` has type `Vec<()>`, which does not implement the `Copy` trait
4 | drop(v);
| - value moved here
5 | drop(v);
| ^ value used here after move
In particular, don't use "if it compiled this would happen" reasoning to justify unsafe: You have to understand the language's actual rules, not what you think a simple and reasonable translation would be. "Nothing bad would happen if it translated like I think it would" is irrelevant to whether there is UB or not (which, again, is defined at the language level).
Thinking about what may happen if a non-compiling program was "let through" can be an effective way to see the benefits of the ownership and borrowing rules, but not for predicting what may actually happen when the rules are broken (i.e. when UB is present).
This I know, what I meant to express is that you don't have to care about use-after-free and double-free errors unless e.g. you write unsafe code. (I edited the original post to make it more clear)
Use-after-free and such are a real problem for code using raw pointers, uninitialized memory, and other tricks/optimizations using unsafe language features.
In Rust you need to be particularly careful about your low level memory management interacting with automatic Drop and panics.
This isn't caused by unsafe itself, but by low-level language features that unsafe allows you to start using.
drop() is just a function that takes a value. It does not do anything with the value. A value moved into the drop() function is dropped when drop() ends. If drop() were called with your value and the drop() code executed, you would have a double-free. But you don't get that far. You can not even call the drop() function with your value 'v' because 'v' has already been moved into your closure. So you get the use after move error.