Repository navigation
Unsoundness due to #[pin_v2] incorrectly handles packed structs. #157011
Description
When the destructor of a repr(packed) struct runs, if it has a field which is unaligned and has a destructor, that field will be moved to an aligned place before the destructor of that field is ran. (See also #143411, taiki-e/pin-project-lite#89, taiki-e/pin-project#34.)
The #[pin_v2] attribute does not take this into account. This allows a Drop impl of a type to access a value of that type via &mut, even after pin-projection was used to access a value of that same type via Pin<&mut>.
The below code demonstrates this unsoundness. The two addresses it prints are different, showing that the invariant of Pin was violated.
#![feature(pin_ergonomics)]
#![expect(incomplete_features)]
use std::{
marker::PhantomPinned,
pin::{Pin, pin},
};
#[pin_v2]
#[repr(C, packed(4))]
struct One<T>(T);
#[pin_v2]
#[repr(C, align(65536))]
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}");
}
}Here's an explanation of the various parts this code:
- The
Onestruct is packed to an alignment of 4 bytes. This causes theThingfield to also be aligned to 4 bytes, which allows projectingpinned_onetopinned_thingwithout causing a compile error due to an unaligned reference toThing. - The
Twostruct is aligned to more than 4 bytes. This means that, when the destructor ofOneis ran, it notices that its field has an alignment of 65536. Therefore, thisTwofield is moved to an aligned place beforeTwo's destructor is ran. - The
Onestruct needs to be generic, to work around a compile error that prevents me from directly putting arepr(align)type inside arepr(packed)type.
Meta
Reproducible on the playground with version 1.98.0-nightly (2026-05-26 d1fc603d1788cc3c0eeb)
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
Dnreikronos commentedon Jun 3, 2026
I took a look into and reached this:
The simplest fix here is probably to just ban
#[pin_v2]on#[repr(packed)], which is what pin-project and pin-project-lite do. But I think that's broader than the actual hole, because a lot of this is already blocked today:#[repr(align)]type") already stops you from putting a concrete over-aligned field in a packed struct.unaligned_references(a hard error since Tracking Issue for future-incompatibility warningunaligned_references#82523) already stops you from taking a reference to an under-aligned packed field.So we can't drop a concrete
repr(align)field in there, and you can't&pin mutan under-aligned leaf either.But the real problem is the move-on-drop. When a
packed(N)struct drops, any direct fieldfwithalign(f) > Ngets moved to an aligned spot first, and that move takes whatever is insidefwith it, including a deeper leaf that is fine on its own and was handed out asPin<&mut leaf>. That's exactly what the repro does:Thing(align 4) is fine underpacked(4), sounaligned_referencesdoesn't complain, but the field that moves is its parentTwo(align 65536), and we never take a reference toTwo, so nothing catches it.So there are really two cases the current checks miss. The first is a generic field, like
One<T>. We don't knowalign(T)at definition time, and it can go overNonce it's monomorphized, which is the repro. The second is plain natural over-alignment, no generics needed: apacked(4)field whose natural alignment is 8 (it has au64in it), with a pinned leaf at a 4-aligned offset. E0588 only checksrepr(align), not natural alignment, so this one slips through.The generic one is the hard part, and honestly I don't think it can be both sound and permissive.
#[repr(packed(N))] #[pin_v2] struct One<T>(T)is unsound the momentThasalign > N, and there's no way to write "T's alignment is at most N" as a bound. So we have to restrict generic packed pin_v2 fields somehow no matter what.That said, instead of banning the whole thing, what if we did a per-field check at the struct definition? Reject a
#[pin_v2]packed(N)struct only when a field can havealign > N: a generic field we can't prove, so reject it; a concrete field we just work out its alignment and compare. The rule is basically "a pin_v2 type has to promise none of its pinned fields ever get moved by drop glue", which under packing just meansalign(field) <= packat every packed level. The good thing is the safe case still compiles (apacked(N)pin_v2 struct where every field is<= N), and both cases above are closed.We could go further and check at the projection site instead, which would let
One<T>stay defined and only reject the W-aligned uses. But that needs layout info right at the projection, and then you run into typeck running before layout, plus the post-mono cases, which feels like a lot of extra work for an experimental feature.I'm leaning towards the per-field check, since the plain ban throws away cases that are actually fine. But I don't feel strongly about it, and the ban is obviously less risky to implement. I can put up a PR for whichever you'd rather have
What you think about it, @theemathas?
theemathas commentedon Jun 3, 2026
@Dnreikronos I worry that doing any logic involving alignment of types might cause a semver hazard.
That is, an upstream crate might define a type. A downstream crate then puts that type into a
#[pin_v2] #[repr(packed)]struct. Later, the upstream crate updates private fields in that type, which increases the alignment. This causes the downstream crate to break. This happens even though it might not be obvious to the downstream crate's author that they're relying on the alignment of the upstream crate's type.Therefore, I'm in favor of just banning
#[pin_v2]on a#[repr(packed)]type. I doubt this would affect many use cases anyway.Dnreikronos commentedon Jun 3, 2026
Yeah, good observation on the semver hazard. That pretty much kills the per-field check for me. If compiling depends on a type's alignment, then an upstream crate bumping its alignment in a patch release would quietly break downstream code, and alignment was never something you're supposed to rely on anyway.
I can put up a PR for it, unless someone on the lang or compiler side wants to take it a different way.
theemathas commentedon Jun 3, 2026
@Dnreikronos On second thought, I'm not sure if the semver hazard is a big deal.
#[repr(packed)]already has a semver hazard. Creating a reference to a packed struct's field causes an alignment check:Not sure what the best way to implement
#[pin_v2]would be...Dnreikronos commentedon Jun 3, 2026
I will study more about it and I will send the info here to take your opinion
Dnreikronos commentedon Jun 3, 2026
Hey @theemathas, ok so I studied this a bit more, here's where I'm at.
The main thing I missed before: the per-field align check (the idea where we'd only reject a packed pin_v2 struct if some field's align could go over the pack) doesn't actually save the repro at all.
One<T>is generic, so there's no way to provealign(T) <= Nat definition time, which means we'd end up rejecting it anyway. So that check would really only keep the narrow case alive where the struct is fully concrete and every field is already align <= N. That's a pretty niche set tbh, and we'd be paying for it with alignment-dependent compilation (the semver thing you mentioned), so imo not really worth it.The only thing that would actually keep
One<T>compiling is checking at the projection site instead of at the definition. But that one needs layout info right where the projection happens, and typeck runs before layout, plus the post-mono cases... it's a fair bit of work for an experimental feature and idk if it's worth doing right now.So yeah, I'm leaning back towards just banning
#[pin_v2]on#[repr(packed)], like pin-project does (pin-project-lite too, though it leans on a lint trick that has a known align-1 hole, see taiki-e/pin-project-lite#89). It's sound, it's tiny to implement (basically a flag check next to the existing packed checks, no align logic at all, so no new semver hazard), and the part I like is that it doesn't lock us in: if we ever want the permissive projection-site version later, that one only ever accepts more code, so adding it wouldn't break anyone.Lmk what you think. If you're good with the ban I'll put up a PR for it.
theemathas commentedon Jun 3, 2026
I'm not the one who's deciding this, but banning packed structs sounds reasonable to me.
Rollup merge of rust-lang#157542 - Dnreikronos:pin_v2_ban_repr_packed…
Unrolled build for #157542