Does this code leak? (static RefCell)

Does the following leak? If so, a) why and b) how can it be fixed? (Use Rc?)

 thread_local! {
        static RNG : RefCell<Xoshiro256PlusPlus> = {
            RefCell::new(Xoshiro256PlusPlus::from_entropy())
        }
    }

    pub fn seed_rng(seed: u64) {
        RNG.with(|x| {
            *x.borrow_mut() = Xoshiro256PlusPlus::seed_from_u64(seed);
        });
    }
1 Like

This code leaks in the same way that any global leaks (which aren't traditionally considered leaks).

The data which holds your lazy static is part of the static section of your program at runtime. It's the same flavour of leak as saying that having a stack frame for main is a leak :slight_smile:.

A static item is always kept alive for the entire duration of the program. At this point it depends on your definition of "leak" whether it leaks. In practice, it's not wise to consider it leaking – if you need a static, then there's no option other than keeping it alive until the end of the program.

Ah, yes :slightly_smiling_face:

I should have been more precise about what I mean by leaking. If I were to call seed_rng repeatedly in main, would each call leave behind an unreachable copy of Xoshiro256PlusPlus sitting in memory? Or does the newly allocate object overwrite the previously allocated memory? Or am I asking the wrong question (which seems a possibility from the replies)?

If Xoshiro256PlusPlus manages an allocation, it will be dropped when the new value is assigned to the static[1]. If it doesn't manage an allocation then there's no memory to leak in the first place.


  1. assuming Xoshiro256PlusPlus doesn't have a bug or memory leak already, obviously ↩︎

You are. A place is a place, regardless of whether it's inside a static item, a local variable, or anywhere else. The same memory management rules are applied to all places. If an initialized place is overwritten with a new value, then the old value is dropped.

This is like asking whether the following code leaks the vector containing 1, 2, 3:

let mut v = vec![1, 2 3];
v = vec![4, 5, 6];

Of course, it doesn't leak.

Thanks. I guess that was the real question.

Seem this pattern is ok. Certainly even if it left behind some un-reuseable memory, seed_rng would typically only be called once, so its probably not very important in any event.

The def is

pub struct Xoshiro256PlusPlus {
    s: [u64; 4],
}

Got it. Thanks.

Must be ghost-of-C-past wondering whether resources get dropped or not. :slightly_smiling_face:

In general, you can count on Rust doing the right thing by default. The language is designed around being hard to abuse, and to behave "as expected" intuitively.

The only accidental way of leaking memory I know of is creating cycles of Rcs or Arcs; anything else that leaks is explicit and can't be done by mistake (e.g. mem::forget() or ManuallyDrop::new() or Box::into_raw() etc.).

1 Like

That's refreshing. In some other world, RefCell might have contained a non-smart pointer and thus not done the right thing, which is what I was trying to be sure I fully understand.

Indeed. However, RefCell does not contain any pointers – it directly wraps the protected value. If seeing implementation details helps your understanding, you can check out the definition of RefCell.

The other answers are valid for normal statics, but thread_local statics are different: they are dropped when the related thread stops.

Moreover in this particular case there's nothing to drop (AFAIK Xoshiro256PlusPlus internally should just be [u64; 4]), so there are no leaks anyway

1 Like

I would generally agree. However, one other case where memory could leak is if you fill data structures and never remove the contents (i.e. only grow the structure). An example could look like this:

use std::cell::RefCell;

thread_local! {
    static BUF: RefCell<String> = RefCell::new(String::new());
}

fn quote(s: &str) -> String {
    BUF.with(|buf| {
        let mut buf = buf.borrow_mut();
        let start = buf.len();
        buf.push('"');
        buf.push_str(s);
        buf.push('"');
        eprintln!("buffer len = {}", buf.len());
        buf[start..].to_owned()
    })
}

fn main() {
    for _ in 0..10 {
        assert_eq!(quote("Hello"), "\"Hello\"");
    }
}

(Playground)

This example neither uses Rc or Arc nor mem::forget or ManuallyDrop, etc., but the foo function does leak memory.

Whether the leak is explicit or not is arguable, I guess.

I understand this example is a bit awkward, but I'm sure there are other scenarios where you accidentally grow structures more and more, which effectively results in a leak, so I wanted to mention this as another sceneraio where memory leaks might happen.


The example I gave is different from leaks through Rc/Arc and mem::forget insofar that the memory is, in theory, still reachable (at least from within the module). However, as I understand the term "memory leak" right, not all leaks require memory to be unreachable (Wikipedia).

I would strongly debate your definition of "leak" here. If the contents of the allocated buffer are needed, then the only correct thing to do is to keep it alive forever. By this definition, basically every static would "leak", which is not a useful notion, to say the least.

1 Like

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

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.