A new memory leak example with channels

It was just pointed out to me that there is another way to cause memory leaks in Rust that I was not aware of yet -- without direct use of reference counting or mem::forget or so:

use std::sync::mpsc::{Receiver, channel};

struct R(Receiver<Box<R>>);

fn main() {
    let (s, r) = channel::<Box<R>>();
    s.send(Box::new(R(r))).unwrap();
    drop(s);
}

Miri reports:

The following memory was leaked: alloc1612 (Rust heap, size: 48, align: 8) {
    0x00 β”‚ 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 β”‚ ................
    0x10 β”‚ 02 00 00 00 00 00 00 00 β•Ύ0x291a0[a1708]<3318>─╼ β”‚ ........╾──────╼
    0x20 β”‚ 05 00 00 00 00 00 00 00 __ __ __ __ __ __ __ __ β”‚ ........β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘
}
alloc1708 (Rust heap, size: 16, align: 8) {
    00 00 00 00 00 00 00 00 β•Ύ0x28920[a1612]<untagged> (8 ptr bytes)β•Ό β”‚ ........╾──────╼
}

Basically, we can define a channel with a fancy recursive type such that we can send the other end of the channel over itself. That way we never drop r.

(mpsc internally uses Arc, but this example would work even with an SPSC channel -- the leak is unrelated to the reference counting.)

Rust doesn't guarantee absence of leaks, so there is no bug here, but it's still a cute new memory leak gadget. I guess we are lucky that Rust 1.0 had Rc so there was a simpler gadget and we knew to give up on the memory leak freedom guarantee. :wink:

Thanks to Jules Jacobs for the example!

32 Likes

(NOT A CONTRIBUTION)

It's interesting to me that both this and the classic Rc/Arc examples are constructs for "sharing;" in one case by direct shared ownership and the other case by passing ownership through a queue. I wonder what sort of deeper connection there might be between memory leak freedom and sharing and the specific weaknesses of Rust's type system to analyse these cases.

I believe this means that if there were Leak bounds, there would be no way to share non-Leak types across thread boundaries. Is this right?

Hm, I think this still is reference counting? An spsc would essentially include a "manual" impl of a two-bit reference count I would think.

1 Like

With an spsc the receiver owns the item that was sent, no reference counting at all that I can see.

Edit: I see. You mean that both the sender and receiver have access to a rendezvous memory location, right?

I think that you could share them between threads as long as the sending thread blocks until the object is received.

Right! The logic I think is that the last of sender/receiver who is dropped is responsible for dropping the shared buffer.

If there’s no shared buffer (sync channel which moves data directly between stacks), then the code above is a deadlock, not a memleak

Interestingly, the Linux kernel has a simple garbage collector (in net/unix/garbage.c) to handle the analogous case of file descriptors sent over UNIX sockets.

14 Likes

Sharing in general is fine, but sharing can easily lead to cyclic structures and cycles are a problem. In this case it's the internal buffer of that channel pointing to itself. With a synchronous channel (without a buffer) this could not happen.

Hm... you may be right. That's now how I thought of the protocol but I guess it is an equivalent view.

2 Likes