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

Can bypass repr(packed) checking by hiding it in a proc-macro-attribute #32

Closed
Nemo157 opened this issue Aug 6, 2019 · 5 comments
Closed
Labels
A-pin-projection Area: #[pin_project] C-bug Category: related to a bug. I-unsound A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness

Comments

@Nemo157
Copy link

Nemo157 commented Aug 6, 2019

Given a proc-macro crate like this:

extern crate proc_macro;

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn hidden_repr(attr: TokenStream, item: TokenStream) -> TokenStream {
    format!("#[repr({})] {}", attr, item).parse().unwrap()
}

Using it along with pin-project in a project can cause a value to be pinned in one location and dropped in another:

use core::pin::Pin;
use hidden_repr::hidden_repr;
use pin_project::pin_projectable;

#[derive(Debug)]
struct Foo(u16);

impl Foo {
    fn foo(self: Pin<&mut Self>) {
        println!("{:?} at {:p}", self, self);
    }
}

impl Drop for Foo {
    fn drop(&mut self) {
        println!("{:?} dropped at {:p}", self, self);
    }
}

#[pin_projectable]
#[hidden_repr(packed)]
struct Bar {
    _bar: u8,
    #[pin]
    foo: Foo,
}

fn main() {
    let mut bar = Box::pin(Bar { _bar: 0, foo: Foo(0) });
    bar.as_mut().project().foo.foo();
}

output:

Foo(0) at 0x560220317a41
Foo(0) dropped at 0x7fff2b8510b6

(it appears proc-macro-attribute application order is not defined, so not guaranteed to reproduce, but this seems like the sane order for rustc to apply them in so it's likely to continue working).

@taiki-e
Copy link
Owner

taiki-e commented Aug 6, 2019

I don't think this issue can be avoided without compiler support. But, I think this should probably be mentioned in the documents.

@taiki-e
Copy link
Owner

taiki-e commented Aug 7, 2019

Refs: rust-lang/reference#578

@Nemo157
Copy link
Author

Nemo157 commented Aug 7, 2019

rust-lang/reference#578 seems to mostly be about execution order for side-effects, rust-lang/rust#63336 is more relevant as that is about the order in which multiple attributes affect the AST of a single item (but should probably be opened as a new reference issue).

Aaron1011 added a commit to Aaron1011/pin-project that referenced this issue Aug 8, 2019
As described in issue taiki-e#32,
it's possible to bypass the #[repr(packed)] check via the use of an
additional procedural macro. However, we need to be able to forbid
sound

To enforce this, we can make use of the fact that taking references to
the fields of a #[repr(packed)] struct is unsafe. Given a

fn check_Foo(val: Foo) {
    &val.field1;
    &val.field2;
    ...
    &val.fieldn;
}

If `Foo` turns out to be #[repr(packed)], the compiler will generate an
error for us, since none of the field references are in an `unsafe`
block.

Unfortunately, this check requires that at least one of the struct's
fields have an alignment greater than 1. If the struct is composed
entirely of fields of alignment 1 (e.g. 'u8'), it will be safe to take a
reference to any of the fields, preventing an error from being emiited.

However, I believe that pin projecting such a struct is actually sound.
If all fields have an alignment 1, #[repr(packed)] is effectively a
no-op, as struct will already have no padding. I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.
Aaron1011 added a commit to Aaron1011/pin-project that referenced this issue Aug 8, 2019
As described in issue taiki-e#32,
it's possible to bypass the #[repr(packed)] check via the use of an
additional procedural macro. However, we need to be able to forbid
sound

To enforce this, we can make use of the fact that taking references to
the fields of a #[repr(packed)] struct is unsafe. Given a

fn check_Foo(val: Foo) {
    &val.field1;
    &val.field2;
    ...
    &val.fieldn;
}

If `Foo` turns out to be #[repr(packed)], the compiler will generate an
error for us, since none of the field references are in an `unsafe`
block.

Unfortunately, this check requires that at least one of the struct's
fields have an alignment greater than 1. If the struct is composed
entirely of fields of alignment 1 (e.g. 'u8'), it will be safe to take a
reference to any of the fields, preventing an error from being emiited.

However, I believe that pin projecting such a struct is actually sound.
If all fields have an alignment 1, #[repr(packed)] is effectively a
no-op, as struct will already have no padding. I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.
Aaron1011 added a commit to Aaron1011/pin-project that referenced this issue Aug 8, 2019
As described in issue taiki-e#32,
it's possible to bypass the #[repr(packed)] check via the use of an
additional procedural macro. However, we need to be able to forbid
using #[repr(packed) in order to make #[pin_projectable] sound

To enforce this, we can make use of the fact that taking references to
the fields of a #[repr(packed)] struct is unsafe. Given a #[repr(packed)]
struct Foo, we can generate code like this:

```rust
fn check_Foo(val: Foo) {
    &val.field1;
    &val.field2;
    ...
    &val.fieldn;
}
```

If `Foo` turns out to be #[repr(packed)], the compiler will generate an
error for us, since none of the field references are in an `unsafe`
block.

Unfortunately, this check requires that at least one of the struct's
fields have an alignment greater than 1. If the struct is composed
entirely of fields of alignment 1 (e.g. 'u8'), it will be safe to take a
reference to any of the fields, preventing an error from being emiited.

However, I believe that pin projecting such a struct is actually sound.
If all fields have an alignment 1, #[repr(packed)] is effectively a
no-op, as struct will already have no padding. I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.
Aaron1011 added a commit to Aaron1011/pin-project that referenced this issue Aug 8, 2019
As described in issue taiki-e#32,
it's possible to bypass the #[repr(packed)] check via the use of an
additional procedural macro. However, we need to be able to forbid
using #[repr(packed) in order to make #[pin_projectable] sound

To enforce this, we can make use of the fact that taking references to
the fields of a #[repr(packed)] struct is unsafe. Given a #[repr(packed)]
struct Foo, we can generate code like this:

```rust
fn check_Foo(val: Foo) {
    &val.field1;
    &val.field2;
    ...
    &val.fieldn;
}
```

If `Foo` turns out to be #[repr(packed)], the compiler will generate an
error for us, since none of the field references are in an `unsafe`
block.

Unfortunately, this check requires that at least one of the struct's
fields have an alignment greater than 1. If the struct is composed
entirely of fields of alignment 1 (e.g. 'u8'), it will be safe to take a
reference to any of the fields, preventing an error from being emiited.

However, I believe that pin projecting such a struct is actually sound.
If all fields have an alignment 1, #[repr(packed)] is effectively a
no-op, as struct will already have no padding. I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.
bors bot added a commit that referenced this issue Aug 10, 2019
34: Ensure that #[repr(packed)] cannot be used on #[pin_projectable] structs r=taiki-e a=Aaron1011

As described in issue #32,
it's possible to bypass the #[repr(packed)] check via the use of an
additional procedural macro. However, we need to be able to forbid
using #[repr(packed) in order to make #[pin_projectable] sound

To enforce this, we can make use of the fact that taking references to
the fields of a #[repr(packed)] struct is unsafe. Given a #[repr(packed)]
struct Foo, we can generate code like this:

```rust
fn check_Foo(val: Foo) {
    &val.field1;
    &val.field2;
    ...
    &val.fieldn;
}
```

If `Foo` turns out to be #[repr(packed)], the compiler will generate an
error for us, since none of the field references are in an `unsafe`
block.

Unfortunately, this check requires that at least one of the struct's
fields have an alignment greater than 1. If the struct is composed
entirely of fields of alignment 1 (e.g. 'u8'), it will be safe to take a
reference to any of the fields, preventing an error from being emiited.

However, I believe that pin projecting such a struct is actually sound.
If all fields have an alignment 1, #[repr(packed)] is effectively a
no-op, as struct will already have no padding. I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.

Note: I've removed the tests for `#[repr(packed)]` being denied on enums - `#[repr(packed)]` isn't even allowed on enums in the first place, so there's no need to do anything.

Co-authored-by: Aaron Hill <aa1ronham@gmail.com>
Co-authored-by: Taiki Endo <te316e89@gmail.com>
@Aaron1011
Copy link
Collaborator

This should now be fixed in master

@taiki-e
Copy link
Owner

taiki-e commented Aug 10, 2019

Yes, thanks for the fix this issue @Aaron1011.

@taiki-e taiki-e closed this as completed Aug 10, 2019
@taiki-e taiki-e added C-bug Category: related to a bug. A-pin-projection Area: #[pin_project] labels Sep 23, 2019
@taiki-e taiki-e added the I-unsound A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness label Apr 21, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-pin-projection Area: #[pin_project] C-bug Category: related to a bug. I-unsound A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
Projects
None yet
Development

No branches or pull requests

3 participants