I'm working with futures::Sink and make a study about Pin<Box<dyn Sink>> between Box<dyn Sink>. I'm into a situation where I need to convert a &mut Box<dyn Sink> to &mut Pin<Box<dyn Sink>>. I believe this conversion should be safe, but I can't find a safe API to do it.
My reasoning
Here's why I think this conversion should be safe:
Unique mutable reference: I have the only &mut Box<T> reference to the boxed value
Consuming conversion: The conversion would consume my original &mut Box<T> reference, so I can't use it to move the contents afterward
Pin guarantees: Through &mut Pin<Box<T>>, I cannot safely obtain &mut T (unless T: Unpin) to move the contents
The key insight is that while Box<T> puts T on the heap, I could theoretically still move T's contents through operations like std::mem::replace(&mut **box_ref, new_value). However, once I convert to &mut Pin<Box<T>> and lose access to the original reference, there's no way to perform such operations anymore.
I'm wondering why there isn't a API just like Option::as_pin_mut() for Box<T>.
Questions
Is my reasoning correct about this conversion being safe?
If it is safe, why doesn't the standard library provide this API?
Are there any edge cases or subtle issues I'm missing?
Would this be worth proposing as an addition to the standard library?
The conversion seems logically sound to me - the borrowing rules ensure I can't access the original Box after conversion, and Pin prevents unsafe access to the contents. But I'm curious if there are deeper reasons why this isn't available as a safe API.
The problem is that after your mutable reference is dropped, the owner of the Box<T> can move the T without dropping it, which violates the Pin guarantee.
Once you call a function that takes Pin<&mut T>, you must assume that T has entered an address-sensitive state, which means that all pointers to the T[1] must be pinned pointers to ensure that the T is not moved until its dropped. The duration of the requirement is until the T is dropped, not until your &mut Box<T> is dropped.
Your proposed conversion is unsound unless you fully control the Box<T> to ensure it won't be moved out of, and if that is true, then you might as well convert it to (or create it as) a Pin<Box<T>>.
Thank you for the detailed explanation! I really appreciate the clarity.
I acknowledge that my understanding was incorrect. Rust requires that T cannot be moved after being pinned until T is dropped, not just until the &mut Pin<Box<T>> reference is dropped.
However, this explanation raises an interesting question about Rust's safety model that I'd like to confirm my understanding of:
It seems that safe Rust can indeed create dangling pointers. For example:
struct SelfRef {
data: String,
ptr: *const String,
}
fn create_self_ref() -> SelfRef {
let mut s = SelfRef {
data: "hello".to_string(),
ptr: std::ptr::null(),
};
s.ptr = &s.data as *const String; // Taking raw pointer is safe
s // Return non-pinned value
}
fn main() {
let s = create_self_ref();
let moved = s; // This creates a dangling pointer in safe Rust!
}
Is my understanding correct that safe Rust can create dangling pointers, but without unsafe code, safe Rust cannot cause memory unsoundness by dereferencing these dangling pointers?
If so, this raises a design question: Both behaviors - "allowing moves after &mut Pin<Box<T>> is dropped" and "allowing creation of dangling pointers in the first place" - rely on the same safety property: that safe Rust cannot dereference raw pointers. They seem to be at the same level of "safety risk."
Given this, why not allow the behavior I originally proposed (converting &mut Box<T> to &mut Pin<Box<T>>)? It doesn't seem to introduce additional unsafety beyond what's already possible in safe Rust. The restriction feels overly conservative when safe code can already create the very problem (dangling pointers) that Pin is trying to prevent.
I'm curious about the design rationale here - is it purely about being conservative, or are there subtle safety issues I'm missing?
Thanks again for helping me understand this complex topic!
Pin provides the property that safe Rust cannot move the owned T. It's what allows unsafe code to assume that dereferencing the raw pointer is safe. Pin resolves the loophole you observe: safe Rust cannot create dangling pointers from Pin<T>. (Any type containing a raw pointer cannot be Unpin.[1])
The conversion you propose would be a fiction because, as described by @kpreid, you reintroduce the possibility of safe Rust moving T after it is "pinned". You've undermined the purpose of Pin.
As a counterexample to this line of reasoning, it’s possible to create non-raw pointers that rely on the Pin guarantee. Async blocks do this all the time:
let future = async {
let x = 1;
let y = &x;
something().await;
println!("{y}");
};
y is a reference to x, which is safe to dereference; and both x and y are owned by future. Therefore, before execution reaches the println!(), future must be pinned to ensure that y will not dangle.
The fundamental purpose of Pin is to communicate a promise that the value won't move from the code that performs pinning to the code that accepts Pin<&mut Self> or similar. Creating Pin must be unsafe in the general case because if it is not, the code receiving Pin can’t rely on that promise — if Pin were to be “no guarantee of validity” like a raw pointer, then Pin would be completely meaningless because it wouldn’t actually offer anything the underlying pointer doesn’t.
Raw pointers have much, much less validity requirements than references (&_, &mut _).[1] The existence of a dangling pointer isn't a safety issue in isolation.
The question generally comes down to, what can others assume? And the decision was made that others can assume no moves after pinning, period.[2]That's the pinning guarantee. So if someone else uses unsafe to create some library which is sound under that guarantee, and you use unsafe to violate the guarantee, and UB results -- you're the one to blame.
Here's an example of what I mean. Run it with Miri under Tools, top-right, and you'll see that there is UB. Your suggested conversion made it possible for the struct to become self-referencial when it wasn't actually pinned.
Thank you all for the incredibly insightful explanations! This discussion has been tremendously educational.
I now understand my fundamental misconception. I was focused on raw pointers and thinking "dangling raw pointers are allowed in safe Rust, so what's the big deal?" But I completely missed the crucial point that Pin protects safe references and unsafe creation operations, not just raw pointers.
@kpreid's async example was particularly illuminating:
let future = async {
let x = 1;
let y = &x; // This is a SAFE REFERENCE, not a raw pointer!
something().await;
println!("{y}");
};
If future could be moved after being "pinned", y would become a dangling reference (not just a raw pointer), which is immediate UB. This is completely different from the raw pointer scenarios I was considering.
@parasyte and @quinedot helped me understand that Pin's guarantee ("no moves after pinning, period") is what allows unsafe code to soundly create self-referential structures with actual safe references inside them. Without this guarantee, such unsafe code would be impossible to write soundly.
@quinedot's playground example perfectly demonstrates how my proposed conversion would enable UB by allowing structures to become self-referential when they're not actually pinned.
The key insight I was missing: Pin isn't just about preventing raw pointer invalidation (which safe Rust can already do). It's about enabling a whole class of unsafe code that creates safe references within self-referential structures. These references must remain valid for their entire lifetime, which requires the "no moves after pinning" guarantee.
Thank you for your patience in explaining this subtle but crucial distinction. This has significantly deepened my understanding of Rust's safety model and the relationship between safe and unsafe code.
That’s all true, but it's not just about references to pinned data. Pin allows raw-pointer-using code (e.g. an intrusive linked list) to be assured that those pointers won’t become dangling. Or simply a safe smart pointer that’s implemented in terms of raw pointers. References are not privileged here; the important thing is that Pin enables safe-to-use types that contain pointers to the pinned data, regardless of whether those pointers are references or not. References are merely the simplest possible example of this, where the pointer type itself is safe to use.