Why is it possible to access a dropped variable in rust?

Hi all,

I tried to run the following rust code, and got the corresponding output.

fn main() {
    let mut a: u32 = 0;
    let mut ptr: *mut u32 = &mut a;
    let mut b: u32 = 4;
    
    unsafe { (*ptr) = 1 };
    drop(ptr);
    drop(b);
    
    unsafe { println!("Print the value: {:?}, {:?}", *ptr, b) };
}

Output:

Print the value: 1, 4

Why is it still possible to access the ptr and b variables even after we have called the drop function for both of them. Isn't this should be caught at compile time?

This is the source code for drop:

pub fn drop<T>(_x: T) {}

It has no effect for Copy types.

6 Likes

hmm ok. So, isn't this against the memory safety rule of rust? If a programmer mistakenly accesses, a memory that is dropped, he still will be able to access the value and modify it.

Isn't this the issue that we are trying to solve using rust?

drop(b) passes b by value to the function. The lifetime of the copy ends, but this has no effect on the original.

If b wasn't a Copy type, then drop(b) would move b to the function. The moved value's lifetime would end there and b would no longer be usable.

13 Likes

Raw pointers are pointers without safety or liveness guarantees.

Working with raw pointers in Rust is uncommon, typically limited to a few patterns. Raw pointers can be unaligned or null.
src: pointer - Rust

So it's programmers' (not the compiler's) duty to uphold safety rules when they decide to use raw pointers.

3 Likes

Ohk. But suppose, b was passed to some other function instead of drop, in that case the value would have been moved to that particular function right? So, why is that, the value is not moved in the case of calling drop.

I understand the technical reason for this behaviour, but what I am interested in knowing why the compiler is designed in this way, that it can't catch the issue of a drop variable being accessed. Is there any specific reason for this?

I can understand the case of raw pointers, but why is the behaviour same for even a normal integer variable.

Types are either Copy or not. u32 is Copy. A u32 never gets destroyed when you pass it by value to another function. String is an example of a non-Copy type. If you pass it by value, then you lose the original and the called function gets the moved value.

11 Likes

Something to keep in mind: local variables aren't allocated on the heap, but they may internally point to heap memory. A u32 doesn't point to heap memory, but String does.

2 Likes

Because you happened to pass a type that is Copy. It has been copied to drop() and the original still remains completely valid. There is no memory unsafety.

7 Likes

And the compiler even warn about it:

warning: calls to std::mem::drop with a value that implements Copy does nothing

fn main() {
    let b: u32 = 4;
    
    drop(b);
    
    println!("Print the value: {:?}",  b);
}
8 Likes

I think the drop function essentially cleans up memory, but the memory allocated on the stack cannot be dynamically adjusted, so it is invalid for copy-type drops.

No. The drop function isn't magic. It literally just moves the passed value into its body.

There's nothing "invalid" here. Stack allocations are known at compile time and the compiler can track statically how much memory is needed for any given function. This includes knowledge about passing arguments to other functions called from the function being analyzed. drop isn't special in any way. If you call drop in your function, the compiler will know that it needs to have enough space on the stack and in registers to pass the value. The allocation of the stack frame of any function will be cleaned up in one go when the function returns.

9 Likes

Let me highlight a possible source of confusion.

When you pass something by value, or assign it to a new variable binding, you either move it or you copy it. If the type implements Copy, you copy it. Otherwise, you move it. Integers, raw pointers, and shared references implement Copy.

Once you move out of some place, like a variable, it can't be used any more; it's uninitialized. But after you copy out of a place, you can still use the original place; it is unchanged and not uninitialized.

std::mem::drop -- the function you can call and could write yourself -- is called drop because if T is not Copy, you move the function argument when you make the call, and it gets destructed when it immediately goes out of scope at the end of that function.

b and ptr never get moved when you called std::mem::drop, because they're Copy. (The copy you pass then goes out of scope, but that has no effect on the originals.)


That's all that's required to understand this thread so far, but let's go a little further.

So in some sense, b and ptr and other Copy values can't "be dropped" at all. However, they can go out of scope, which has a similar effect for borrow checking.[1] (Borrow check errors call going out of scope "being dropped" even if the type is Copy, for example.)

And the Drop trait is somewhat of a red-herring; a Drop bound is pretty much useless, for example, as a type can have a non-trivial destructor even if it doesn't implement Drop.


  1. So does being overwritten, for that matter. ↩ī¸Ž

17 Likes

What I mean is that he called drop(A) in this function. In fact, this function call has not exited yet. The variable A is allocated on the stack. Because this call stack has not ended, this function cannot be released during the function call. The stack of secondary functions, so it is available before the function call returns,

I am using a translation, and the description may not be clear. In short, it means that before the call of function A ends, the local variables allocated in function A will not be cleaned up, and it is impossible for Rust to discard functions that implement the copy feature.

I'm sorry but that description makes zero sense to me. The meaning probably got lost in the translation.

1 Like

For example, you run a function A, but the local variables allocated on the stack will not be cleared before the call to function A ends.

Non-Copy values on the stack can still be unitialized. And types that go out of scope may have their stack memory re-used. And stack memory may also be reused if the compiler can prove the memory is not otherwise needed beyond a certain point in the function. And local variables can be optimized to be in registers, instead of on the stack; those registers can similarly be reused. And alloca exists.

Which is to say, exactly how the stack is used is largely an implementation detail with few guarantees.

The ownership and borrowing model doesn't directly care about the stack. Instead, how the stack is used is to some extent constrained by the ownership and borrowing model.

2 Likes