30
loading...
This website collects cookies to deliver better user experience
pointer aliasing
. This means that your code has more than one pointer pointing to some memory, perhaps in different parts of your code. When one pointer is used to free some memory, the other pointers now "dangle". This means, they point to nothing. And so the main reason use-after-free occurs is when these other pointers continue to be used. That can lead to a crash (or segmentation fault). Look at this C++ code to demonstrate how this can happen:std::vector<int> v;
v.push(1);
const int& ref_v = v[0]; // a
v.push(2); // b
std::cout << "v[0] = " << ref_v; // c
a
, we take a reference, which is a pointer, to the first element of the C++ vector. After we push the next value in line b
, it is possible that the memory used by the vector is moved because it needs to be enlarged. This will invalidate the reference stored in ref_v
. It is, in effect, pointing to garbage. Then we use it in line c
- a use-after-free bug.int gCounter = 0;
void increment_counter()
{
int counter = gCounter; // a
counter = counter + 1; // b
gCounter = counter; // c
}
a
and b
after thread 2 has executed line a
and before it has executed line c
, both threads will write 1 to gCounter
. This is called a data race. Although this is often not a crash bug, it can be amazingly hard to track down. This is because unless those or similar conditions happen, you will not have a data race. So, in more complex situations, your application may run correctly 99% of the time, resulting in you desperately trying to recreate that 1% occurrence to track down the bug.unsafe
keyword (used to lower Rust's shields when you need to). They are impossible to occur because any such attempt to produce those bugs results in a compilation error. This means that you can't even start a program with those bugs. This is a huge win for programmers.let a = vec![1, 2, 3];
let b = a; // Ownership of vector is transferred to b.
let c = a; // Compiler error! a does not own any data.
a
variable owns the vector [1, 2, 3]
. In line 2, by assigning a
to b
, you transfer ownership. The variable b
now owns the vector, and as a result a
owns nothing. It is a compilation error to use a
afterwards. So as a result, in line 3 you would get a compilation error.fn print_vector(v: Vec<i32>) {
println!("{}", v);
}
...
let a = vec![1, 2, 3]
print_vector(a);
println!("{}", a); // Compilation error!!!
a
as a parameter to print_vector
, you transfer ownership to print_vector
's local variable v
. When v
goes out of scope as the function ends, the compiler knows to destroy the vector. Using a
afterwards is a compilation error.&
symbol before the type. You are essentially creating a pointer. Looking at an example:let a = vec![1, 2, 3];
let b = &a;
let c = &a;
let mut a = vec![1, 2, 3];
let b = &a;
let c = &a;
a.push(4);
b
and c
are not used any more and a
is free to mutate its data.let mut a = vec![1, 2, 3];
let b = &a;
let c = &a;
a.push(4);
println!("{:?}", b);
b
in the println!
statement, we are using an immutable borrow at the same time as we are mutating the data. Remember, as in the C++ example above, pushing more data can move the memory used by the vector and invalidate b
and c
causing a use-after-free bug.mut
keyword after the &
. Let us look at the increment_counter
function above rewritten in Rust:fn increment_counter(counter: &mut i32) {
*counter = *counter + 1;
}
unsafe
keyword to change them, so we pass it as a parameter. We're only talking about safe Rust in this article for now.*
operator to dereference them.&[T]
where T is any data type. Without the ampersand, the type [T]
is just a fixed array and needs to be owned.&mut [T]
.str
, which is an alias for [char]
, so whenever you see &str
, think of it as a slice of a character array. String literals will have the type &str
as they are sliced views on static data in your executable. So in this code:let a = "hello";
a
is of type &str
.'name
where name
can be anything. Usually, these lifetime annotations can be implied. For example, when you write:fn substr(s: &str, index: usize, len: usize) -> &str;
fn substr<'a>(s: &'a str, index: usize, len: usize) -> &'a str;
a
as the name of the lifetime. And as you can see, the lifetime of the result must match the lifetime of the input parameter s
. This makes sense as the function will return a reference to the data passed in as s
. Notice, that the lifetime annotation is declared within <...>
next to the function's name.lifetime elision
. Rust has various rules for eliding lifetimes. If your function is more complicated, the compiler might complain and you will have to add the annotations yourself. If you're interested you can find more information here.'static
. This means that the data lasts as long as the program itself. Functions have lifetimes of 'static
for example. Also literal strings are immutable character slices (i.e. &str
) with a lifetime of 'static
.struct Person {
name: &str,
age: u32,
}
error[E0106]: missing lifetime specifier
--> test.rs:2:11
|
2 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 | struct Person<'a> {
2 | name: &'a str,
|
Person
structure cannot outlive the data that name
references. If it did, we'd have a dangling pointer.Type | Syntax |
---|---|
Owner | T |
Immutably borrowed | &T |
Mutably borrowed | &mut T |
Immutable borrowed slice | &[T] |
Mutably borrowed slice | &mut [T] |
let mut a: i32;
println!("{}", a);
a = 42;
a
is undefined when println!
is executed and so the compiler stops this from happening. However, it is OK to initialise a
on a different statement as long as you don't use a
before you do so:// This is OK
let a: i32;
a = 42;
a
doesn't even have to be defined mut
because Rust sees that the a = 42
statement is in fact an initialisation and not a mutation.Option
enum, which I will be visiting in my next blog.Copy
trait. I will talk about traits another time, but essentially it means that the type is marked with a property. That property allows the compiler to copy the data byte-for-byte into the new variable or parameter. This means that ownership is not transferred but a new owner is created with a copy of the data.Vec
does not implement the Copy
trait. Types that do implement it are so-called POD types (Pieces Of Data) such as i32
, u32
, f32
, char
etc. Because of this, the following code is correct:let a = 42; // a is an i32, which implements the Copy trait
let b = a;
let c = a;
b
and c
become owners of the copy of a
's data. So, both b
and c
will hold the value 42.#[derive(Copy, Clone)]
struct Person {
name: String,
age: u32,
}
Clone
trait must also be implemented if Copy
is. The Clone
trait provides the clone()
method on the type and is required to do the actual copy. After adding the #[derive(Copy, Clone)]
statement, this code is possible:let p = Person { name: String::from("Matt"), age: 48 };
let a = p;
let b = p;
b
and c
will hold different instances of Person
but contain the same values.Option
and Result
enum types.