Just want to know is there any way to check a mem pointed by a pointer has been dropped

Hi

When I was practicing unsafe code of rust, I realize some pointer may point to structs that have been dropped, and the pointer is not null( ptr.is_null() returns false ), just curious is there any way to check if a mem has been freed? I just want to know and may not use it in any of the logic.

fn deref_box_pointer_test(nn_node_next: NonNull<Node<u32>>) {
    //get boxed here, and will drop when moving out context.
    let mut boxed = unsafe { Box::from_raw(nn_node_next.as_ptr()) };

    boxed.element += 1;

    println!("{}", boxed.element);
}

let nn_node_next = Box::into_raw_non_null(Box::new(Node::new(1u32, None)));

//read and drop node in this function
deref_box_pointer_test(nn_node_next);  

//element inside node may still available
println!("{}", unsafe { (*nn_node_next.as_ptr()).element });  

println!("{}", nn_node_next.as_ptr().is_null()); 
//the pointer is_null is still false

let boxed_node = unsafe { Box::from_raw(nn_node_next.as_ptr()) };

println!("freed {}", boxed_node.element);

// will try to drop the box again and there will be an error when moving out of the context.
// pointer being freed was not allocated

thank you very much!

Nope! There is no way at all.

The fact that you could still access the value after it being dropped is a coincidence, and this is undefined behavior.

Freeing it again is also undefined behavior.

3 Likes

There isn't a general way to know just from a pointer, no.

The allocator, the thing which manages handing out memory, may know this. But it's not going to be easy to get access to that information, and it could be unreliable.

In Rust, like in C or C++ or most other languages without built-in garbage collection, the allocator at its heart has a fairly simple interface. To allocate, you give it a size and an alignment, and it gives back a pointer. To deallocate, you give it that same size and alignment and the pointer it gave you.

There's more functionality (see the Alloc trait), but at its heart this is what's required and expected. It won't allocate overlapping pieces of memory, but if one pieces of memory is freed, it could definitely use it for something else.

You're even free to de-allocate things twice, but as @alice pointer out, this is undefined behavior.

The allocator operates under the assumption that the program knows what its allocated, and will not double-free things nor use freed memory. This allows it to be very fast, and also allows you to break things when you use unsafe code.


Related note - if you want a short book or long tutorial on using unsafe code in rust, and you haven't already heard of it, Learn Rust With Entirely Too Many Linked Lists is a great read.

3 Likes

Even if you could check if there are an allocation at a pointer, you don't know if it's the same, or if it's a new allocation that just happens to be at the same place. Even if they have the same size, they may still be different types, and if you think it contains an u8, but the new allocation is a NonZeroU8 you will have bad stuff if you try to put a zero there.

4 Likes

Thanks for your very detailed answer, and the suggestion of the book. I'm also learning the unsafe code by reading the std collection double linked list and trying to implement a single linked list by myself. This kind of practice really make people learn deep into Rust.

This's really enlightening. No one knows what will happen after a mem's freed, so any check on that would not be reliable. Thanks for your inspiration.

For debugging / testing purposes, you can:

  • add Drop impls:

    impl<T> Drop for Node<T> {
        fn drop (self: &'_ mut Self)
        {
            eprintln!("Dropping Node at {:p}", self);
        }
    }
    
    Dropping Node at 0x55ac5322cac0
    Dropping Node at 0x55ac5322caa0
    Dropping Node at 0x55ac5322ca80
    Dropping Node at 0x55ac5322ca60
    Dropping Node at 0x55ac5322ca40
    
  • adornate Rust's global (heap-)allocator:

    Allocated 16 bytes at 0x55c641591950
    Allocated 16 bytes at 0x55c641591b50
    Allocated 16 bytes at 0x55c641591b70
    Allocated 16 bytes at 0x55c641591b90
    Allocated 16 bytes at 0x55c641591bb0
    Freed 16 bytes at 0x55c641591950
    Freed 16 bytes at 0x55c641591b50
    Freed 16 bytes at 0x55c641591b70
    Freed 16 bytes at 0x55c641591b90
    Freed 16 bytes at 0x55c641591bb0
    
2 Likes

Side note: if you actively want the "does this pointer point to an object that is still alive?" semantics in safe code, consider using Rc and especially Weak.

2 Likes

What's interesting about this question is that it's basically the whole point of the borrow checker. But I imagine you're using FFI or something?

I have a potential solution, but it might be overly-complex or have some other flaws.

Basically, wherever this pointer is first received by your program, you wrap it in this type which guarantees that it can only be consumed once.

struct PointerToken {}

impl PointerToken {
    fn new() -> Self {
        Self {}
    }
}

// this implementation is merely here to prevent `Copy` from being implemented on `PointerToken`
impl Drop for PointerToken {
    fn drop(&mut self) {}
}

// consumes the type, which cannot be copied, therefore this function can only be called once
fn validate_pointer_token(token: PointerToken) {}

pub struct Pointy {
    nn_node_next: NonNull<Node<u32>>,
    validation_token: PointerToken,
}

impl Pointy {
    pub fn new(nn_node_next: NonNull<Node<u32>>) -> Self {
        Self {
            nn_node_next,
            validation_token: PointerToken::new(),
        }
    }

    pub fn as_ptr(self) -> Box<Node<u32>> {
        validate_pointer_token(self.validation_token);
        unsafe { Box::from_raw(self.nn_node_next.as_ptr()) }
    }
}

fn main() {
    let mut node = Node { t: 0 };
    let internal = NonNull::new(&mut node).unwrap();

    let p = Pointy::new(internal);
    let y = p.as_ptr();
    let z = p.as_ptr();
}

Note that this code should fail to compile. It's the entire point of it. If you remove let z = p.as_ptr();, it will compile.

3 Likes

Just to reiterate @daboross' comment, I would highly recommend reading Learn Rust With Entirely Too Many Linked Lists!

It's a really high quality document and takes you through building up a solid linked list implementation, starting with a safe (naive and slow) singly linked list, all the way through to building a double-ended queue.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.