Repository navigation
Description
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
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
Dnreikronos commentedon Jun 8, 2026
Dnreikronos commentedon Jun 8, 2026
Invested some time on this a bit. the check that should catch it (
ProjectOnNonPinProjectTypeinpat.rs) is gated onpat.default_binding_modes, so it only runs for the implicit projection. explicit&pin mut/ref pin mutgo throughcheck_pat_ref->check_pat_tuple_struct/check_pat_struct, which dont do any pin or packed check at all (nopackedanywhere inpat.rs).so two things fall out, not just the packed bug: the hole here, and explicit projection skiping
#[pin_v2]entirely. idk if that second ones a bug or intended (explicit = the manual path that doesn't need the attr?), and that kinda decides the fix. this compiles today, nothing packed, no#[pin_v2]:good thing is the signal's alredy there:
check_pat_refsetspat_info.max_pinnedness = Pinnedafter peeling&pin mut, and it flows down to every subpattern (the binding site already uses it for theUnpinbound). so the only new bit isadt.repr().packed().three ways imo:
(1) gate the explicit path on
#[pin_v2]too, i.e. runProjectOnNonPinProjectTypethere as well. #157542 (still open) bans#[pin_v2]+ packed, so once that lands this closes the packed hole for free and makes explicit/implicit match. only works if the skip is a bug though.(2) blunt packed check at the destructure:
same move as #157542 (ban the combo), matches the
upvar.rscapture restriction that stops at a packed field regardless of alignment. one false positive:&pin mut Packed(x)by value is sound but gets rejected. rare, dont care.(3) like 2 but precise: thread a "went through packed" flag on
PatInfo, reset on deref (likemost_packed_projection), error only when a pinned ref's actually formed. no false positives, just more fiddly.leaning 2 to start, 3 later if the by-value case ever bites. but if explicit should just require
#[pin_v2]like implicit, 1 is cleaner and i'll do that. ltk what you prefer.cc @theemathas @mejrs
theemathas commentedon Jun 9, 2026
I think only approach 1 is the only viable approach, due to #157634.
Rollup merge of rust-lang#157680 - Dnreikronos:pin_ergonomics_explici…
Unrolled build for #157680
Rollup merge of #157680 - Dnreikronos:pin_ergonomics_explicit_project…
theemathas commentedon Jun 19, 2026
Fixed by #157680