Why does reading a raw pointer cause a drop?

See the following:

#![allow(unused_variables)]
use std::ptr;

// INTENTIONALLY not derive(Copy), even without a Drop impl
struct Foo {
    bar: u32
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("dropped");
    }
}

fn main() {
    let n = Foo { bar: 123 };
    let ptr = &n as *const Foo;
    
    let n2 = (unsafe { ptr::read(ptr) }).bar;
}

(Playground)

Output:

dropped
dropped

As you can see, there's a double drop because of n2. I know that ptr::read returns a shallow copy of Foo, but is there any way to read a pointer to a non-Copy struct without dropping it? Of course, I can't use simple *ptr because that only works for Copy structs.

I know I could wrap the result in ManuallyDrop to stop that, but then to do anything with the pointer, I would need to go through a reference (via ManuallyDrop's Deref impl), and I explicitly do not want to handle references because my program will have aliasing in a single function, and, correct me if I'm wrong, but Rust does allow aliasing mutable raw pointers.

But you don't want to read the whole struct, you want to read a field:

let n2 = unsafe { (*ptr).bar };

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=c6a3880aa090c4c5f2b2b8b560d68f28

2 Likes

Using ptr::read() on a !Copy type will take the Foo by value, leaving the place your pointer points to logically uninitialized. You would need to make sure the old location never gets read again (which includes being dropped), so you should wrap it in a ManuallyDrop.

You probably just want a reference though, in which case you can use pointer casting (e.g. ptr as &Foo &*ptr) to get a reference to the Foo, or read/write one of its fields using something like std::ptr::addr_of!(ptr.bar).read().

Edit: Not sure why I thought you use as for turning a pointer into a reference – I blame the lack of coffee :man_facepalming: Thanks @steffahn for picking this up!

Yes, this is correct. You may alias raw pointers as long as you ensure that you follow the borrow checker's rules any time those pointers are converted to & or &mut references, even temporarily.

The semantics around unsafe are quite subtle and I would be curious to hear more about why this function forces you to use aliased mutation. There's a good chance there is a better way.

3 Likes

That’s not how you turn a pointer into a reference. Instead, you’ll need to do &*ptr (of course in an unsafe context).

of course in case the field wouldn’t be Copy, you can risk double-frees the same way.

I want to second this. @bread-dreams, your example code does not explain any context of why unsafe code is used in the first place; it’s possible to do a lot in Rust without any need for unsafe code, on the other hand it’s incredibly easy to mess up and either run into UB or at least create unsound APIs when you do use it, even for super experienced Rust users. So it’s a good advice to avoid unsafe if possible.

In particular, you have a code example involving a pointer derived from an immutable reference, yet you talk about “aliasing mutable raw pointers” below – I hope you’re aware that you must not mutate a struct through a pointer derived from a shared/immutable (“&T”) reference.

2 Likes

@scottmcm Thanks. That hadn't worked before for some reason but it does now… weird. I think I was just screwing something up inadvertently, woops :sweat_smile:

@Michael-F-Bryan @steffahn Yeah, so, I don't want to use references because, as I've discussed before here, I'm implementing the GC for a dynamic programming language that allows mutable aliasing. So I've just given up on using references for objects and am using pointers everywhere—there's no getting around it, I'm gonna need mutable aliasing, and Rc/RefCell/integer indexes/whatever will not work for my use case.

And yes @steffahn I am very aware of the "no mutation through pointers derived from shared references" rule; in my actual code everything the GC manages is on the heap, allocated manually, and only raw pointers are used with no references either shared or mutable to the allocations. I only did this in the example to make it simpler, although I should've just done Box::into_raw(Box::new(n)) probably.

I also do hear you about avoiding unsafe, but how am I going to learn how to use unsafe without using unsafe? :slight_smile: This is just a toy program anyway, so might as well throw myself directly into the spike abyss because it's fun!

1 Like

I’ve only realized that you’re the person from that other discussion after I’d already edited my answer above :wink:

As to creating references: Creating (very) short-lived references to the target of a pointer, or one of its fields, etc, is not actually dangerous w.r.t aliasing guarantees. If you have a ptr: *mut SomeType, then doing e.g.

*ptr = foo();

is equivalent to doing

let reference: &mut SomeType = &mut *ptr;
*reference = foo();

or

let value = foo();
let old = ptr::read(ptr);
ptr::write(ptr, value);
drop(old);

The creation of a short-lived mutable reference derived from a pointer does not stand in contradiction with the existence of other copies of that pointer, it just stands in contradiction with the simultaneous creation and use of other shared or mutable references derived from (a copy of) the same pointer. Or in general any simultaneous use of the pointer, e.g. another ptr::read must also not interleave with the time-span that that short-lived mutable reference is used/alive. But doing ptr::write, too, does not support the simultaneous existence and use of other references. It also doesn’t support “simultaneous” ptr::read or other operations, yet in a single-threaded context it’s not actually possible to have those be simultaneous; and in a multi-threaded context this setting of simultaneous – i.e. unsyncronized – reads and writes to the same pointer is what’s called a “data race”.

It’s also not even necessary to actively (try to) drop any of these short-lived references. You can easily do

let reference: &mut SomeType = &mut *ptr;
*reference = foo();
let another_reference: &mut SomeType = &mut *ptr;
*another_reference = bar();

and that’s fine as long as you make absolutely sure that the first reference is never used again after the creation of another_reference. (With “never used again”, I mean there should not be any subsequent mention of the variable reference at all, it’s not just dereferencing it that counts.)

6 Likes

Thank you for following me in my unsafe journey :sweat_smile:

Also thanks for that, I wasn't sure if this was allowed or not but your explanation helped. I guess I was being too hardline. Is it correct that it's safe to me to use mutable references to objects in a single function as long as that same function doesn't have another pointer to an object in its scope, ie aliasing cannot really happen? Like:

// T can be &Obj or &mut Obj here
fn no_alias(obj1: T) {}

// T must be *const Obj or *mut Obj as it can alias
fn can_alias(obj1: T, obj2: T) {}

This makes sense to me as the only reason reference aliasing is UB is because of optimisations within a single function (and I do not and will not ever use more than a single thread) This would make things a lot simpler implementation-wise, as raw pointers are a bit of a pain in Rust tbh.

It’s not even a problem for the pointer to still exist or be moved/copied around while a mutable reference (derived from the pointer) to its target exists. You just can’t dereference the pointer. For a function fn can_alias(obj1: &mut Obj, obj2: *mut Obj) {} where obj1 is expected to be derived from obj2 that means the function must not dereference obj2 at all during it’s call. (A function argument is always considered alive throughout the function, it doesn’t matter if or where you use obj1 inside of can_alias. Edit: Actually, nevermind, this might not be true. I'm not sure what restrictions apply. I don't know whether or not it's e. g. allowed to stop using obj1 and then start using obj2 in such a function.)

Another thing to keep in mind when working with references and pointers: If you derive a reference from a pointer, then a new pointer from that reference, the new pointer must only be used while the reference is still alive. I.e. if you have a ptr: *mut T then

let ptr_copy: *mut T = ptr;

is fundamentally different from

let ptr_copy: *mut T = &mut *ptr;

The former allows you to use ptr_copy and ptr interchangably. So you may e.g. alternate writing to ptr_copy and ptr afterwards. OTOH in the latter case, ptr_copy becomes immediately invalidated once you use ptr again.

If you “derive” a pointer from a reference by copying one of it’s target’s fields, which happens to be a pointer, this is of course not a problem.

Something like

struct Foo(*mut Bar);`
let ptr: *mut Foo = …;

let reference = &mut *ptr;
let derived: *mut Bar = reference.0; // copies the field

// continue using BOTH `ptr` and `derived` …

is totally fine. Even if you then do it another time

let another_reference = &mut *ptr;
let another_derived: *mut Bar = another_reference.0; // copies the field

// continue using `ptr`, `derived`, and `another_derived` …

it’s not a problem.

Edit: Fixed a typo, I had “let another_derived: *mut Bar = reference.0;” before which would’ve been undefined behavior of course, because another_reference was already created.

2 Likes

Thanks! This simplifies things a lot so I'll make good use of it :heart:

Ahh, thanks for the extra context, I remember your other questions about GCs now!

When I first read your post it sounded like you wanted to use unsafe as a hack to make the borrow checker stop complaining, but Implementing a GC'd language - especially one with shared mutation - is a very legitimate reason to reach for unsafe.

Because your code is pure Rust, probably the best advice for you is to write lots of tests and run them using cargo miri test. Miri implements one interpretation of Rust's memory model (stacked borrows) and will run your tests under an interpreter with the express goal of finding UB. I've found the error messages to be pretty reasonable, although you might want to check out Ralf Jung's blog for some background.

It's can only find UB in code that actually runs and even then it's not perfect, but Miri should be able to help you learn the subtleties of unsafe like "can I take a reference here", "am I allowed to mutate this value", and "is this code exception safe" (i.e. can a poorly timed panic trigger UB).

6 Likes

Oh wow I didn't know miri test was a thing, thank you, I'll definitely use this :slight_smile:

Make sure to run with MIRIFLAGS=-Zmiri-track-raw-pointers in the environment to get the full effect. (Miri's rules are an implementation of a model called "stacked borrows", which has not been adopted—and may never be adopted—as the official description of what Rust allows. But you should still fix any issues Miri reports, because it's the best way available to ensure that your code doesn't invoke UB "in practice" and will not do so under a future formalization of the rules.)

9 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.