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

Ensure that #[repr(packed)] cannot be used on #[pin_projectable] structs #34

Merged
merged 6 commits into from
Aug 10, 2019

Conversation

Aaron1011
Copy link
Collaborator

@Aaron1011 Aaron1011 commented Aug 8, 2019

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:

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.

@Aaron1011
Copy link
Collaborator Author

cc @RalfJung: I've not 100% sure that my reasoning about 'no-op #[repr(packed)] structs' is correct. Could the compiler decide to copy an aligment-1 field, even though it's never necessary for proper alignment? Are there other ways that #[repr(packed)] can cause undefined behavior in conjunction with pin projections?

@Aaron1011
Copy link
Collaborator Author

It looks like CI started using a newer version of Clippy, which is causing lints to trigger for code that was passing before.

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.
This lint is triggering on existing code, completely unrelated to this
PR
@RalfJung
Copy link
Contributor

RalfJung commented Aug 8, 2019

AFAIK currently the compiler both makes taking a reference to a 1-aligned packed field unsafe and moves them for Drop. So you are good. But you should be able to test for this using trybuild: just have a packed struct with a few 1-aligned fields, try to take references to them, and test that you get diagnostics.

If you want to be really sure, you could even add something like that as a ui compile-fail test to rustc. Then add a comment in that test explaining that if we ever start accepting this, we should also change the drop glue for packed structs to not copy 1-aligned fields. Link to this issue for why that is important.

tests/repr_packed.rs Outdated Show resolved Hide resolved
tests/repr_packed.rs Outdated Show resolved Hide resolved
tests/repr_packed.rs Outdated Show resolved Hide resolved
@RalfJung
Copy link
Contributor

RalfJung commented Aug 8, 2019

I've added a test to
verify that the compiler does not copy the fields of such a struct when
it is dropped.

Wait and this passes? That's odd. I will have to find the code in rustc that does the moving, I don't recall an exception for already "sufficiently aligned" fields. But that would be good news for you. I guess.

@RalfJung
Copy link
Contributor

RalfJung commented Aug 8, 2019

Found it. And this is not very recent. Interesting.

Given this, the compiler could also stop moving things entirely e.g. in a packed(2) struct if the first two fields have size and alignment 1, and then the third field has alignment 2 -- that one is also sufficiently aligned without padding. Currently it looks like 1 is special-cased, but I don't think there is a fundamental reason for that. So, the real situation with repr(packed) is that the compiler might move fields of packed structs -- it will move them if they are not sufficiently aligned, but might also move them unnecessarily if it is not smart enough. I will update the pinning docs.

I am not aware of a similar exception for the error (and it would be much harder there in general as that's pre-monomorphization). But better add some tests as described above.

Centril added a commit to Centril/rust that referenced this pull request Aug 8, 2019
move of packed fields might or might not occur when they actually are sufficiently aligned

See taiki-e/pin-project#34, where it was pointed out that we actually don't move fields of 1-aligned types when dropping a packed struct -- but e.g. in a `packed(2)` struct, we don't do something similar for 2-aligned types. The code for that is [here](https://github.com/rust-lang/rust/blob/db7c773a6be2f050d1d1504763819ea3916f5428/src/librustc_mir/util/alignment.rs#L7).
Centril added a commit to Centril/rust that referenced this pull request Aug 8, 2019
move of packed fields might or might not occur when they actually are sufficiently aligned

See taiki-e/pin-project#34, where it was pointed out that we actually don't move fields of 1-aligned types when dropping a packed struct -- but e.g. in a `packed(2)` struct, we don't do something similar for 2-aligned types. The code for that is [here](https://github.com/rust-lang/rust/blob/db7c773a6be2f050d1d1504763819ea3916f5428/src/librustc_mir/util/alignment.rs#L7).
@Aaron1011
Copy link
Collaborator Author

Would it make sense for Rust to have an officially supported way of checking for #[repr(packed)] structs? Maybe something like a 'magic' NotReprPacked trait, which is only implemented for structs which are not #[repr(packed)]?

One one hand, this is a very niche use case. On the other hand, it's not currently possible to build a completely safe pin projection abstraction, despite the fact that the compiler has all the information that it needs to allow us to do so.

@RalfJung
Copy link
Contributor

RalfJung commented Aug 9, 2019

Maybe? It seems worth asking by putting some pre-RFC onto internals.

it's not currently possible to build a completely safe pin projection abstraction

What is wrong with your idea of having a "test function" that tries to take references?

@Aaron1011
Copy link
Collaborator Author

Aaron1011 commented Aug 9, 2019

What is wrong with your idea of having a "test function" that tries to take references?

I have a few concerns with my current implementation:

  • AFAICT, the compiler doesn't actually guarantee that it won't move the fields of an alignment-1 #[repr(packed)] struct. While this is the current behavior, I don't think it would be considered a breaking change for the compiler to do something different in the future.
  • I'm not 100% certain that this is correct, even given the current behavior of the compiler. Are there any circumstances where #[repr(packed)] are treated specially (in particular, differently than alignment-1 types)?
  • The pin module docs (even with your change) still state that #[repr(packed)] should not be used with pin projections - and we are (under certain circumstances) doing just that. Can pin projections on #[repr(packed)] structs cause UB in ways that don't involve the Drop impl?

Having a NotReprPacked trait would avoid all of these questions, as we could be certain that we're not dealing with #[repr(packed)] at all.

@RalfJung
Copy link
Contributor

AFAICT, the compiler doesn't actually guarantee that it won't move the fields of an alignment-1 #[repr(packed)] struct. While this is the current behavior, I don't think it would be considered a breaking change for the compiler to do something different in the future.

You could ask the compiler team. IMO it would be reasonable to demand that for any field that will be moved by the drop glue, there also will be an error when trying to create a reference. Basically, the error shouldn't be smarter than the drop glue.

Can pin projections on #[repr(packed)] structs cause UB in ways that don't involve the Drop impl?

I don't think so. That text was added because of the implicit move, and I was not aware that there are exceptions to the move for 1-aligned types.

repr(packed) is sort of a red herring here -- the property we care about is "don't move", and repr(packed) induces moves, and that's why we have to care about it.

Copy link
Owner

@taiki-e taiki-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is a better solution at this time. Thanks to both of you for the investigation and the fixes.

pin-project-internal/src/pin_projectable/mod.rs Outdated Show resolved Hide resolved
@taiki-e
Copy link
Owner

taiki-e commented Aug 10, 2019

bors r+

bors bot added a commit that referenced this pull request 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 <[email protected]>
Co-authored-by: Taiki Endo <[email protected]>
@taiki-e
Copy link
Owner

taiki-e commented Aug 10, 2019

(I applied with those suggestions myself.)

@bors
Copy link
Contributor

bors bot commented Aug 10, 2019

Build succeeded

  • taiki-e.pin-project

@bors bors bot merged commit 8f6d7fd into taiki-e:master Aug 10, 2019
@Aaron1011 Aaron1011 deleted the fix/packed-bypass branch August 10, 2019 17:04
@taiki-e taiki-e added this to the v0.4 milestone Aug 10, 2019
@taiki-e taiki-e mentioned this pull request Aug 11, 2019
11 tasks
bors bot added a commit that referenced this pull request Sep 10, 2019
83: Improve error message for #[repr(packed(N))] r=taiki-e a=taiki-e

#34 can [already detect this](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=7331036de27faeb8354e367ef514c6c6), but this will generate better error messages.

This also adds some tests related to `#[repr(packed)]`.

Co-authored-by: Taiki Endo <[email protected]>
@taiki-e taiki-e added the A-pin-projection Area: #[pin_project] label Sep 25, 2019
@taiki-e
Copy link
Owner

taiki-e commented Sep 8, 2020

Seems this logic can be bypassed using --cap-lints: https://godbolt.org/z/jEf1fG

Some crates may inadvertently rely on memory safety through lints, or otherwise very much not want lints to be turned off. For example if modifications to a new lint to generate more warnings caused an upstream dependency to fail to compile, it could represent a serious bug indicating the dependency needs to be updated. This system would paper over this issue by forcing compilation to succeed. This use case seems relatively rare, however, and lints are also perhaps not the best method to ensure the safety of a crate.

pin-project v0.4.3 or later (#135, btw, v0.4.0-0.4.2 are already yanked for another reason) is internally proc-macro-derive, so they are not affected by the problem that the struct definition is rewritten by another macro after the #[pin_project] is expanded (it is the underlying cause of the problem this PR fixed).
So even if this PR's logic doesn't work, the current pin-project can properly reject repr(packed) type.

However, pin-project-lite is affected by this issue. The current pin-project-lite generates almost the same code as pin-project, but does not have the equivalent of the repr(packed) checking that proc-macro(pin-project) does during expansion. (Since the proc-macro-attribute used on the struct expand later than declarative macro used for the struct, a complete fix for the problem is probably impossible until safe_packed_borrow gets a hard error.)

cc taiki-e/pin-project-lite#26

bors bot added a commit to taiki-e/pin-project-lite that referenced this pull request Mar 2, 2021
55: Prepare for removal of safe_packed_borrows lint r=taiki-e a=taiki-e

pin-project-lite is using `safe_packed_borrows` lint for checking `repr(packed)` types. (See taiki-e/pin-project#34 for more)

As described in #26, lint-based tricks aren't perfect, but they're much better than nothing.

`safe_packed_borrows` is planned to be removed in favor of `unaligned_references` (rust-lang/rust#82525), so I would like to switch to `unaligned_references`. (However, actually, enable both lint as `unaligned_references` does not exist in older compilers.)

Co-authored-by: Taiki Endo <[email protected]>
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]
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants