Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pointer casts allow switching trait parameters for trait objects, which doesn’t interact soundly with trait upcasting #120222

Closed
steffahn opened this issue Jan 22, 2024 · 12 comments
Assignees
Labels
A-coercions Area: implicit and explicit `expr as Type` coercions A-trait-objects Area: trait objects, vtable layout C-bug Category: This is a bug. F-trait_upcasting `#![feature(trait_upcasting)]` I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness requires-nightly This issue requires a nightly compiler in some way. T-lang Relevant to the language team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@steffahn
Copy link
Member

steffahn commented Jan 22, 2024

Pointer casts allow switching trait parameters for trait objects, which can change the set of supertraits (and thus the vtable layout), ultimately making upcasting of raw pointers quite unsound

This code reproduces a segfault, on upcoming stable Rust, starting in 1.76 (stabilization of trait object upcasting).

Update: Stabilization was reverted, 1.76 stable Rust is good, the below code examples needs nightly Rust and #⁠[feature(trait_upcasting)] now.

#![feature(trait_upcasting)]

pub trait SupSupA {
    fn method(&self) {}
}
pub trait SupSupB {}
impl<T> SupSupA for T {}
impl<T> SupSupB for T {}

pub trait Super<T>: SupSupA + SupSupB {}

pub trait Unimplemented {}

pub trait Trait<T1, T2>: Super<T1> + Super<T2> {
    fn missing_method(&self)
    where
        T1: Unimplemented,
    {
    }
}

impl<S, T> Super<T> for S {}

impl<S, T1, T2> Trait<T1, T2> for S {}

#[inline(never)]
pub fn user1() -> &'static dyn Trait<u8, u8> {
    &()
/* VTABLE:
.L__unnamed_2:
        .quad   core::ptr::drop_in_place<()>
        .asciz  "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
        .quad   example::SupSupA::method
        .quad   .L__unnamed_4 // SupSupB vtable (pointer)
        .zero   8             // null pointer for missing_method
*/
}

#[inline(never)]
pub fn user2() -> &'static dyn Trait<u8, u16> {
    &()
/* VTABLE:
.L__unnamed_3:
        .quad   core::ptr::drop_in_place<()>
        .asciz  "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
        .quad   example::SupSupA::method
        .quad   .L__unnamed_4 // SupSupB vtable (pointer)
        .quad   .L__unnamed_5 // Super<u16> vtable (pointer)
        .zero   8             // null pointer for missing_method
*/
}

fn main() {
    let p: *const dyn Trait<u8, u8> = &();
    let p = p as *const dyn Trait<u8, u16>; // <- this is bad!
    let p = p as *const dyn Super<u16>; // <- this upcast accesses improper vtable entry
    // accessing from L__unnamed_2 the position for the 'Super<u16> vtable (pointer)',
    // thus reading 'null pointer for missing_method'
 
    let p = p as *const dyn SupSupB; // <- this upcast dereferences (null) pointer from that entry
    // to read the SupSupB vtable (pointer)
    
    // SEGFAULT

    println!("{:?}", p);
}

(playground, compiler explorer)

This issue exists next to #120217, but here I’ve found how the issue of overly liberal casting of raw pointers can be made into a concrete soundness issue, using no feature flags, producing an actual segfault.

Whether or now we need (to keep) two separate & somewhat similar GitHub issues can probably become clear eventually. If we don’t, we can always close one or the other.


Miri will already complain with a less sophisticated setup:

#![feature(trait_upcasting)]

trait Trait<T>: Super {}
trait Super {}

impl<S, T> Trait<T> for S {}
impl<S> Super for S {}

fn main() {
    let p: *const dyn Trait<u8> = &();
    let p = p as *const dyn Trait<u16>;
    let _p = p as *const dyn Super; // this is where miri complains already
}

(playground)

error: Undefined Behavior: upcast on a pointer whose vtable does not match its type
  --> src/main.rs:12:14
   |
12 |     let _p = p as *const dyn Super; // this is where miri complains already
   |              ^ upcast on a pointer whose vtable does not match its type
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: BACKTRACE:
   = note: inside `main` at src/main.rs:12:14: 12:15

Three possible ways of fixing this would be to

  • either make vtable layout even more regular,
    • (and to officially allow somewhat “wrong” vtable types, also for miri, and to kill raw pointer[-like] types as arbitrary_self_types receiver…)1
  • or to start working on limiting those pointer casts,
  • or to disallow upcasting raw pointers, after all.
    • (perhaps also temporarily as a quick fix before pursuing a better approach)

I’m marking as regression in beta as this is newly introduced unsoundness; and an approach like temporarily removing raw pointers again from our upcasting feature (assuming that’s technically possible in the first place) and/or delaying the stabilization are options, in case this soundness issue is perceived as sufficiently relevant not to be “ignored”.

@rustbot label +regression-from-stable-to-beta +F-trait_upcasting +A-coercions +A-trait-objects +I-unsound

Footnotes

  1. my thought here is that e.g. it seems like missing methods are replaced with zeroes already, too, instead of skipped, and that pattern could be extended so that in the case of different sets of supertraits (like the Super<u8> == Super<u8> + Super<u8> vs. Super<u8> + Super<u16> above) could thus perhaps also avoid supertrait pointers appearing in inconsistent places between an applied trait constructor [Trait<Args1…> vs Traits<Args2…>] with different parameters

@steffahn steffahn added the C-bug Category: This is a bug. label Jan 22, 2024
@rustbot rustbot added needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. A-coercions Area: implicit and explicit `expr as Type` coercions A-trait-objects Area: trait objects, vtable layout F-trait_upcasting `#![feature(trait_upcasting)]` I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness regression-from-stable-to-beta Performance or correctness regression from stable to beta. I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Jan 22, 2024
@steffahn steffahn changed the title Pointer casts allow switching trait parameters for trait objects, which can change the set of supertraits (and thus the vtable layout), ultimately making upcasting of raw pointers quite unsound Pointer casts allow switching trait parameters for trait objects, which doesn’t interact soundly with trait upcasting Jan 22, 2024
@saethlin saethlin removed the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Jan 22, 2024
@steffahn
Copy link
Member Author

Between T-lang, T-compiler, and T-types [all 3 listed on the tracking issue], I’m not sure which (or which ones) to assign.

@compiler-errors compiler-errors added the T-types Relevant to the types team, which will review and decide on the PR/issue. label Jan 22, 2024
@Noratrieb
Copy link
Member

P-critical, I do not want to stabilize a feature involving a new unsoundness

lcnr on Zulip

@Noratrieb Noratrieb added P-critical Critical priority and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Jan 22, 2024
@RalfJung
Copy link
Member

Yes, we should get a de-stabilization PR ASAP to avoid shipping this, and then assess the situation. Is someone working on that?

This seems like @rust-lang/lang @rust-lang/types to me; there's no bug in the compiler that I can see.

@lcnr lcnr added the I-types-nominated Nominated for discussion during a types team meeting. label Jan 22, 2024
@RalfJung
Copy link
Member

There's now a revert up at #120233.

@WaffleLapkin
Copy link
Member

WaffleLapkin commented Jan 22, 2024

I think the bug is that we allow casting *const dyn Trait<u8> as *const dyn Trait<u16> (where Trait<u16> is not supertrait of Trait<u8>). Those are different traits, and we generally disallow casting pointers to trait objects with different traits:

trait A {} impl<T> A for T {}
trait B {} impl<T> B for T {}

let a: *const dyn A = &();
let b: *const dyn B = a as _; //~ error: casting `*const dyn A` as `*const dyn B` is invalid

trait Trait<G> {}
struct X; impl<T> Trait<X> for T {}
struct Y; impl<T> Trait<Y> for T {}

let x: *const dyn Trait<X> = &();
let y: *const dyn Trait<Y> = x as _; // ok???

(play)


The check is here:

// vtable kinds must match
if fcx.tcx.erase_regions(cast_kind) == fcx.tcx.erase_regions(expr_kind) {
Ok(CastKind::PtrPtrCast)
} else {
Err(CastError::DifferingKinds)
}

Note that we only compare trait def_ids:
ty::Dynamic(tty, _, ty::Dyn) => Some(PointerKind::VTable(tty.principal_def_id())),

I wander how much breakage will cause the change to this... (upd: I'm working on trying this out)

@lcnr
Copy link
Contributor

lcnr commented Jan 22, 2024

  • either make vtable layout even more regular,
  • (and to officially allow somewhat “wrong” vtable types, also for miri, and to kill raw pointer[-like] types as arbitrary_self_types receiver…)[^1]

I believe this to be fairly straightforward by computing the vtable layout once for a fully generic princial, e.g. reusing the same vtable layout for all instances of for<T, U> dyn Foo<T, U>, regardless of the actual arguments.

I still think we should revert and also try @WaffleLapkin's approach. 2 weeks is not enough time to play with any non-trivial fixes 🤔

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 22, 2024
…abilization, r=lcnr

Revert stabilization of trait_upcasting feature

Reverts rust-lang#118133

This reverts commit 6d2b84b, reversing changes made to 73bc121.

The feature has a soundness bug:

* rust-lang#120222

It is unclear to me whether we'll actually want to destabilize, but I thought it was still prudent to open the PR for easy destabilization once we get there.
bors added a commit to rust-lang-ci/rust that referenced this issue Jan 22, 2024
…r=<try>

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang#120222 and rust-lang#120217.

cc `@oli-obk` `@compiler-errors` `@lcnr`
@traviscross traviscross added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Jan 22, 2024
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 22, 2024
…abilization, r=lcnr

Revert stabilization of trait_upcasting feature

Reverts rust-lang#118133

This reverts commit 6d2b84b, reversing changes made to 73bc121.

The feature has a soundness bug:

* rust-lang#120222

It is unclear to me whether we'll actually want to destabilize, but I thought it was still prudent to open the PR for easy destabilization once we get there.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jan 23, 2024
Rollup merge of rust-lang#120233 - oli-obk:revert_trait_obj_upcast_stabilization, r=lcnr

Revert stabilization of trait_upcasting feature

Reverts rust-lang#118133

This reverts commit 6d2b84b, reversing changes made to 73bc121.

The feature has a soundness bug:

* rust-lang#120222

It is unclear to me whether we'll actually want to destabilize, but I thought it was still prudent to open the PR for easy destabilization once we get there.
@wesleywiser wesleywiser added requires-nightly This issue requires a nightly compiler in some way. and removed regression-from-stable-to-beta Performance or correctness regression from stable to beta. labels Jan 25, 2024
@wesleywiser
Copy link
Member

Adjusting labels to reflect the reverted stabilization of the feature.

@steffahn
Copy link
Member Author

And probably P-critical prioritization can go away eventually.

@lcnr
Copy link
Contributor

lcnr commented Feb 5, 2024

dropping the t-types nomination. the stabilization has been reverted and we now have people working on this. While types team members may still be involved, this does not need any team input rn

@lcnr lcnr removed the I-types-nominated Nominated for discussion during a types team meeting. label Feb 5, 2024
bors added a commit to rust-lang-ci/rust that referenced this issue Feb 13, 2024
…r=<try>

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang#120222 and rust-lang#120217.

This is done by adding restrictions on casting pointers to trait objects. Currently the restriction is
> When casting `*const X<dyn A>` -> `*const Y<dyn B>`, if `B` has a principal trait, `dyn A + 'erased: Unsize<dyn B + 'erased>` must hold

This ensures that
1. Principal trait's generic arguments match (no `*const dyn Tr<A>` -> `*const dyn Tr<B>` casts, which are a problem for [rust-lang#120222](rust-lang#120222))
2. Principal trait's lifetime arguments match (no `*const dyn Tr<'a>` -> `*const dyn Tr<'b>` casts, which are a problem for [rust-lang#120217](rust-lang#120217))
3. No auto traits can be _added_ (this is a problem for arbitrary self types, see [this comment](rust-lang#120248 (comment)))

Some notes:
 - We only care about the metadata/last field, so you can still cast `*const dyn T` to `*const WithHeader<dyn T>`, etc
- The lifetime of the trait object itself (`dyn A + 'lt`) is not checked, so you can still cast `*mut FnOnce() + '_` to `*mut FnOnce() + 'static`, etc
  - This feels fishy, but I couldn't come up with a reason it must be checked
- The new checks are only done if `B` has a principal, so you can still do any kinds of cast, if the target only has auto traits
  - This is because auto traits are not enough to get unsoundness issues that this PR fixes
  - ...and so it makes sense to minimize breakage

The plan is to, ~~once the checks are properly implemented~~, run crate to determine how much damage does this do.

The diagnostics are currently not great, to say the least, but as far as I can tell this correctly fixes the issues.

cc `@oli-obk` `@compiler-errors` `@lcnr`
@lcnr lcnr moved this to unblocked in T-types unsound issues May 17, 2024
@VorpalBlade
Copy link

I ran into something where trait upcasting would be the perfect fit. As such I went to look for what was blocking in the tracking PR. It seems that this is the only real blocker? If so, what is the current progress on this (if any)?

@oli-obk
Copy link
Contributor

oli-obk commented Jul 1, 2024

#120248 is making progress on this

cc @WaffleLapkin

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jul 8, 2024
…, r=compiler-errors,oli-obk,lnicola

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang#120222 and rust-lang#120217.

This is done by adding restrictions on casting pointers to trait objects.

Before this PR the rules were as follows:

> When casting `*const X<dyn A>` -> `*const Y<dyn B>`, principal traits in `A` and `B` must refer to the same trait definition (or no trait).

With this PR the rules are changed to

> When casting `*const X<dyn Src>` -> `*const Y<dyn Dst>`
> - if `Dst` has a principal trait `DstP`,
>   - `Src` must have a principal trait `SrcP`
>   - `dyn SrcP` and `dyn DstP` must be the same type (modulo the trait object lifetime, `dyn T+'a` -> `dyn T+'b` is allowed)
>   - Auto traits in `Dst` must be a subset of auto traits in `Src`
>     - Not adhering to this is currently a FCW (warn-by-default + `FutureReleaseErrorReportInDeps`), instead of an error
> - if `Src` has a principal trait `Dst` must as well
>   - this restriction will be removed in a follow up PR

This ensures that
1. Principal trait's generic arguments match (no `*const dyn Tr<A>` -> `*const dyn Tr<B>` casts, which are a problem for [rust-lang#120222](rust-lang#120222))
2. Principal trait's lifetime arguments match (no `*const dyn Tr<'a>` -> `*const dyn Tr<'b>` casts, which are a problem for [rust-lang#120217](rust-lang#120217))
3. No auto traits can be _added_ (this is a problem for arbitrary self types, see [this comment](rust-lang#120248 (comment)))

Some notes:
 - We only care about the metadata/last field, so you can still cast `*const dyn T` to `*const WithHeader<dyn T>`, etc
- The lifetime of the trait object itself (`dyn A + 'lt`) is not checked, so you can still cast `*mut FnOnce() + '_` to `*mut FnOnce() + 'static`, etc
  - This feels fishy, but I couldn't come up with a reason it must be checked

The diagnostics are currently not great, to say the least, but as far as I can tell this correctly fixes the issues.

cc `@oli-obk` `@compiler-errors` `@lcnr`
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jul 8, 2024
Rollup merge of rust-lang#120248 - WaffleLapkin:bonk-ptr-object-casts, r=compiler-errors,oli-obk,lnicola

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang#120222 and rust-lang#120217.

This is done by adding restrictions on casting pointers to trait objects.

Before this PR the rules were as follows:

> When casting `*const X<dyn A>` -> `*const Y<dyn B>`, principal traits in `A` and `B` must refer to the same trait definition (or no trait).

With this PR the rules are changed to

> When casting `*const X<dyn Src>` -> `*const Y<dyn Dst>`
> - if `Dst` has a principal trait `DstP`,
>   - `Src` must have a principal trait `SrcP`
>   - `dyn SrcP` and `dyn DstP` must be the same type (modulo the trait object lifetime, `dyn T+'a` -> `dyn T+'b` is allowed)
>   - Auto traits in `Dst` must be a subset of auto traits in `Src`
>     - Not adhering to this is currently a FCW (warn-by-default + `FutureReleaseErrorReportInDeps`), instead of an error
> - if `Src` has a principal trait `Dst` must as well
>   - this restriction will be removed in a follow up PR

This ensures that
1. Principal trait's generic arguments match (no `*const dyn Tr<A>` -> `*const dyn Tr<B>` casts, which are a problem for [rust-lang#120222](rust-lang#120222))
2. Principal trait's lifetime arguments match (no `*const dyn Tr<'a>` -> `*const dyn Tr<'b>` casts, which are a problem for [rust-lang#120217](rust-lang#120217))
3. No auto traits can be _added_ (this is a problem for arbitrary self types, see [this comment](rust-lang#120248 (comment)))

Some notes:
 - We only care about the metadata/last field, so you can still cast `*const dyn T` to `*const WithHeader<dyn T>`, etc
- The lifetime of the trait object itself (`dyn A + 'lt`) is not checked, so you can still cast `*mut FnOnce() + '_` to `*mut FnOnce() + 'static`, etc
  - This feels fishy, but I couldn't come up with a reason it must be checked

The diagnostics are currently not great, to say the least, but as far as I can tell this correctly fixes the issues.

cc `@oli-obk` `@compiler-errors` `@lcnr`
@traviscross
Copy link
Contributor

This is believed to be fixed by:

github-actions bot pushed a commit to rust-lang/miri that referenced this issue Jul 9, 2024
…ler-errors,oli-obk,lnicola

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang/rust#120222 and rust-lang/rust#120217.

This is done by adding restrictions on casting pointers to trait objects.

Before this PR the rules were as follows:

> When casting `*const X<dyn A>` -> `*const Y<dyn B>`, principal traits in `A` and `B` must refer to the same trait definition (or no trait).

With this PR the rules are changed to

> When casting `*const X<dyn Src>` -> `*const Y<dyn Dst>`
> - if `Dst` has a principal trait `DstP`,
>   - `Src` must have a principal trait `SrcP`
>   - `dyn SrcP` and `dyn DstP` must be the same type (modulo the trait object lifetime, `dyn T+'a` -> `dyn T+'b` is allowed)
>   - Auto traits in `Dst` must be a subset of auto traits in `Src`
>     - Not adhering to this is currently a FCW (warn-by-default + `FutureReleaseErrorReportInDeps`), instead of an error
> - if `Src` has a principal trait `Dst` must as well
>   - this restriction will be removed in a follow up PR

This ensures that
1. Principal trait's generic arguments match (no `*const dyn Tr<A>` -> `*const dyn Tr<B>` casts, which are a problem for [#120222](rust-lang/rust#120222))
2. Principal trait's lifetime arguments match (no `*const dyn Tr<'a>` -> `*const dyn Tr<'b>` casts, which are a problem for [#120217](rust-lang/rust#120217))
3. No auto traits can be _added_ (this is a problem for arbitrary self types, see [this comment](rust-lang/rust#120248 (comment)))

Some notes:
 - We only care about the metadata/last field, so you can still cast `*const dyn T` to `*const WithHeader<dyn T>`, etc
- The lifetime of the trait object itself (`dyn A + 'lt`) is not checked, so you can still cast `*mut FnOnce() + '_` to `*mut FnOnce() + 'static`, etc
  - This feels fishy, but I couldn't come up with a reason it must be checked

The diagnostics are currently not great, to say the least, but as far as I can tell this correctly fixes the issues.

cc `@oli-obk` `@compiler-errors` `@lcnr`
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Jul 11, 2024
…ler-errors,oli-obk,lnicola

Make casts of pointers to trait objects stricter

This is an attempt to `fix` rust-lang/rust#120222 and rust-lang/rust#120217.

This is done by adding restrictions on casting pointers to trait objects.

Before this PR the rules were as follows:

> When casting `*const X<dyn A>` -> `*const Y<dyn B>`, principal traits in `A` and `B` must refer to the same trait definition (or no trait).

With this PR the rules are changed to

> When casting `*const X<dyn Src>` -> `*const Y<dyn Dst>`
> - if `Dst` has a principal trait `DstP`,
>   - `Src` must have a principal trait `SrcP`
>   - `dyn SrcP` and `dyn DstP` must be the same type (modulo the trait object lifetime, `dyn T+'a` -> `dyn T+'b` is allowed)
>   - Auto traits in `Dst` must be a subset of auto traits in `Src`
>     - Not adhering to this is currently a FCW (warn-by-default + `FutureReleaseErrorReportInDeps`), instead of an error
> - if `Src` has a principal trait `Dst` must as well
>   - this restriction will be removed in a follow up PR

This ensures that
1. Principal trait's generic arguments match (no `*const dyn Tr<A>` -> `*const dyn Tr<B>` casts, which are a problem for [#120222](rust-lang/rust#120222))
2. Principal trait's lifetime arguments match (no `*const dyn Tr<'a>` -> `*const dyn Tr<'b>` casts, which are a problem for [#120217](rust-lang/rust#120217))
3. No auto traits can be _added_ (this is a problem for arbitrary self types, see [this comment](rust-lang/rust#120248 (comment)))

Some notes:
 - We only care about the metadata/last field, so you can still cast `*const dyn T` to `*const WithHeader<dyn T>`, etc
- The lifetime of the trait object itself (`dyn A + 'lt`) is not checked, so you can still cast `*mut FnOnce() + '_` to `*mut FnOnce() + 'static`, etc
  - This feels fishy, but I couldn't come up with a reason it must be checked

The diagnostics are currently not great, to say the least, but as far as I can tell this correctly fixes the issues.

cc `@oli-obk` `@compiler-errors` `@lcnr`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-coercions Area: implicit and explicit `expr as Type` coercions A-trait-objects Area: trait objects, vtable layout C-bug Category: This is a bug. F-trait_upcasting `#![feature(trait_upcasting)]` I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness requires-nightly This issue requires a nightly compiler in some way. T-lang Relevant to the language team, which will review and decide on the PR/issue. T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests