Valgrind suspects memory leak

Hi, i'm trying to implement my own data structures for deeper understanding of how does rust works "under hood" and get more knowledge about data structures themselves.

I was going through video on yt: https://www.youtube.com/watch?v=3OL95gZgPWA
Checked his explanation on single linked list and really liked it.

So i was following his video on vector and at the end he used valgrind and got 0 leaks after implementing drop trait.
12 allocs and 12 frees for him.
And when i used it on mine i got 10 allocs and only 9 frees. Valgrind output:

==65605== 
==65605== HEAP SUMMARY:
==65605==     in use at exit: 56 bytes in 1 blocks
==65605==   total heap usage: 10 allocs, 9 frees, 3,184 bytes allocated
==65605== 
==65605== 56 bytes in 1 blocks are possibly lost in loss record 1 of 1
==65605==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==65605==    by 0x12A600: std::rt::lang_start_internal (in /home/lf/personal/data_structs/rust/target/release/rust)
==65605==    by 0x110064: main (in /home/lf/personal/data_structs/rust/target/release/rust)
==65605== 
==65605== LEAK SUMMARY:
==65605==    definitely lost: 0 bytes in 0 blocks
==65605==    indirectly lost: 0 bytes in 0 blocks
==65605==      possibly lost: 56 bytes in 1 blocks
==65605==    still reachable: 0 bytes in 0 blocks
==65605==         suppressed: 0 bytes in 0 blocks
==65605== 
==65605== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Here is my vector implementation:

#[derive(Debug, PartialEq)]
pub struct Vec<T> {
    ptr: NonNull<T>,
    len: usize,
    cap: usize,
}

impl<T> Vec<T> {
    pub fn new() -> Self {
        Self {
            ptr: NonNull::dangling(),
            len: 0,
            cap: 0,
        }
    }

    pub fn push(&mut self, val: T) {
        if size_of::<T>() == 0 {
            panic!("No zero sized types");
        }

        if self.cap == 0 {
            let layout = alloc::Layout::array::<T>(4).expect("Could not allocate");
            // SAFETY: the layout is hardcoded to be 4 * size_of<T>
            // size_of<T> is > 0
            let ptr = unsafe { 
                alloc::alloc(layout) 
            } as *mut T;
            let ptr = NonNull::new(ptr).expect("Could not allocate memory");

            // SAFETY: pointer is NonNull
            // and we have just allocated enough sapce for this val (and 3 more).
            // The memory previously at ptr is not read.
            unsafe {
                ptr.as_ptr().write(val);
            }
            self.ptr = ptr;
            self.cap = 4;
            self.len = 1;
        } else if self.len < self.cap {
            let offset = (self.len() + 1)
                .checked_mul(size_of::<T>())
                .expect("Cannot reach memory location");
            assert!(offset < isize::MAX as usize, "Wrapped isize");


            // SAFETY: offset cannot wrap around
            // And pointer is pointing to valid memory
            // Writing to a offset at self.len is valid
            unsafe {
                self.ptr.as_ptr().add(self.len).write(val);
                self.len += 1;
            }
        } else {
            debug_assert!(self.len == self.cap);
            let new_cap = self.cap.checked_mul(2).expect("Capacity wrapped");
            let align = std::mem::align_of::<T>();
            let size = size_of::<T>() * self.cap;

            size.checked_add(size % align).expect("Cannot allocate");

            let ptr = unsafe {
                let layout = alloc::Layout::from_size_align_unchecked(size, align);
                let new_size = size_of::<T>() * new_cap;
                let ptr = alloc::realloc(self.ptr.as_ptr() as *mut u8, layout, new_size);
                let ptr = NonNull::new(ptr as *mut T).expect("Cannot reallocate");


                // let offset = (self.len() + 1)
                //     .checked_mul(size_of::<T>())
                //     .expect("Cannot reach memory location");
                // assert!(offset < isize::MAX as usize, "Wrapped isize");

                self.ptr.as_ptr().add(self.len).write(val);

                ptr
            };

            self.ptr = ptr;
            self.len += 1;
            self.cap = new_cap;
        }
    }

    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }

        Some(unsafe {
            &*self.ptr.as_ptr().add(index)
        })
    }

    pub fn len(&self) -> usize {
        return self.len
    }

    pub fn cap(&self) -> usize {
        return self.cap
    }
}

And drop implementation:

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            std::ptr::drop_in_place(
                std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len)
            );

            let layout = Layout::from_size_align_unchecked(
                std::mem::size_of::<T>() * self.cap, 
                std::mem::align_of::<T>()
            );

            alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
        }
    }
}

I found that it could be not about my implementation but rust runtime, but i'm not sure about if it's true at all.

And so to confirm i didn't make any mistakes i'm here. Of course i did rewatch video to be confident that i didn't make mistakes in syntax but anyways

This was asked a while ago:

That's interesting, but for now i'd like to know why it doesn't free the same amount of times as it allocates.

Considering runtime empty main fn will still allocate and free memory. So in linked example it was 9 to 9. But when im creating my vector it doesn't get free same amount of times as it allocates. Also same happens with std vector

I don't know the answer. But you should show the rest of the code for completeness, including main, in case there is something suspicious there.

I couldn't replicate the leak on MIRI, but I did find some UB :smile:, you accidentally used self.ptr where you should have used the newly generated ptr.

(to run Miri in Playground, go to Tools in the top right corner then run Miri)

I think the leak is in main, and not in any code you have shared so far.

1 Like

I ran the code provided in Valgrind and it shows there are no leaks. My main looks like this:

fn main() {
    let mut v = Vec::new();
    v.push(13);
    println!("{:?}, {}, {}", v.get(0), v.len(), v.cap());
}

And valgrind output:

$ rustc --version
rustc 1.85.0-nightly (33c245b9e 2024-12-10)

$ cargo build
   Compiling vecleak v0.1.0 (/home/jay/other-projects/vecleak)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s

$ valgrind --leak-check=full ./target/debug/vecleak
==495== Memcheck, a memory error detector
==495== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==495== Using Valgrind-3.23.0 and LibVEX; rerun with -h for copyright info
==495== Command: ./target/debug/vecleak
==495==
Some(13), 1, 4
==495==
==495== HEAP SUMMARY:
==495==     in use at exit: 0 bytes in 0 blocks
==495==   total heap usage: 9 allocs, 9 frees, 3,112 bytes allocated
==495==
==495== All heap blocks were freed -- no leaks are possible
==495==
==495== For lists of detected and suppressed errors, rerun with: -s
==495== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

I agree with @RustyYato, there is no leak in the code you have presented.

use std::ptr::NonNull;
use std::alloc::{self, Layout};

#[derive(Debug, PartialEq)]
pub struct Vec<T> {
    ptr: NonNull<T>,
    len: usize,
    cap: usize,
}

impl<T> Vec<T> {
    pub fn new() -> Self {
        Self {
            ptr: NonNull::dangling(),
            len: 0,
            cap: 0,
        }
    }

    pub fn push(&mut self, val: T) {
        if size_of::<T>() == 0 {
            panic!("No zero sized types");
        }

        if self.cap == 0 {
            let layout = alloc::Layout::array::<T>(4).expect("Could not allocate");
            // SAFETY: the layout is hardcoded to be 4 * size_of<T>
            // size_of<T> is > 0
            let ptr = unsafe { 
                alloc::alloc(layout) 
            } as *mut T;
            let ptr = NonNull::new(ptr).expect("Could not allocate memory");

            // SAFETY: pointer is NonNull
            // and we have just allocated enough sapce for this val (and 3 more).
            // The memory previously at ptr is not read.
            unsafe {
                ptr.as_ptr().write(val);
            }
            self.ptr = ptr;
            self.cap = 4;
            self.len = 1;
        } else if self.len < self.cap {
            let offset = (self.len() + 1)
                .checked_mul(size_of::<T>())
                .expect("Cannot reach memory location");
            assert!(offset < isize::MAX as usize, "Wrapped isize");


            // SAFETY: offset cannot wrap around
            // And pointer is pointing to valid memory
            // Writing to a offset at self.len is valid
            unsafe {
                self.ptr.as_ptr().add(self.len).write(val);
                self.len += 1;
            }
        } else {
            debug_assert!(self.len == self.cap);
            let new_cap = self.cap.checked_mul(2).expect("Capacity wrapped");
            let align = std::mem::align_of::<T>();
            let size = size_of::<T>() * self.cap;

            size.checked_add(size % align).expect("Cannot allocate");

            let ptr = unsafe {
                let layout = alloc::Layout::from_size_align_unchecked(size, align);
                let new_size = size_of::<T>() * new_cap;
                let ptr = alloc::realloc(self.ptr.as_ptr() as *mut u8, layout, new_size);
                let ptr = NonNull::new(ptr as *mut T).expect("Cannot reallocate");


                // let offset = (self.len() + 1)
                //     .checked_mul(size_of::<T>())
                //     .expect("Cannot reach memory location");
                // assert!(offset < isize::MAX as usize, "Wrapped isize");

                ptr.as_ptr().add(self.len).write(val);

                ptr
            };

            self.ptr = ptr;
            self.len += 1;
            self.cap = new_cap;
        }
    }

    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }

        Some(unsafe {
            &*self.ptr.as_ptr().add(index)
        })
    }

    pub fn len(&self) -> usize {
        return self.len
    }

    pub fn cap(&self) -> usize {
        return self.cap
    }
}

impl<T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            std::ptr::drop_in_place(
                std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len)
            );

            let layout = Layout::from_size_align_unchecked(
                std::mem::size_of::<T>() * self.cap, 
                std::mem::align_of::<T>()
            );

            alloc::dealloc(self.ptr.as_ptr() as *mut u8, layout);
        }
    }
}

Full vec implementation

And main.rs

use rust::vector::Vec;

// use std::vec::Vec;

fn main() {
    let mut v = Vec::new();
    v.push(13);
    println!("{:?}, {}, {}", v.get(0), v.len(), v.cap());
}

I'm still facing same results with valgrind:

==3413== Memcheck, a memory error detector
==3413== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==3413== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==3413== Command: ./target/release/rust
==3413== 
Some(13), 1, 4
==3413== 
==3413== HEAP SUMMARY:
==3413==     in use at exit: 56 bytes in 1 blocks
==3413==   total heap usage: 10 allocs, 9 frees, 3,168 bytes allocated
==3413== 
==3413== 56 bytes in 1 blocks are possibly lost in loss record 1 of 1
==3413==    at 0x48447A8: malloc (vg_replace_malloc.c:446)
==3413==    by 0x12A130: std::rt::lang_start_internal (in /home/lf/personal/data_structs/rust/target/release/rust)
==3413==    by 0x10FB94: main (in /home/lf/personal/data_structs/rust/target/release/rust)
==3413== 
==3413== LEAK SUMMARY:
==3413==    definitely lost: 0 bytes in 0 blocks
==3413==    indirectly lost: 0 bytes in 0 blocks
==3413==      possibly lost: 56 bytes in 1 blocks
==3413==    still reachable: 0 bytes in 0 blocks
==3413==         suppressed: 0 bytes in 0 blocks
==3413== 
==3413== For lists of detected and suppressed errors, rerun with: -s
==3413== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Now im wondering what's the problem as i copied working code with no leaks but in my pc i do have potential leaks

Also fixed a bug with self.ptr to ptr. It was causing all elements after 4th to be 0

What is your rustc --version? Maybe I can reproduce with the exact compiler version you are using?

rustc 1.83.0 (90b35a623 2024-11-26)