Skip to content

Reject #[repr(packed)] on #[pin_v2] types#157542

Merged
rust-bors[bot] merged 2 commits into
rust-lang:mainfrom
Dnreikronos:pin_v2_ban_repr_packed
Jun 9, 2026
Merged

Reject #[repr(packed)] on #[pin_v2] types#157542
rust-bors[bot] merged 2 commits into
rust-lang:mainfrom
Dnreikronos:pin_v2_ban_repr_packed

Conversation

@Dnreikronos

@Dnreikronos Dnreikronos commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #157011

#[repr(packed)] can store an over-aligned field below its alignment, and the drop glue for a packed type moves that field to a properly aligned spot before dropping it. When the field is structurally pinned, that move pulls it out from under a Pin<&mut _> we already handed out, which breaks the pin invariant. The repro in the issue makes it pretty clear: it prints one address during pinned access and a different one on drop.

So the fix just rejects the combo at the type definition. If a type is both #[pin_v2] and #[repr(packed)], it no longer compiles. The check sits in check_packed in rustc_hir_analysis, so it catches the concrete case and the generic one too (like One<T>, where we don't know T's alignment yet), with no layout or monomorphization needed.

One thing I want to be upfront about: this is necessary but don't solve the problem entirely. Sure, it stops the spelling the issue used, but you can still trigger the same move-on-drop through an explicit &pin mut / ref pin mut projection with no #[pin_v2] anywhere. Leaving that broader case open is intentional, imo the narrow ban is the right call for now, and the leftover stays tracked on the pin ergonomics tracking issue #130494.

This is the direction we landed on with @workingjubilee over on Zulip, lgtm from their side: https://rust-lang.zulipchat.com/#narrow/channel/182449-t-compiler.2Fhelp/topic/.60pin_v2.60.20is.20unsound.20with.20.60packed.60/with/600522893

Tests live in tests/ui/pin-ergonomics/pin_v2-packed.rs: the three rejected shapes (packed struct, generic packed(4) struct, packed union), plus two controls that still compile fine, #[pin_v2] without packed and packed without #[pin_v2].

Disclosure: AI tooling was used on the code changes, and everything was strictly validated by me before sending to remote.

A `#[repr(packed)]` type can store an over-aligned field below its
alignment. Drop glue for such a type moves the field to a properly
aligned location before dropping it, which would move a structurally
pinned field out from under a `Pin<&mut _>` that was handed out,
breaking Pin's invariant.

Reject the combination at the type definition so the unsound program
never compiles.
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jun 6, 2026
@rustbot

rustbot commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

r? @folkertdev

rustbot has assigned @folkertdev.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: compiler
  • compiler expanded to 73 candidates
  • Random selection from 20 candidates

@mejrs mejrs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +1639 to +1647
if def.is_pin_project()
&& let Some(pin_v2_span) = find_attr!(tcx, def.did(), PinV2(span) => *span)
{
tcx.dcx().emit_err(errors::PinV2OnPacked {
span: sp,
pin_v2_span,
adt_name: tcx.item_name(def.did()),
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should be emitted whether or not the attribute can be found. Your best option is to use Option<Span> instead. Take for example

if tcx.adt_def(adt_def_id).is_pin_project() {
let pin_v2_span = rustc_hir::find_attr!(tcx, adt_def_id, PinV2(attr) => *attr);
let adt_name = tcx.item_name(adt_def_id);
return Err(tcx.dcx().emit_err(crate::errors::PinV2WithoutPinDrop {
span,
pin_v2_span,
adt_name,
}));

@rustbot rustbot assigned mejrs and unassigned folkertdev Jun 6, 2026
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 6, 2026
@rustbot

rustbot commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

Tie emission to `is_pin_project()` alone and pass the `#[pin_v2]`
span as `Option<Span>`, so the error still fires if the span cannot
be found, mirroring `PinV2WithoutPinDrop`.
@Dnreikronos Dnreikronos force-pushed the pin_v2_ban_repr_packed branch from 75249a0 to 9663384 Compare June 7, 2026 00:46
@theemathas

Copy link
Copy Markdown
Contributor

Sure, it stops the spelling the issue used, but you can still trigger the same move-on-drop through an explicit &pin mut / ref pin mut projection with no #[pin_v2] anywhere.

Could you provide example code for this?

@Dnreikronos

Dnreikronos commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Sure, it stops the spelling the issue used, but you can still trigger the same move-on-drop through an explicit &pin mut / ref pin mut projection with no #[pin_v2] anywhere.

Could you provide example code for this?

yeah here's a repro w/ no #[pin_v2] anywhere. it's basically the issue's example w/ the attrs stripped off, projecting purely through the explicit &pin mut / ref pin mut patterns:

#![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}");
    }
}

the pinned-access addr and the drop addr come out different, so the Pin invariant is broken, same as the original.

afaict #[pin_v2] only gates the implicit projection (the default binding-mode shift, like plain Foo { x, y } against a Pin<&mut Foo>). the explicit ref pin mut / &pin mut patterns project in place no matter whether the type opted in. the projection check in rustc_hir_typeck/src/pat.rs only runs under default_binding_modes, so spelling the deref out (&pin mut .. ref pin mut ..) just walks right past it. so banning pin_v2 + packed kills the exact spelling the issue used, but this one still hands you an in-place Pin<&mut> into the field that the packed drop glue then moves.

two things worth noting. i used align(4096) instead of the issue's 65536 just so it prints cleanly, at 65536 it segfaults on my machine (stack realignment during the drop-glue move), same unsoundness, just louder. and the &pin mut <place> borrow operator (like &pin mut one.0.0) doesn't repro this, 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.

@mejrs

mejrs commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

yeah here's a repro w/ no #[pin_v2]

Is there an issue for this and can either of you open one if it doesn't exist?

@Dnreikronos

Copy link
Copy Markdown
Contributor Author

yeah here's a repro w/ no #[pin_v2]

Is there an issue for this and can either of you open one if it doesn't exist?

On my search I didn't found any related with this specifically....so, yes, I will open one.

@Dnreikronos

Copy link
Copy Markdown
Contributor Author

e for this and can either of you open one if it doesn't exist?

On my search I didn't found any related with this specifically....so, yes, I will open one.

Can you guys validate, please?
#157615

cc// @theemathas @mejrs

@mejrs mejrs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll admit to not being an expert on Pin stuff but that looks good to me. I've also added it as a sub-issue.

@bors r+ rollup

View changes since this review

@rust-bors

rust-bors Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 9663384 has been approved by mejrs

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jun 8, 2026
rust-bors Bot pushed a commit that referenced this pull request Jun 9, 2026
Rollup of 5 pull requests

Successful merges:

 - #154543 (Fix deref field pattern suggestions and improve error messages)
 - #157476 (Adopt to LLVM 23 CfiFunctionIndex change)
 - #157542 (Reject `#[repr(packed)]` on `#[pin_v2]` types)
 - #157603 (Fixed Doc Comment Typo in Linewritershim)
 - #157624 (Move AttributeTemplate from rustc_feature to rustc_attr_parsing)
@rust-bors rust-bors Bot merged commit e43ec0c into rust-lang:main Jun 9, 2026
12 checks passed
@rustbot rustbot added this to the 1.98.0 milestone Jun 9, 2026
rust-timer added a commit that referenced this pull request Jun 9, 2026
Rollup merge of #157542 - Dnreikronos:pin_v2_ban_repr_packed, r=mejrs

Reject `#[repr(packed)]` on `#[pin_v2]` types

Fixes #157011

`#[repr(packed)]` can store an over-aligned field below its alignment, and the drop glue for a packed type moves that field to a properly aligned spot before dropping it. When the field is structurally pinned, that move pulls it out from under a `Pin<&mut _>` we already handed out, which breaks the pin invariant. The repro in the issue makes it pretty clear: it prints one address during pinned access and a different one on drop.

So the fix just rejects the combo at the type definition. If a type is both `#[pin_v2]` and `#[repr(packed)]`, it no longer compiles. The check sits in `check_packed` in `rustc_hir_analysis`, so it catches the concrete case and the generic one too (like `One<T>`, where we don't know `T`'s alignment yet), with no layout or monomorphization needed.

One thing I want to be upfront about: this is necessary but don't solve the problem entirely. Sure, it stops the spelling the issue used, but you can still trigger the same move-on-drop through an explicit `&pin mut` / `ref pin mut` projection with no `#[pin_v2]` anywhere. Leaving that broader case open is intentional, imo the narrow ban is the right call for now, and the leftover stays tracked on the pin ergonomics tracking issue #130494.

This is the direction we landed on with `@workingjubilee` over on Zulip, lgtm from their side: https://rust-lang.zulipchat.com/#narrow/channel/182449-t-compiler.2Fhelp/topic/.60pin_v2.60.20is.20unsound.20with.20.60packed.60/with/600522893

Tests live in `tests/ui/pin-ergonomics/pin_v2-packed.rs`: the three rejected shapes (packed struct, generic `packed(4)` struct, packed union), plus two controls that still compile fine, `#[pin_v2]` without packed and packed without `#[pin_v2]`.

*Disclosure: AI tooling was used on the code changes, and everything was strictly validated by me before sending to remote.*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants