So when the destructor of a repr(packed) struct runs, a field that's unaligned and has a destructor gets moved to an aligned place before that field's destructor runs. (See also #157011, #143411, taiki-e/pin-project-lite#89, taiki-e/pin-project#34.)
#157011 covers this for #[pin_v2], and the fix there (#157542) just bans #[pin_v2] + #[repr(packed)]. But afaict the #[pin_v2] attribute only gates the implicit projection (the default binding-mode shift, like plain Foo { x, y } matched against a Pin<&mut Foo>). The explicit &pin mut / ref pin mut patterns project in place no matter whether the type opted in, so banning the attribute combo doesn't really close this. You can hit the exact same move-on-drop with no #[pin_v2] anywhere. Filing this as the standalone issue for the leftover case that #157542 left open under the pin ergonomics tracking issue #130494.
The projection soundness check lives in rustc_hir_typeck/src/pat.rs and only runs under default_binding_modes. Spelling the deref out (&pin mut .. ref pin mut ..) kinda walks right past it, which is why no attribute is needed and why the #157011 fix doesn't catch this.
I tried this code:
#![feature(pin_ergonomics)]
#![allow(incomplete_features)]
use std::{marker::PhantomPinned, pin::{Pin, pin}};
// no #[pin_v2] on either type
#[repr(C, packed(4))]
struct One<T>(T);
#[repr(C, align(4096))]
struct Two(Thing);
struct Thing(#[expect(dead_code)] i32, PhantomPinned);
fn main() {
let pinned_one: Pin<&mut One<Two>> = pin!(One(Two(Thing(0, PhantomPinned))));
let &pin mut One(Two(ref pin mut pinned_thing)) = pinned_one;
access(pinned_thing);
}
fn access(x: Pin<&mut Thing>) {
println!("Pinned access at {x:p}")
}
impl Drop for Thing {
fn drop(&mut self) {
println!("Dropped at {self:p}");
}
}
I expected to see this happen: the explicit &pin mut / ref pin mut projection through a packed type should get rejected, like the #[pin_v2] case in #157011. A Pin<&mut Thing> you hand out should sure keep a stable address for the rest of the value's life.
Instead, this happened: it just compiles and runs, and the pinned-access addr and the drop addr come out different, so the Pin invariant is broken. One is packed(4), so its Thing leaf is aligned to 4. That's fine on its own, so unaligned_references doesn't complain. The thing that actually moves on drop is the parent Two (align 4096), and we never take a reference to Two, so nothing catches it. (One has to be generic to dodge E0588.)
Couple more notes: I used align(4096) instead of #157011's 65536 just so it prints cleanly. At 65536 it straight up segfaults on my machine (stack realignment during the drop-glue move), same unsoundness, just louder. Also the &pin mut <place> borrow operator (like &pin mut one.0.0) does not repro this, imo because it copies the packed field out to an aligned temp first, so it stays safe. It's specifically the pattern form that pins in place.
Meta
rustc --version --verbose:
rustc 1.98.0-nightly (f20a92ec0 2026-06-07)
binary: rustc
commit-hash: f20a92ec01483dc5c58e90e246f266bdad822d86
commit-date: 2026-06-07
host: x86_64-pc-windows-msvc
release: 1.98.0-nightly
LLVM version: 22.1.6
So when the destructor of a
repr(packed)struct runs, a field that's unaligned and has a destructor gets moved to an aligned place before that field's destructor runs. (See also #157011, #143411, taiki-e/pin-project-lite#89, taiki-e/pin-project#34.)#157011 covers this for
#[pin_v2], and the fix there (#157542) just bans#[pin_v2]+#[repr(packed)]. But afaict the#[pin_v2]attribute only gates the implicit projection (the default binding-mode shift, like plainFoo { x, y }matched against aPin<&mut Foo>). The explicit&pin mut/ref pin mutpatterns project in place no matter whether the type opted in, so banning the attribute combo doesn't really close this. You can hit the exact same move-on-drop with no#[pin_v2]anywhere. Filing this as the standalone issue for the leftover case that #157542 left open under the pin ergonomics tracking issue #130494.The projection soundness check lives in
rustc_hir_typeck/src/pat.rsand only runs underdefault_binding_modes. Spelling the deref out (&pin mut .. ref pin mut ..) kinda walks right past it, which is why no attribute is needed and why the #157011 fix doesn't catch this.I tried this code:
I expected to see this happen: the explicit
&pin mut/ref pin mutprojection through a packed type should get rejected, like the#[pin_v2]case in #157011. APin<&mut Thing>you hand out should sure keep a stable address for the rest of the value's life.Instead, this happened: it just compiles and runs, and the pinned-access addr and the drop addr come out different, so the
Pininvariant is broken.Oneispacked(4), so itsThingleaf is aligned to 4. That's fine on its own, sounaligned_referencesdoesn't complain. The thing that actually moves on drop is the parentTwo(align 4096), and we never take a reference toTwo, so nothing catches it. (Onehas to be generic to dodge E0588.)Couple more notes: I used
align(4096)instead of #157011's65536just so it prints cleanly. At65536it straight up segfaults on my machine (stack realignment during the drop-glue move), same unsoundness, just louder. Also the&pin mut <place>borrow operator (like&pin mut one.0.0) does not repro this, imo because it copies the packed field out to an aligned temp first, so it stays safe. It's specifically the pattern form that pins in place.Meta
rustc --version --verbose: