Skip to content

Unsoundness due to #[pin_v2] incorrectly handles packed structs. #157011

@theemathas

Description

@theemathas

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 One struct is packed to an alignment of 4 bytes. This causes the Thing field to also be aligned to 4 bytes, which allows projecting pinned_one to pinned_thing without causing a compile error due to an unaligned reference to Thing.
  • The Two struct is aligned to more than 4 bytes. This means that, when the destructor of One is ran, it notices that its field has an alignment of 65536. Therefore, this Two field is moved to an aligned place before Two's destructor is ran.
  • The One struct needs to be generic, to work around a compile error that prevents me from directly putting a repr(align) type inside a repr(packed) type.

Meta

Reproducible on the playground with version 1.98.0-nightly (2026-05-26 d1fc603d1788cc3c0eeb)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-destructorsArea: Destructors (`Drop`, …)A-reprArea: the `#[repr(stuff)]` attributeA-repr-packedArea: the naughtiest reprC-bugCategory: This is a bug.F-pin_ergonomics`#![feature(pin_ergonomics)]`I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions