DEV Community

Cover image for Handling memory leaks in Rust
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

Handling memory leaks in Rust

Written by Ukeje Goodness✏️

Rust’s low-level nature gives you access to resources and memory, so many developers choose Rust for their projects across various tech sectors. Ownership and borrowing are among the first concepts you’ll learn when dealing with Rust, as they form the primitives for how Rust handles memory.

Once in a while, you may experience memory leaks in your Rust projects due to many factors, from unsafe code to shared references, etc. Ideally, you’ll want to fix these memory leaks and ensure your programs are efficient, which may lead to performance gains and resource safety.

Memory leaks and unsafe behavior

Rust’s built-in ownership model and compile-time checks reduce the possibility and risks you’ll encounter with memory leaks, but they’re still quite possible.

Memory leaks don’t violate the ownership rules, so the borrow checker lets them slide at compile time. Leaking memory is inefficient and generally not a great idea, especially if you have resource constraints.

On the other hand, unsafe behavior can also slide if you embed it in an unsafe block. In this case, memory safety is your responsibility regardless of the operation, e.g. pointer dereferencing, manual memory allocation, or concurrency issues.

Memory Leaks via ownership and borrowing

Rust does not use a garbage collector. Instead, it uses ownership and borrowing (the borrow checker enforces the ownership model), which form the core principles for memory handling in Rust programs.

The borrow checker prevents dangling references, use-after-free errors, and data races at compile time before the compiler executes the program. Still, memory leaks can occur when memory is allocated without dropping it throughout the execution time.

Here’s an example of how I implement a doubly linked list. The program would run successfully, but there would also be a memory leak issue:

use std::rc::Rc;
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Rc<RefCell<Node>>>,
}

fn main() {
    let first = Rc::new(RefCell::new(Node {
        value: 1,
        next: None,
        prev: None,
    }));

    let second = Rc::new(RefCell::new(Node {
        value: 2,
        next: Some(Rc::clone(&first)),
        prev: Some(Rc::clone(&first)),
    }));

    first.borrow_mut().next = Some(Rc::clone(&second));
    first.borrow_mut().prev = Some(Rc::clone(&second));

    println!("Reference count of first: {}", Rc::strong_count(&first)); 
    println!("Reference count of second: {}", Rc::strong_count(&second)); 

}
Enter fullscreen mode Exit fullscreen mode

The problem with this program occurs with the circular reference between two nodes, resulting in a memory leak. Since RC smart pointers don’t handle cyclic references by default, each node holds a strong reference to the other, creating a cycle.

After the main function is executed, the reference count for the second and first variables will equal the first value, although it’s no longer accessible. This results in a memory leak since none of the nodes are deallocated:

A terminal output showing a memory leaks warning in Rust with reference counts of 3 for variables, highlighting a memory leak scenario.

You can fix cases like this by:

  • Using weak references likeWeak<T> for one link direction
  • Manually breaking the cycle before the end of the function

Here’s an example where I address the reference problem with Weak pointers on the prev field:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Weak<RefCell<Node>>>,
}

fn main() {
    let first = Rc::new(RefCell::new(Node {
        value: 1,
        next: None,
        prev: None,
    }));

    let second = Rc::new(RefCell::new(Node {
        value: 2,
        next: Some(Rc::clone(&first)),
        prev: Some(Rc::downgrade(&first)),
    }));

    first.borrow_mut().next = Some(Rc::clone(&second));
    first.borrow_mut().prev = Some(Rc::downgrade(&second));

    println!("Reference count of first: {}", Rc::strong_count(&first)); 
    println!("Reference count of second: {}", Rc::strong_count(&second)); 

    println!("First value: {}", first.borrow().value);
    println!("Second value: {}", second.borrow().value);

    let next_of_first = first.borrow().next.as_ref().map(|r| r.borrow().value);
    println!("Next of first: {}", next_of_first.unwrap());

    let prev_of_second = second.borrow().prev.as_ref().unwrap().upgrade().unwrap();
    println!("Prev of second: {}", prev_of_second.borrow().value);
}
Enter fullscreen mode Exit fullscreen mode

You can use <Weak<RefCell<Node>>> to prevent the memory leak since the Weak reference doesn’t increase the strong reference count, and the nodes can be deallocated.

The std::mem::forget function

You can intentionally use the std::mem::forget function to leak memory in your Rust project when necessary. Rust includes the function for this behavior, so the compiler considers it safe.

Even if the memory isn’t reclaimed, there’ll be no unsafe access or memory issues.

The std::mem::forget takes ownership of a value and forgets it without running the destructor, and since resources held in memory aren’t released, there will be a memory leak:

use std::mem;

fn main() {
    let data = Box::new(42);
    mem::forget(data);
}
Enter fullscreen mode Exit fullscreen mode

At runtime, Rust skips the usual cleanup process, the data variable’s value is not dropped, and the memory allocated for data is leaked after the function is executed.

Leaking memory with unsafe blocks

Using raw pointers gives you the responsibility to manage memory. Here’s how using raw pointers in an unsafe block may lead to a memory leak:

fn main() {
    let x = Box::new(42);
    let raw = Box::into_raw(x); 

    unsafe {
        println!("Memory is now leaked: {}", *raw);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, the memory isn’t freed explicitly, and there will be a memory leak at runtime. After the program’s execution, the memory will be deallocated, so this isn’t the most critical case, but it’s not memory efficient.

Deliberately leaking memory with Box::leak

The Box::leak function allows you to leak memory deliberately. This function is proper when you need to use a value throughout runtime:

fn main() {
    let x = Box::new(String::from("Hello, world!"));
    let leaked_str: &'static str = Box::leak(x);
    println!("Leaked string: {}", leaked_str);
}
Enter fullscreen mode Exit fullscreen mode

Don’t abuse this; leak is helpful if you need a static reference to meet specific API requirements.

Fixing memory leaks in Rust

The golden rule for fixing memory leaks is avoiding them in the first place, except if your use case requires you to. Following the ownership rules is always a great idea. In fact, with the borrow checker, Rust enforces great memory management practices:

  1. Use references when you need to borrow values without transferring the ownership.
  2. You can try out Miri to detect undefined behavior and catch memory-leak-related bugs.
  3. Implement the Drop trait on custom types for cleanups.
  4. Don’t use std::mem::forget unnecessarily. Check out Box<T> for automatic cleanups on heap allocations when a value is off the scope.
  5. Don’t throw unsafe blocks everywhere without reason.
  6. Use Rc<T> or Arc<T> for shared ownership of variables.
  7. Use RefCell<T> or Mutex<T> for interior mutability. They’re helpful if you need to ensure safe concurrent access.

Following these tips and building more Rust programs with lower memory requirements should provide everything you need to handle memory leaks in your Rust programs.

Conclusion

You’ve learned how memory leaks can happen in your Rust programs and how you can simulate them in necessary cases for different purposes, like having a persistent variable in a memory location at runtime.

Understanding the fundamentals of ownership, borrowing, and unsafe Rust can help manage memory and reduce memory leaks.


LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket Rust Demo

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — start monitoring for free.

Top comments (0)