Does this code leak? (static RefCell)

From the cited Wikipedia article (which may be wrong here, of course):

In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.

I would argue that foo implicitly allocates memory by pushing chars and strs to a static String. In the context of the overall program, the "memory which is no longer needed is not released".

Not every static is a leak though, as the memory may be needed. Moreover, the example above leaks memory cumulatively, i.e. if foo is used in a loop, then you might lose megabytes or gigabytes of memory (which is not immediatly visible by an explicit leak or using circular Rcs/Arcs). Not every use of static will exhibit this possibly desastrous behavior as shown in my example.


What else would be a good demonstration for a leak where the memory is still reachable?

My point is: when we speak of memory leaks, we almost always think on leaks where memory becomes unreachable. But there is also the more general case of leaks by logic errors. These can make you lose megabytes or gigabytes of memory as well.


Speaking of "logic errors", this also brings me to another case where leaks can happen in Rust due to due inconsisent implementations of PartialEq/Hash. From the documentation of HashMap for example:

It is a logic error for a key to be modified in such a way that the key’s hash, as determined by the Hash trait, or its equality, as determined by the Eq trait, changes while it is in the map. This is normally only possible through Cell, RefCell, global state, I/O, or unsafe code. The behavior resulting from such a logic error is not specified, but will be encapsulated to the HashMap that observed the logic error and not result in undefined behavior. This could include panics, incorrect results, aborts, memory leaks, and non-termination.

(made the relevant part bold)

That said, I think both cases (growing a Vec or String or any other collection indefinitely and/or providing inconsistent Hash implementations) is a rather rare error to make. But I wanted to list both for a more complete picture, i.e. it is possible in Rust to have memory leaks, even if you don't explicitly leak memory and also when there are no Rcs or Arcs involved.


Yet another example for a memory leak which isn't explicit and doesn't use Rc/Arc either:

async fn foo(a: i32, b: i32) -> i32 {
    let (arg_tx, mut arg_rx) = tokio::sync::mpsc::unbounded_channel::<(i32, i32)>();
    let (res_tx, mut res_rx) = tokio::sync::mpsc::unbounded_channel::<i32>();
    let arg_tx2 = arg_tx.clone();
    tokio::spawn(async move {
        println!("Started task");
        while !arg_tx2.is_closed() { // moving `arg_tx2` to the `Future` leaks memory!
            let Some((a, b)) = arg_rx.recv().await else { break };
            res_tx.send(a + b).expect("channel unexpectedly closed");
        }
        println!("Finished task");
    });
    arg_tx.send((a, b)).expect("channel unexpectedly closed");
    res_rx.recv().await.expect("channel unexpectedly closed")
}

#[tokio::main]
async fn main() {
    for i in 0..10 {
        assert_eq!(foo(5, i).await, 5 + i);
    }
}

(Playground)

Edit: Maybe this example is more than a pure memory leak, as I guess the tokio runtime will also become slower as more and more tasks are spawned, but this is still leaking memory as the spawned Futures will never be freed.

The leak is fixed if you, for example, do a loop:

-       while !arg_tx2.is_closed() { // moving `arg_tx2` to the `Future` leaks memory!
+       loop {

(Playground)

Which is somewhat counterintuitive perhaps.


Another example for a non-explicit memory leak, which neither involves Rc or Arc:

fn main() {
    let (query_tx, query_rx) = std::sync::mpsc::channel::<i32>();
    let (result_tx, result_rx) = std::sync::mpsc::channel::<i32>();
    std::thread::spawn(move ||
        while let Ok(query) = query_rx.recv() {
            result_tx.send(query + 5).unwrap();
            result_tx.send(query + 6).unwrap();
        }
    );
    for i in 0..10 { // imagine an infinite loop here
        query_tx.send(i).unwrap();
        let _result = result_rx.recv().unwrap();
        let _result2 = result_rx.recv().unwrap(); // commenting out this line would result in a leak
        // do something with `_result`
    }
}

(Playground)

If you push two values into result_tx but only read one value from result_rx, and if you repeat this over a long time, you will leak memory. This is similar to my original example of growing a String further and further, except it's an mpsc queue which grows here.

2 Likes