Skip to content

Cloning a Pin<Box> with custom allocator can violate the drop guarantee without ever calling Box::pin or similar #159445

Description

@maxdexh

Like #157089, this uses a custom clone impl on Box<LocalType, A> to trick the derive(Clone) on Pin into pinning a box with an inelligible allocator. However, unlike that issue, this one creates the initial Pin<Box> using a type that implements Unpin, thus bypassing all current attempts to fix the issue through stricter requirements on Box -> Pin<Box> conversions. There is also no variance involved here, only unsize coercions through Pin<Box>.

playground (using a wrapper for bumpalo due to missing feature flags)

#![feature(allocator_api)]

use std::{alloc::Allocator, marker::PhantomPinned, pin::Pin};

use bumpalo::Bump;

// This type is `Unpin`
type BoxParts<T, A> = (fn() -> T, A);

// Goal: Use a `Clone` impl on `Box<dyn Helper<T, A>, A>` to turn `Box<BoxParts<T, A>, A>` into `Box<T,A>`, 
// so that `Pin::clone` automatically pins it in an arbitrary allocator, which is unsound.
trait Helper<T, A> {
    fn parts(&self) -> BoxParts<T, A>;
    fn downcast(self: Pin<&mut Self>) -> Pin<&mut T>;
}
impl<T, A: Clone> Helper<T, A> for BoxParts<T, A> {
    fn parts(&self) -> Self {
        self.clone()
    }
    fn downcast(self: Pin<&mut Self>) -> Pin<&mut T> {
        unreachable!("never called")
    }
}
impl<T, A> Helper<T, A> for T {
    fn parts(&self) -> BoxParts<T, A> {
        unreachable!("never called")
    }
    fn downcast(self: Pin<&mut Self>) -> Pin<&mut T> {
        self
    }
}
impl<'a, T: 'a, A: Allocator> Clone for Box<dyn Helper<T, A> + 'a, A> {
    fn clone(&self) -> Self {
        let (fac, alloc) = (**self).parts();
        Box::new_in(fac(), alloc)
    }
}

fn break_drop_guarantee<T>(fac: fn() -> T) {
    let mut short = Bump::new();

    // no requirements for the allocator, we are just pinning a `Box<impl Unpin>` 
    let base: Pin<Box<BoxParts<T, &Bump>, &Bump>> = Pin::new(Box::new_in((fac, &short), &short));

    // unsize coercion
    let base: Pin<Box<dyn Helper<T, &Bump> + '_, &Bump>> = base;

    // Make `Clone` produce a different value to bypass the Box::pin requirements
    let mut bad: Pin<Box<dyn Helper<T, &Bump> + '_, &Bump>> = base.clone();

    // the destructor of `T` must run before its memory is reused
    let proof: Pin<&mut T> = bad.as_mut().downcast();

    // payload: 0x57dcbd949390
    println!("payload: {:p}", proof);

    // get rid of the borrows of `short`
    std::mem::forget(base);
    std::mem::forget(bad);

    // deallocate `T` without running its destructor
    short.reset();

    // unsoundly reuse the memory of `T`
    let reuse = short.alloc([0u8; 1024]);

    // reuse: 0x57dcbd949390
    println!("reuse: {:p}", reuse);
}

pub fn main() {
    struct Victim(#[expect(dead_code)] [u8; 1024], PhantomPinned);
    impl Drop for Victim {
        fn drop(&mut self) {
            unreachable!("never run")
        }
    }
    break_drop_guarantee(|| Victim([0; _], PhantomPinned));
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-allocatorsArea: Custom and system allocatorsA-boxArea: Our favorite opsem complicationA-pinArea: PinC-bugCategory: This is a bug.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.needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

    Type

    No type

    Fields

    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