Module 12: Ownership
Lecture 10: Wednesday, June 10, 2026.
Code examples
This module looks at the following concepts:
- How to store data on the heap to avoid dangling pointers?
- The problems of manual memory management: memory leaks and use-after-free.
- Boxes: heap allocation with ownership.
- What is ownership?
Dangling Pointers Revisited
Pointers can be created in many ways:
- They can be created by taking the address of an existing value/variable.
- They can be created using
malloc. - They can be created by manipulating other pointers (e.g. using
ptr.add(<some offset>)).
But even if a pointer is valid when created, it can become dangling. To demonstrate this, let’s consider the following code.
fn main() { let mut v: Vec<String> = vec![String::from("str1"), String::from("str2")]; // pointer to the first element. let ptr0: *const String = &v[0] as *const String; // We are inserting many elements to the vector. // This causes the vector to resize and changes the location // of its elements in memory. for i in 0..10 { v.push(format!("str{}", i)); } // Now, the old ptr address is no longer valid. println!("address of first element used to be {:p}", ptr0); println!("address of first element became {:p}", &v[0]); unsafe { println!("dereferencing the pointer"); println!("{}", *ptr0); println!("program done!"); } }
The above code is unsafe and quite dangerous. The pointer ptr0 becomes dangling after pushing new elements to the vector,
meaning that dereferencing it is dangerous. Indeed, the code crashes when attempting to dereference it.
Pointers to Heap-Allocated Data
Let’s think about why the pointer became dangling in the first place. The core issue is that the elements of the vector move: when we push enough elements, the vector resizes, and moves all its elements to a new, bigger heap allocation. Our pointer still contains the old address, but the string is not there anymore.
One way to fix this is to not store the strings inside the vector at all. Instead, we can store every string in its own separate allocation on the heap, and only store pointers to the strings inside the vector.
We can do this using the tools we learned in the previous module: libc::malloc to allocate memory on the heap, and std::ptr::write to initialize it. Let’s wrap them in a small helper function put_on_heap that takes a string, puts it on the heap, and returns a pointer to it.
fn put_on_heap(x: String) -> *mut String { unsafe { // Allocate enough memory on the heap to store a String, // then initialize that memory with x. let ptr: *mut String = libc::malloc(size_of::<String>()) as *mut String; std::ptr::write(ptr, x); ptr } } fn main() { let mut v: Vec<*mut String> = vec![ put_on_heap(String::from("str1")), put_on_heap(String::from("str2")) ]; // pointer to the first element. let ptr0: *mut String = v[0]; // We are inserting many elements to the vector, like before. // The vector still resizes, and its elements still move in memory. // But the elements are now just pointers (i.e., addresses)! // The strings themselves never move from their own heap allocations. for i in 0..10 { v.push(put_on_heap(format!("str{}", i))); } println!("address of first string used to be {:p}", ptr0); println!("address of first string is still {:p}", v[0]); unsafe { println!("dereferencing the pointer"); println!("{}", *ptr0); println!("program done!"); } }
Run this code: it completes successfully! When the vector resizes, it moves its elements to a new location, but the elements are just addresses. Moving an address to a new location does not change the address itself: ptr0 and v[0] both still point to where the string "str1" lives on the heap, which never changed.
Memory Leaks and the Absence of Ownership
Our fix solved the dangling pointer problem, but it created a new problem: we are now managing the heap data ourselves!
When the vector gets destroyed at the end of the function, it only frees its own heap allocation: the one containing its elements. The vector has no idea that its elements are pointers to strings that we allocated, and it would not free or destruct them. If we do nothing, the strings remain on the heap forever, even though no one can use them anymore. This is a memory leak.
To avoid the leak, we must remember to clean up after ourselves: for every string, we must destruct it (with std::ptr::read from the previous module), and free the memory we allocated for it with malloc (with libc::free).
fn put_on_heap(x: String) -> *mut String { unsafe { let ptr: *mut String = libc::malloc(size_of::<String>()) as *mut String; std::ptr::write(ptr, x); ptr } } fn main() { let mut v: Vec<*mut String> = vec![ put_on_heap(String::from("str1")), put_on_heap(String::from("str2")) ]; // ... use the vector ... unsafe { println!("{}", *v[0]); println!("{}", *v[1]); } // We are done with the vector: we must manually delete every element. unsafe { std::ptr::read(v[0]); // destructs the string. libc::free(v[0] as *mut libc::c_void); // frees the malloc-ed memory. std::ptr::read(v[1]); // destructs the string. libc::free(v[1] as *mut libc::c_void); // frees the malloc-ed memory. } // The pointers in the vector are now all dangling! // We clear the vector so that no one can use them by accident. v.clear(); println!("program done!"); }
Forgetting to delete the strings is bad, but the opposite mistake is even worse: deleting a string too early, while some part of the program still intends to use it.
The code below demonstrates this. It uses std::ptr::read to destruct the first string in the vector. Then, a later part of the program grabs a pointer to that string from the vector and tries to print it anyways.
fn put_on_heap(x: String) -> *mut String { unsafe { let ptr: *mut String = libc::malloc(size_of::<String>()) as *mut String; std::ptr::write(ptr, x); ptr } } fn main() { let mut v: Vec<*mut String> = vec![ put_on_heap(String::from("str1")), put_on_heap(String::from("str2")) ]; // Somewhere in the program, we decide we are done with the // first string, and destroy it. unsafe { std::ptr::read(v[0]); // destructs the string. } // ... later, a different part of the program tries to use it! let ptr0: *mut String = v[0]; unsafe { println!("{}", *ptr0); println!("program done!"); } }
This is a use-after-free: the pointer ptr0 points to a string that was already destroyed. Just like dereferencing a dangling pointer, this is undefined behavior: the program may crash, print garbage, or appear to work fine until it fails mysteriously later.
Notice the deeper issue behind all these problems: with raw pointers, no one is responsible for the string. The vector stores its address, ptr0 stores its address, and any other part of the program may copy that address too. Any of them can destroy the string at any time, and none of them can tell whether the others are done with it. There is no owner. This is why it is so easy to leak memory (everyone assumes someone else will free it), to use-after-free (someone frees it while others still use it), or to double free (two parties free it, like we saw in the previous module).
Boxes and Ownership
Rust provides a safe alternative that gives us the benefits of our put_on_heap function without its dangers: Box.
Box::new(data) allocates memory on the heap, moves the data into it, and returns a box pointing to that data. So far, this is exactly what put_on_heap did. The crucial difference is that the box owns the data it points to: when the box itself gets destroyed (e.g., it goes out of scope, or the vector containing it is destroyed), Rust automatically destructs the heap data and frees its memory.
Let’s rewrite our vector example using boxes instead of raw pointers.
fn main() { let mut v: Vec<Box<String>> = vec![ Box::new(String::from("str1")), Box::new(String::from("str2")) ]; // Just like with put_on_heap, every string lives in its own // heap allocation that never moves, and the vector only stores // the addresses. println!("address of first string used to be {:p}", v[0]); for i in 0..10 { v.push(Box::new(format!("str{}", i))); } println!("address of first string is still {:p}", v[0]); println!("{}", v[0]); // No manual clean up needed! When v gets destroyed here, it // destroys the boxes inside it, and every box automatically // destructs its string and frees its heap allocation. println!("program done!"); }
Notice how the boxes solve all of our problems at once:
- The strings have stable addresses on the heap that do not change when the vector resizes, so no dangling pointers.
- We do not need to remember to delete anything: when the vector is destroyed, the boxes inside it are destroyed, and each box automatically cleans up its string. No memory leaks.
- We cannot destroy a string too early or twice: the only way to destroy the string is to destroy the box that owns it, and the Rust compiler tracks the box and refuses to compile code that uses it after that point. No use-after-free and no double free.
Let’s see that last point in action. The code below destroys a box with drop, and then tries to use it anyways. With raw pointers, this was a use-after-free that the compiler happily accepted. With boxes, the Rust compiler catches the mistake and refuses to compile the code. Try to run it and look at the compilation error!
fn main() { let b: Box<String> = Box::new(String::from("str1")); println!("{}", b); // This destroys the box, which destructs its string and // frees the heap memory. drop(b); // The compiler knows b was destroyed and will not let us use it! println!("{}", b); }
The reason this works is that every string now has exactly one clear owner: the box. This idea is so powerful that Rust applies it to all data, not just boxes. It is called ownership, and it is the topic of the rest of this module.
What Is Ownership?
Rust’s philosophy and design is based on the notion of ownership. Specifically, that a variable or a piece of data owns the resources associated with that data. The resources we are specifically talking about here are any heap allocations required for that data.
In other words, a vector owns the memory its elements are stored at in the heap. A string also owns the memory where its characters are on the heap, etc.
Rust uses this idea to ensure the following:
- The data allocates and initializes any memory it needs when it is created. E.g., a vector allocates the data it needs on the heap.
- The memory and resources can be destroyed or freed when the object that owns them is destroyed.
Let’s look at moving, cloning, and borrowing data in light of this view of ownership.
Ownership and Move
When we move data, we are transferring ownership of it from one variable to another. For example, look at this code:
fn main() { let x: String = String::from("hello"); // this moves x to y let y: String = x; println!("{}", y); // println!("{}", x); }
The code above moves the String "hello" and all of its resources and heap allocations from x to y.
This means that after the move, y owns the string and allocations and controls when they get deleted.
It also means that x no longer owns it!
Try to print x and run the code. What do you think will happen?
Ownership and Clone
On the other hand, when we clone something, we create a new copy of it and give that copy new ownership. The original data is unaffected, and remains owned by whatever was owning it before.
fn main() { let x: String = String::from("hello"); // this clones x to y let mut y: String = x.clone(); // y.push_str(" everyone!"); println!("{}", y); println!("{}", x); }
Try to modify y by adding more characters to it. What do you think will happen? What if we drop x? Would y be affected?
Ownership and References/Borrowing
Finally, when we borrow some data, we do not transfer over its ownership nor do we copy it elsewhere. We simply create a reference to it.
fn main() { let x: String = String::from("hello"); // this borrows x let y: &String = &x; println!("{}", y); drop(y); println!("{}", x); }
Notice how in the above, we can print x and its borrow y, and that we can drop the reference y without affecting the string, since it is owned by x.
Note however that while x remains the owner of the string, our ability to use it gets restricted while it is actively being borrowed.
This means we cannot destroy it while y is active, but could destroy it after y is done.
Teaser: References and the Borrow Checker
References are really similar to pointers: they are also based on addresses. However, unlike pointers, they are safe to use!
fn main() { let x1: i32 = 10; let ref_x1: &i32 = &x1; println!("address of x1 {:p}", &x1); println!("ref_x1 refers to address {:p}", ref_x1); println!("ref_x1 refers to value {}", ref_x1); }
By contrast, look at the code below. Rust realizes this code is potentially dangerous and does not let us compile it! Specifically, it realizes that after creating ref0, but before using it, the vector is mutated using push, which causes dangerous behavior.
Try to run the code and look at the compilation error.
fn main() { let mut v: Vec<String> = vec![String::from("str1"), String::from("str2")]; // reference to the first element. let ref0: &String = &v[0]; // We are inserting many elements to the vector. // This causes the vector to resize and changes the location // of its elements in memory. for i in 0..10 { v.push(format!("str{}", i)); } // Now, the old reference is no longer valid. println!("{}", ref0); }
We will learn a system based on permissions to help us understand how and why Rust does these checks about references in the next module.