Dropping uninitialized structs

I am writing a windows program that uses only ntdll.dll in its import, that is why I need custom memory manager. I know that there is a GlobalAlloc trait for implementing custom allocator, but as I said, my program must have imports from ntdll.dll only, that is why I am writing all memory operations myself.

In particular, I am implementing reference counter, that frees memory when counter = 0. Here is code:

use core::ops::Drop;

use super::{alloc_mem, free_mem, ALLOC_MEM_ERROR};

pub struct TheBox<T: Clone>
{
    data: *mut T,
    rc: *mut u64,
}

impl<T: Clone> TheBox<T>
{
    pub fn new(v: T) -> Result<TheBox<T>, u32>
    {
        unsafe
        {
            let mem: usize = alloc_mem(core::mem::size_of::<T>() + core::mem::size_of::<u64>());
            if mem == 0
            {
                return Err(ALLOC_MEM_ERROR);
            }

            let d = mem + core::mem::size_of::<u64>();
            let rc_mem = mem;

            let rc: *mut u64 = rc_mem as *mut u64;

            *rc = 1;

            let data: *mut T = d as *mut T;
            *data = v;

            Ok(TheBox
            {
                data: data,
                rc: rc,
            })
        }
    }

    pub fn get_val(&self) -> &T
    {
        unsafe
        {
            &(*self.data)
        }
    }

    pub fn get_val_mut(&mut self) -> &mut T
    {
        unsafe
        {
            &mut (*self.data)
        }
    }

    pub fn unbox(&self) -> T
    {
        unsafe
        {
            (*(self.data)).clone()
        }
    }
}

impl<T: Clone> Clone for TheBox<T>
{
    fn clone(&self) -> TheBox<T>
    {
        unsafe
        {
            asm!("lock inc qword ptr [rax]" : : "{rax}"(self.rc) : : "intel");
        }

        TheBox
        {
            data: self.data,
            rc: self.rc,
        }
    }
}

impl<T: Clone> Drop for TheBox<T>
{
    fn drop(&mut self)
    {
        unsafe
        {
            let mut do_free: u8;
            asm!("lock dec qword ptr [rax]" : : "{rax}"(self.rc) : : "intel");
            asm!("sete cl" : "={cl}"(do_free) : : : "intel");

            if do_free == 1
            {
                free_mem(self.rc as usize);
            }
        }
    }
}

Here alloc_mem and free_mem calls RtlAllocateHeap and RtlFreeHeap.

When I initialize memory with zeros in alloc_mem everything is like normal, but when I do not initialize memory with zeros, it crashes in drop method on instruction with lock prefix because self.rc is invalid.

What I am doing wrong?

Any use of uninitialized memory is UB.

*data = v;

This will attempt to drop *data, which is unininitialized, so it will cause UB. Instead do data.write(v); Similarly with *rc = 1;, you should use rc.write(1);


get_val_mut is unsound because having a unique reference to the reference counted box does not guarantee that you have unique access to data.

i.e. I could do

let mut other: TheBox<_> = my_box.clone();

let my_box_mut = my_box.get_val_mut();
let other_mut = other.get_val_mut(); // oh no, aliasing unique references, insta-UB using only the safe api you made!

Take a look at Rc<T>'s source to get an idea of how std does this.

Here is a sketch based on std.

Thank you for answering. Really, I need this structure for creating bidirectional list, that is why I want a mutable access to content of TheBox. Is it possible to create bidirectional list with this structure, or it will be better to use another method?

You can provide an api like Rc then compose it with any of the interior mutability types like Cell or Mutex to get MyBox<Cell<T>> and regain mutation.

Maybe you can get away with statically linking jemalloc? I'm not sure if it needs any dlls.

If you're trying to implement a list, Learn Rust With Entirely Too Many Linked Lists should be helpful.

1 Like