Skip to content

Cloning a Pin<Box> with a custom allocator can violate the pinning invariant #157089

@theemathas

Description

@theemathas

View all comments

This unsoundness is caused by a violation of #152667. I discovered this after @Darksonn noted a discrepancy at #156935 (comment).

Cloning a Pin<Box<T, A>>, causes <Box<T, A>>::clone() to run, and then Pin::new_unchecked() is called on the result. <Box<T, A>>::clone() can be user-defined code, since Box is a fundamental type. Therefore, it is possible to call Pin::new_unchecked() on an arbitrary value of Box<T, A> in safe code.

In most cases, calling Pin::new_unchecked() on a Box<T, A> is harmless, as it is usually equivalent to Box::into_pin(). However, Box::into_pin() actually has an A: 'static bound. This is because, if A is not 'static, then the memory in the allocator may be reused/deallocated after 'a expires, without actually running the destructor of T, violating the pinning invariant.

In order to produce a Pin<Box<T, A>> with a non-'static allocator A in the first place, so we could call clone, we can create a pinned box with a 'static allocator, and then use a subtype coercion to turn it into a non-'static allocator.

The below code exploits this. (Tested with pin-project version 1.1.13, and bumpalo version 3.20.3 with the allocator_api feature.)

#![feature(allocator_api)]

use std::{alloc::Allocator, cell::Cell, marker::PhantomPinned, mem::forget, pin::Pin};

use bumpalo::Bump;
use pin_project::pin_project;

#[pin_project]
struct Thing<'a, T, A>(#[pin] Option<T>, Cell<Option<(T, &'a A)>>);

impl<'a, T, A> Clone for Box<Thing<'a, T, A>, &'a A>
where
    &'a A: Allocator,
{
    fn clone(&self) -> Self {
        let (new_t, new_a) = self.1.take().unwrap();
        Box::new_in(Thing(Some(new_t), Cell::new(None)), new_a)
    }
}

// Allocates the payload in allocator_2, then
// calls the callback with a pinned reference to that.
fn wrong_pin<'a, T, A>(
    payload: T,
    allocator_1: &'static A,
    allocator_2: &'a A,
    callback: impl FnOnce(Pin<&mut T>),
) where
    for<'b> &'b A: Allocator,
{
    let pin_1: Pin<Box<Thing<'a, T, A>, &'static A>> = Box::pin_in(
        Thing(None, Cell::new(Some((payload, allocator_2)))),
        allocator_1,
    );
    let pin_1_again: Pin<Box<Thing<'a, T, A>, &'a A>> = pin_1;
    let mut pin_2: Pin<Box<Thing<'a, T, A>, &'a A>> = Pin::clone(&pin_1_again);
    let pin_option_t: Pin<&mut Option<T>> = pin_2.as_mut().project().0;
    let pin_t: Pin<&mut T> = pin_option_t.as_pin_mut().unwrap();
    callback(pin_t);
    forget(pin_2);
}

struct Payload(PhantomPinned, #[expect(dead_code)] [u8; 1024]);

impl Drop for Payload {
    fn drop(&mut self) {
        println!("Drop ran");
    }
}

fn main() {
    let payload = Payload(PhantomPinned, [0_u8; 1024]);
    let bump_1_ref: &'static Bump = Box::leak(Box::new(Bump::new()));
    let mut bump_2: Bump = Bump::with_capacity(16);
    wrong_pin::<Payload, Bump>(
        payload,
        bump_1_ref,
        &bump_2,
        |pinned_payload: Pin<&mut Payload>| println!("Pinned 1024 bytes at {pinned_payload:p}"),
    );
    bump_2.reset();
    let reused: Box<_, &Bump> = Box::new_in([0_u8; 1024], &bump_2);
    println!("Reused 1024 bytes at {reused:p}");
}

On my computer, the code prints the following:

Pinned 1024 bytes at 0xa8b008bb9
Reused 1024 bytes at 0xa8b008bc0

Based on the output, we can see that the program pins a 1024-byte value at a certain memory, and then (without running the value's destructor) proceeds to reuse an overlapping chunk of memory for something else. This violates the pinning invariant.

Meta

rustc --version --verbose:

rustc 1.98.0-nightly (cced03bfd 2026-05-28)
binary: rustc
commit-hash: cced03bfd61a304243a34504618ecec86c17063f
commit-date: 2026-05-28
host: aarch64-apple-darwin
release: 1.98.0-nightly
LLVM version: 22.1.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-allocatorsArea: Custom and system allocatorsA-boxArea: Our favorite opsem complicationA-coherenceArea: CoherenceA-pinArea: PinA-varianceArea: Variance (https://doc.rust-lang.org/nomicon/subtyping.html)C-bugCategory: This is a bug.I-types-nominatedNominated for discussion during a types team meeting.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions