Skip to content

Potential memory leak #913

@abh1nav10

Description

@abh1nav10

We recently added the following changes in #901.

    if ::std::thread::panicking() {
        ::std::process::abort()
    }

It was added in Task::drop_future, the drop implementation for Task and Task::drop. The last one seems to be introducing a subtle condition where we might leak memory which I tried to reproduce in this way.

#[test]
#[should_panic]
fn check_drop() {
    use std::sync::mpsc;
    let (tx, rx) = mpsc::channel();
    let first = std::thread::spawn(move || {
        compio_runtime::Runtime::new().unwrap().block_on(async {
            // This future is never polled for the second time because the timer wheel will notify
            // us about the expiration of 1 second first and we will proceed to panic next!
            let handle = compio_runtime::spawn(async {
                sleep(Duration::from_secs(3)).await;
                10
            });
            tx.send(handle).unwrap();
            sleep(Duration::from_secs(1)).await;
            panic!("top future panics!");
        })
    });
    let second = std::thread::spawn(move || {
        compio_runtime::Runtime::new().unwrap().block_on(async {
            let handle = rx.recv().unwrap();
            compio_runtime::spawn(async {
                // output never arrives because the first runtime panics before the spawned future
                // gets to return the output and in that case it clears the executor which leads to
                // drop being called on the task inside of it. However, here since we had registered
                // our waker and the `Task::drop` does not decrement the refcount as it will
                // encounter the panicking check and return early, when this runtime gets dropped,
                // this task will leak memory because its refcount has not reached zero.
                let _output = handle.await;
            })
            .detach();
            sleep(Duration::from_secs(3)).await;
        })
    });
    second.join().unwrap();
    // joining this will panic!
    first.join().unwrap();
} 

This may leak the underlying memory because when the first runtime is clearing the executor, it is calling Task::drop for the task that we spawned there which immediately returns because the check for whether we are panicking or not returns true. Since we return early, the refcount does not get decremented for the waker that we had registered and that leads to the memory for the task on the second runtime from never being deallocated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions