rust-lang / rust-lang/rust

Tracking Issue for breaking raw pointer casts of trait objects

Open
#141,402 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-dyn-trait A-raw-pointers C-tracking-issue F-arbitrary_self_types F-derive_coerce_pointee T-lang
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

This is the tracking issue for the breaking changes made in #136776, https://github.com/rust-lang/rust/pull/120248, and https://github.com/rust-lang/rust/pull/136764. The goal of this page is describe why these changes were made and how you can fix code that is affected by them. It also provides a place to ask questions or register a complaint if you feel the changes should not have been made.

What is the error for?

As part of stabilizing the arbitrary_self_types and derive_coerce_pointee we needed to change what raw pointer casts are legal. Specifically:

  1. Casting *const dyn Trait + 'a to *const dyn Trait + 'b now requires that 'a outlives 'b
  2. Casting *const dyn Trait to *const dyn Trait + AutoTrait requires Trait: AutoTrait
  3. Casting *const dyn Trait<'a, T, N> to *const dyn Trait<'b, U, M> requires 'a == 'b, T == U and N == M, where T/U are type parameters, and N/M are const parameters
Why was this change made?

Casting these parts of trait objects can invalidate the VTable for the trait object, allowing dispatching to methods that should not be callable.

For points 1 and 2 (and lifetimes from point 3) the trait may have a where Self: 'a or where Self: AutoTrait bound on some of its methods. Extending the lifetime of the trait object, or adding new auto traits would result in new methods being callable which may not be present in the VTable.

For point 3, Trait<T> and Trait<U> may have entirely different VTables even in methods which exist in both VTables. Though, this does include the possibility of new methods becoming callable.

Examples
Extending lifetimes of trait objects
#![forbid(unsafe_code)]
#![feature(arbitrary_self_types, derive_coerce_pointee)]

use std::any::TypeId;
use std::marker::{CoercePointee, PhantomData};

#[derive(CoercePointee)]
#[repr(transparent)]
struct SelfPtr<T: ?Sized>(*const T);

impl<T: ?Sized> std::ops::Deref for SelfPtr<T> {
    type Target = T;
    fn deref(&self) -> &T {
        panic!("please don't call me, I just want the `Receiver` impl!");
    }
}

trait GetTypeId {
    fn get_type_id(self: SelfPtr<Self>) -> TypeId
    where
        Self: 'static;
}

impl<T: ?Sized> GetTypeId for PhantomData<T> {
    fn get_type_id(self: SelfPtr<Self>) -> TypeId
    where
        Self: 'static,
    {
        TypeId::of::<T>()
    }
}

// no `T: 'static` bound necessary
fn type_id_of<T: ?Sized>() -> TypeId {
    let ptr = SelfPtr(
        // This line no longer compiles
        &PhantomData::<T> as *const (dyn GetTypeId + '_) as *const (dyn GetTypeId + 'static),
    );
    ptr.get_type_id()
}
Introducing new auto-traits to a trait object
#![feature(arbitrary_self_types)]
trait Trait {
    fn f(self: *const Self)
    where
        Self: Send;
}

impl Trait for *const () {
    fn f(self: *const Self) {
        unreachable!()
    }
}

fn main() {
    let unsend: *const () = &();
    let unsend: *const dyn Trait = &unsend;
    let send_bad: *const (dyn Trait + Send) = unsend as _;
    send_bad.f(); // this crashes, since vtable for `*const ()` does not have an entry for `f`
    //~^ warning: adding an auto trait `Send` to a trait object in a pointer cast may cause UB later on
}
Migrations
Extending lifetimes of trait objects

Existing code can be migrated by replacing the offending raw pointer cast with a transmute. See metrics-rs/metrics#564 as an example of how such a migration can be accomplished. It's advised to only do so if actually sure that extending the lifetime of the trait object is sound.

Introducing new auto-traits to a trait object

If your usage is sound (e.g. because the trait doesn't have auto trait bounds), you can replace cast with a transmute to suppress the error:

trait Cat {}
impl Cat for *const () {}

fn main() {
    let unsend: *const () = &();
    let unsend: *const dyn Cat = &unsend;
    let _send: *const (dyn Cat + Send) = unsafe {
        // Safety:
        // - Both types are pointers, to the same trait object (and thus have the same vtable)
        // - `Cat` does not have methods with `Send` bounds
        std::mem::transmute::<*const dyn Cat, *const (dyn Cat + Send)>(unsend)
    };
    // meow
}
Related Links

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read this issue first, then the linked changes #136776, #136702, PRs #120248 and #136764, and the Rust Reference PR #1951. Identify a specific missing explanation or migration case; done means the tracking page clearly explains the affected cast, soundness rationale, and migration guidance.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, documentation
Issue type
Documentation
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.