You can think of it like that, yeah. There’s the raw mut ptr to the Node created inside push() and the Box itself has a ptr to its contents (via the Unique
wrapper).
Derefe’ing a Box gets you a shared or unique (depending on context) reference to its contents. A reference is just a borrow-checked pointer. So then you also have a mutable ptr stored inside the Box and then a shared or unique ptr (really reference but it’s essentially the same thing for our purpose here) elsewhere.
So having two ptrs exist in memory at the same time isn’t an issue on its own - the safety/legality comes into play when you start doing read/write operations through the pointer. Under normal borrow conditions, if you obtain a &mut
from the Box, then no more shared or unique borrows can be taken out while that &mut
is still active - compiler enforces this. You also cannot move the Box until all borrows of it (or its content) are gone - this is also compiler checked. This latter bit is what the code there is sidestepping, and its able to get away with it because of the stable address aspect.
You can also consider how mutable reborrow works. This is a case where some code has a &mut and then another piece of code borrows it temporarily (ie reborrow) - while the reborrow is active, the original &mut is frozen: no reads or writes through it. But, it exists in memory. When the reborrow ends, the original &mut becomes active again and can read/write the value. The compiler verifies all of this. The key part is that a read/write happens through a unique path to the value in this case. The path may change (eg when a reborrow is done) but there’s a unique one at any given point in time. When you use raw ptrs, you need to ensure the same thing manually.
This is a somewhat subtle and nuanced topic though. At some point, Rust will likely gain a formal memory model, at which point I suspect these interactions and aspects will be defined a bit more formally and/or comprehensively.