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

Implementation is technically unsound #13

Open
kotauskas opened this issue Jun 1, 2021 · 11 comments
Open

Implementation is technically unsound #13

kotauskas opened this issue Jun 1, 2021 · 11 comments

Comments

@kotauskas
Copy link

I'm talking about lines 167 to 173 of lib.rs:

#[repr(C)]
#[derive(Copy, Clone)]
#[doc(hidden)]
pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}

The macro expansion then goes on to use this declaration like this:

pub unsafe fn downcast_ref_unchecked<T: $trait_>(&self) -> &T {
    let trait_object: $crate::TraitObject = ::$core::mem::transmute(self);
    ::$core::mem::transmute(trait_object.data)
}

This code makes an assumption that the memory layout of &dyn Trait for any trait Trait is the layout of TraitObject, which isn't a real guarantee that the Rust compiler is required to abide. Using the same TraitObject struct from core::raw would require nightly, but break compilation when this assumption would be broken because of a change to how trait object references are stored, averting possible undefined behavior by stopping compilation of the code.

The only sound alternative to this right now is to depend on a nightly compiler, make use of the unstable Pointee trait and await its stabilization, thus removing the nightly requirement.

Keep in mind that this is a breaking change. Code which depends on the 0.2.x line and is expected to be compiled with the stable compiler would break if mopa starts requiring nightly on that release line. This fix must therefore be deployed as 0.3.0 or, preferably, 1.0.0-rc1 (since Pointee is not stable yet and its fate is not absolutely guaranteed). The previous 0.2.x and 0.1.x release lines may then be yanked.

@danielhenrymantilla
Copy link

The only sound alternative to this right now is to depend on a nightly compiler, make use of the unstable Pointee trait and await its stabilization, thus removing the nightly requirement.

Not really necessary, since one can get the data pointer out of a &dyn … ("thin the pointer") by casting raw pointers:

pub unsafe fn downcast_ref_unchecked<T : $trait_>(&self) -> &T {
    &*(self as *const _ as *const T)
}

And this has already been done in this repo: it was done 5 years ago! 2bb823b

But the crate seems to be unmaintained, and that commit never made it to crates.io

@chetaldrich
Copy link

GHSA-2gxj-qrp2-53jv Just linking the relevant CVE here.

@Cypher1
Copy link

Cypher1 commented Jul 8, 2022

Any chance this can be fixed?
It seems that shred, used by specs depends on this

@kotauskas
Copy link
Author

@Cypher1, perhaps shred could specifically pull in the fixed version like this:

[patch.crates-io]
mopa = { git = "https://github.com/chris-morgan/mopa", rev = "de0066e" }

@danielhenrymantilla
Copy link

danielhenrymantilla commented Jul 8, 2022

Sadly a patch section only works for the downstream user, so it would require that specs use it when testing, and that further downstream users do the same, etc.

I've decided to go and publish https://crates.io/crates/mopa-maintained to palliate that, using my own otherwise-identical fork: master...danielhenrymantilla:mopa:v0.2.3

The fix is then to do:

- mopa = "0.2.2"
+ mopa-maintained = "0.2.3"

@Cypher1
Copy link

Cypher1 commented Jul 8, 2022 via email

torkleyy added a commit to amethyst/shred that referenced this issue Jul 10, 2022
- The previous implementation uses `mopa`, which has a known safety
issue: chris-morgan/mopa#13
- This commit essentially inlines the code generated by
the `mopafy!` macro provided by `mopa`, thus removing the
dependency
- Since `mopa::Any` was part of the public interface of the `Resource`
trait, this is technically a breaking change

Fixes #217
@Cypher1
Copy link

Cypher1 commented Jul 11, 2022

Just closing the loop:
@torkleyy removed shred/specs/amethyst dependency on this crate as a fix for this issue and the crate not being maintained.
This fixes my usage as well :)

Thanks to @torkleyy

@shinmao
Copy link

shinmao commented Sep 20, 2023

Not really necessary, since one can get the data pointer out of a &dyn … ("thin the pointer") by casting raw pointers:

pub unsafe fn downcast_ref_unchecked<T : $trait_>(&self) -> &T {
    &*(self as *const _ as *const T)
}

@danielhenrymantilla sorry to ping this response after 2 years :) I just felt curious why casting can solve the problem here? As I know, the raw pointer to trait object is still fat pointer. Isn't it still unsound to cast a fat pointer?

@danielhenrymantilla
Copy link

@shinmao if using as then by design it is not unsound: a common misconception is that as is just a non-unsafe transmute for certain cases, but as can actually perform conversion operations (in the pointer to usize case, it can "expose the provenance", in the unsizing case, it can embed the necessary metadata so as to get a wide pointer, and when the input is a wide pointer and the output is a thin (data) pointer), the as cast then becomes a legal "deünsizing"/wide-pointer-truncating operation:

fn as_<T : ?Sized>(p: *const T) -> *const () {
    p as _
}

which is guaranteed to extract the data pointer off the *const (impl !Sized) wide pointer, thanks to language semantics. That is, even if, say, the data pointer were laid out after the vtable reference, then this as cast would be guaranteed to extract the data pointer at that offset.

  • (the operation which cannot be done in current stable Rust (1.72.0)) is the metadata-extracting one: ::core::ptr::metadata())

@shinmao
Copy link

shinmao commented Sep 21, 2023

@danielhenrymantilla thanks for the answer. So the difference with transmute is that transmute will just copy the value at the memory address without metadata?

@danielhenrymantilla
Copy link

danielhenrymantilla commented Sep 21, 2023

transmute (or transmute_copy() or ptr.cast().read()) is a very precise operation which takes a value (and a size), and reinterprets that span of bytes.

We could imagine having:

//! Since the layout of wide pointers is unspecified, neither
//! `option_a` nor `option_b` are guaranteed to be sound!

fn option_a(p: *const dyn Trait) -> *const () {
    let at_p = <*const _>::cast::<u8>(&p);
    const OFFSET_TO_DATA_PTR: usize = 0; // ???
    unsafe { at_p.add(OFFSET_TO_DATA_PTR).cast().read() }
}

fn option_b(p: *const dyn Trait) -> *const () {
    let at_p = <*const _>::cast::<u8>(&p);
    const OFFSET_TO_DATA_PTR: usize = mem::size_of::<&'static VTable>(); // ???
    unsafe { at_p.add(OFFSET_TO_DATA_PTR).cast().read() }
}

with the option_a also being expressible as:

fn option_a(p: *const dyn Trait) -> *const () {
    unsafe { transmute_copy(&p) }
}

And since 0x142_u32 as u8 == 0x42_u8 == transmute_copy::<_, u8>(&0x142_u32), I was just hinting at the ideä/notion that:

  1. we could be tempted to see x as type as being kind of analogous to transmute_copy::<_, type>(&x) (papering over non-Copy cases),
  2. which in turn is similar to <*const _>::cast::<u8>(&x).add(0).cast().read()
  3. which in turn is assuming that the data pointer of a wide pointer would necessarily be at offset 0

And since this whole issue #13 is about the precise layout of wide pointers being unspecified, that would mean usage of as would not be fine. That's how I have interpreted your question anyhow.

And my point was basically that this first 1. bullet is wrong: as is truly a special (overly-)multipurpose operation, which in the case of a wide pointer magically "knows" how to extract its data pointer in a sound and robust fashion, much like, on nightly, the dedicated ::core::ptr::metadata() function is capable of doing for extracting the non-data pointer (metadata) component of a wide pointer.


[META] If you want to keep talking about this, this Github issue may be causing too much "notification noise" to people already subscribed to it; so I'd advise to go elsewhere for this kind of discussions, such as the Rust Community Discord server (https://discord.gg/rust-lang-community), in the #dark-arts channel, for instance 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants