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

Tracking issue for safe_packed_borrows compatibility lint #46043

Closed
arielb1 opened this issue Nov 16, 2017 · 42 comments · Fixed by #82525
Closed

Tracking issue for safe_packed_borrows compatibility lint #46043

arielb1 opened this issue Nov 16, 2017 · 42 comments · Fixed by #82525
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-future-incompatibility Category: Future-incompatibility lints C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@arielb1
Copy link
Contributor

arielb1 commented Nov 16, 2017

What is this warning about

Fields of structs with the #[repr(packed)] attribute might be unaligned. Even if a field has a valid offset (e.g. 0) within the struct, the struct itself might be unaligned.

On the other hand, safe references (&T and &mut T) must always have valid alignment (even when they are just created) - not only will dereferencing these references cause segfaults on architectures such as Sparc, but the compiler is allowed to optimize based on that assumption (e.g. in a future version of rustc, it might use the "free" alignment bits to represent enum discriminants), so even just creating an unaligned reference is Undefined Behavior and might cause the program to behave in unexpected ways.

Therefore, borrowing a field in the interior of a packed structure with alignment other than 1 is unsafe - if the reference is not known to be aligned, the borrow must be done to an unsafe pointer.

Note that borrowing a field with alignment 1 is always safe.

For example, consider the common struct Unaligned:

#[repr(packed)]
pub struct Unaligned<T>(pub T);

That struct can be placed inside a bigger struct at an unaligned offset:

pub struct Foo {
    start: u8,
    data: Unaligned<u32>,
}

In that case, a borrow of a field of the struct would be Undefined Behavior (UB), and therefore is made unsafe:

let x = Foo { start: 0, data: Unaligned(1) };
let y = &x.data.0; // UB, also future-compatibility warning
println!("{}", x.data.0); // this borrows `x.data.0`, so UB + future-compat warning
use(y);

This used to be left unchecked by the compiler - issue #27060.

How to fix this warning/error

The most common ways to fix this warnings are:

  1. remove the #[repr(packed)] attribute if it is not actually needed. A few crates had unnecessary #[repr(packed)] annotations - for example, tendril.
  2. copy the fields to a local first. When accessing a field of a packed struct directly without using a borrow, the compiler will make sure the access is done correctly even when the field is unaligned. The then-aligned local can then be freely used. For example:
    let x = Foo { start: 0, data: Unaligned(1) };
    let temp = x.data.0;
    println!("{}", temp); // works
    // or, to the same effect, using an `{x}` block:
    println!("{}", {x.data.0}); // works
    use(y);

In some cases, it might be necessary to borrow the fields as raw pointers and use read_unaligned/write_unaligned to access them, but I haven't encountered such a case.

Derives

One annoying case where this problem can appear is if a packed struct has builtin derives, e.g.

#[derive(PartialEq, ...)]
#[repr(packed)]
pub struct Unaligned<T>(pub T);

Which essentially expands to this:

impl<T: PartialEq> PartialEq for Unaligned<T> {
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(&self.0, &other.0) // this is UB and unsafe
    }
}

If your struct already derives Copy and has no generics, the compiler will generate code that copies the fields to locals and will therefore work. Otherwise, you'll have to write the derive implementations manually.

For example, you might want to add a T: Copy bound and copy things out:

#[repr(packed)]
pub struct Unaligned<T: Copy>(pub T);

impl<T: Copy + PartialEq> PartialEq for Unaligned<T> {
    fn eq(&self, other: &Self) -> bool {
        ({self.0}) == ({other.0}) // this copies fields to temps, and works
    }
}
@arielb1 arielb1 added B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Nov 16, 2017
@arielb1 arielb1 changed the title Tracking issue for safe_packed_borrow compatibility lint Tracking issue for safe_packed_borrows compatibility lint Nov 16, 2017
CasualX added a commit to CasualX/pelite that referenced this issue Jan 5, 2018
* Remove all `#[repr(packed)]` on the IMAGE structs due to a misunderstanding of the Windows.h struct definitions:
Only the old 16-bit headers want 2 byte packing, all the others want 4 byte packing. However all of these structs end up already aligned anyway.
Safeguard their implementation by asserting their sizes.

  Fixes rust-lang/rust#46043

* Explicitly specify the raw pointer type when casting from reference and calling a method on it.

  Fixes rust-lang/rust#46906
CasualX added a commit to CasualX/pelite that referenced this issue Jan 5, 2018
* Remove all `#[repr(packed)]` on the IMAGE structs due to a misunderstanding of the Windows.h struct definitions:
Only the old 16-bit headers want 2 byte packing, all the others want 4 byte packing. However all of these structs end up already aligned anyway.
Safeguard their implementation by asserting their sizes.

  Fixes rust-lang/rust#46043

* Explicitly specify the raw pointer type when casting from reference and calling a method on it.

  Fixes rust-lang/rust#46906
ppannuto added a commit to tock/tock that referenced this issue Jan 7, 2018
rust-lang/rust#46043 makes accessing members
of packed structs unsafe. For MMIO structs, `repr(C)` alone should be
sufficient as the layout by definition has no holes.
@luser
Copy link
Contributor

luser commented Feb 24, 2018

My rust-minidump crate has a bunch of packed structs derived from a C header that I ran bindgen on. Now that I've updated to a newer rustc I'm getting a slew of E0133 warnings. The fact that you get a warning using a field from a packed struct in println! is pretty annoying, especially when the field itself is Copy! Is there any way we can make this case more ergonomic?

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 16, 2018

What's the time-line for turning this warning a hard-error on nightly, and moving towards making it a hard error on stable ? It has been a warning on nightly for a couple of releases already.

@RalfJung
Copy link
Member

RalfJung commented Apr 16, 2018

@luser
The println! likely actually takes a reference and uses it, so adding an unsafe block doesn't even fix anything -- it just makes your code compile with UB. Try wrapping the arguments in curly braces:

println!("{}", {self.packed_field});

This should work if they are Copy, should not require an unsafe block because no reference is taken, and properly avoids the UB.


However, I am worried many people do not realize what this unsafety really is about, and just add an unsafe block to silence the warning. I just read over the commit by @yodaldevoid that is linked above because they mentioned this issue (yodaldevoid/mkl26@c1c1d5c), and it contains code like (comment is mine)

impl<'a> Adc<'a> {
    pub fn is_conv_done(&mut self) -> bool {
        // self.reg has type AdcRegs which is packed; then we call RW::read(&self, u32)
        unsafe { self.reg.sc1a.read().get_bit(7) }
  }
}

So, this code is UB if these fields are actually unaligned. @yodaldevoid, it would be interesting to learn why you thought that passing an unaligned reference to RW::read is okay, or did you ensure that the reference is actually aligned despite packing? (Looking at AdcRegs, it consists entirely of u32 and i32, so repr(packed) is likely not even needed?)

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 16, 2018

@RalfJung the current warning message is pretty bad:

See it live (playground):

#[repr(packed)]
struct A {
    x: u8,
    y: u64
}

fn main() {
    let a = A { x: 1, y: 2 };
    let _ = &a.y;
}

outputs:

warning: borrow of packed field requires unsafe function or block (error E0133)
 --> src/main.rs:9:13
  |
9 |     let _ = &a.y;
  |             ^^^^
  |
  = note: #[warn(safe_packed_borrows)] on by default
  = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
  = note: for more information, see issue #46043 <https://github.com/rust-lang/rust/issues/46043>

It says that taking a reference requires an unsafe block, which encourages users to do just that. Open an unsafe block and be done with it. It should at least mention why this is unsafe, and encourage users to fix these issues with care:

warning: borrow of packed field is unsafe (error E0133)
 --> src/main.rs:9:13
  |
9 |     let _ = &a.y;
  |             ^^^^
  |
  = note: #[warn(safe_packed_borrows)] on by default
  = warning: fields of packed structs might be misaligned: dereferencing a misaligned reference is undefined behavior. 
  = note: this is warning will turn into a hard error in the next Rust 1.2x.0 release. Taking references to packed struct fields allows undefined behavior to happen in safe Rust. Fix this with great care - for more information, see issue #46043 <https://github.com/rust-lang/rust/issues/46043>

It is also "only" a warning, which encourage people that numbly pursue clean builds to just want to "silence it" without digging deeper. Also some users might conclude that, if this was important, it would be a hard error, saying that it will become an error at some undetermined point in the future doesn't really convey much seriousness.

@RalfJung
Copy link
Member

RalfJung commented Apr 16, 2018

@gnzlbg

the current warning message is pretty bad:

Yeah that probably doesn't help. However, I also feel this violates the usual pattern in Rust that you should not make unsafe operations accidentally. The only other non-function-call operations we have that are unsafe involve dereferencing a raw pointer, and everyone kind-of knows why that is unsafe (though they may forget about alignment). That the mere act of taking a reference can be UB will probably be surprising to many. If they are doing this e.g. in an unsafe fn, they will never even see the warning/error, no matter how much we improve it.

I think long-term we want to have an operation (that @arielb1 has proposed several times I think) that directly creates a raw pointer to a field, and we want to make taking a reference to a packed field outright illegal. People should use the raw pointer operations instead. However, I don't even know how the syntax for that should look like.

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 16, 2018

I think long-term we want to have an operation (that @arielb1 has proposed several times I think) that directly creates a raw pointer to a field, and we want to make taking a reference to a packed field outright illegal.

Honestly, I would be fine with making taking references to fields of packed structs illegal at first. For FFI this is not something that's really needed. I don't know how that would interact with derives but at worst they just won't compile and users that want to can manually implement these.

However, I also feel this violates the usual pattern in Rust that you should not make unsafe operations accidentally.

Sure and I agree. As you mention, this is surprising and people can slip which is why we need to be more proactive here.

@RalfJung
Copy link
Member

Honestly, I would be fine with making taking references to fields of packed structs illegal at first.

Given that this has been possible on stable for several years, I don't think that's realistic.

Though maybe we can make it illegal unless it is immediately followed by as *const _ or as *mut _?

@gnzlbg
Copy link
Contributor

gnzlbg commented Apr 16, 2018

Though maybe we can make it illegal unless it is immediately followed by as *const _ or as *mut _?

That would be an improvement.

Given that this has been possible on stable for several years, I don't think that's realistic.

Turning this warning into an error is a breaking change, and breaking changes that fix soundness bugs are "allowed". What complicates this case is that the code might actually be working fine in the platforms that the users are actually using, and that without an escape hatch users don't have a "trivally-easy" way to work around this.

@RalfJung
Copy link
Member

What complicates this case is that the code might actually be working fine in the platforms that the users are actually using,

We provide alignment annotations to LLVM irrespective of the platform. So, even on platforms where all loads are allowed to be unaligned, this kind of code only "happens to work" in the sense that the compiler was not "smart enough" to exploit the UB.

But yes, what I meant is that at the least, we need an escape hatch. We can't just make it entirely impossible to write such code.

But before we talk about a migration strategy, I think we should have some idea of what the goal is. Do we consider &x as *const _ to be a primitive operation (with its own MIR representation) that immediately creates a raw pointer? In that case, doing this to unaligned fields could even be a safe operation. That's probably pretty confusing though; let-expanding the code would then result in compiler errors or (worse) introduce UB. Or do we actually declare it legal for unsafe code to create an unaligned reference as long as they don't do much with it? But then, what are they allowed to do?

There is another case where we'd like people to use such a raw-ptr operation: Imagine we have a x: *const T which may not be aligned (because it could originate from a packed struct), and now we want to get a pointer to a field. &(*x).field would create an unaligned reference, which is a bad value and actually causes insta-UB under some proposed unsafe code guidelines. We'd rather have people write &(*x).field as *const _ (or whatever the primitive create-raw-ptr looks like) so that the bad reference is never even created.
However, I have no idea how we can enforce this. I imagine plenty of code around packed structs would be UB under this model.

(One reason we want it to be insta-UB is to allow more layout optimizations: Something like Option<Option<&i32>> could be ptr-sized if we could use the bit patterns for 0, 1, 2, 3 as discriminants. All of these are illegal for a 4-aligned pointer like &i32. But if we want to do this, we better make it illegal for there to ever be a reference that is not aligned.)

@RalfJung
Copy link
Member

In the MaybeUninit discussion, another use case for immediately creating a raw pointer came up: Creating a pointer to an uninitialized field of a union. Creating a reference is invalid because it is not yet initialized, but creating a raw pointer is fine.

@retep998
Copy link
Member

I just received this warning in winapi in case where it should not occur.

> cargo build --all-features
   Compiling winapi v0.3.4 (file:///C:/Users/Peter/Code/winapi-rs)
warning: #[derive] can't be used on a non-Copy #[repr(packed)] struct (error E0133)
   --> src\macros.rs:354:54
    |
354 |           STRUCT!{#[cfg_attr(feature = "debug", derive(Debug))] $($rest)*}
    |                                                        ^^^^^
    |
   ::: src\um\wingdi.rs:599:1
    |
599 | / STRUCT!{ #[debug] #[repr(packed)] struct BITMAPFILEHEADER {
600 | |     bfType: WORD,
601 | |     bfSize: DWORD,
602 | |     bfReserved1: WORD,
603 | |     bfReserved2: WORD,
604 | |     bfOffBits: DWORD,
605 | | }}
    | |__- in this macro invocation
    |
    = note: #[warn(safe_packed_borrows)] on by default
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #46043 <https://github.com/rust-lang/rust/issues/46043>

    Finished dev [unoptimized + debuginfo] target(s) in 33.10s

All structs defined by STRUCT! have manual Copy and Clone impls and should never trigger this warning, and yet the fact that is triggering indicates something is wrong with how the warning is implemented. Please fix this and don't you dare turn this into a hard error like this.

@RalfJung
Copy link
Member

RalfJung commented May 17, 2018

I reported a minimal example as #50826

don't you dare turn this into a hard error like this.

Calm down, it's just a bug. That's why we have these warning cycles.

@ColinFinck
Copy link
Contributor

For the record, declaring the borrowing of packed structure fields an unsafe operation makes Rust warn about getting the address of a packed structure field (like &fadt_table.x_dsdt as *const _ as usize), too.
However, getting a memory address is a totally safe operation.

Some may already conclude this from the previous comments, but this is the problem I'm currently facing.
Rust definitely needs a warning-free way for getting the address or a raw pointer to a packed structure field before this warning is turned into a hard error.

@gnzlbg
Copy link
Contributor

gnzlbg commented May 30, 2018

Can't we just add an intrinsic that gives you a pointer to a value without going through a reference? Once we have that, if we really need sugar for this, we can add this later.

@RalfJung
Copy link
Member

@retep998 certainly, long-term the lint should be entirely independent of whether there is an unsafe block or not.

Shorter-term... I suppose we could already add that lint and make it allow-by-default, would that work?
I'm afraid a warn-by-default lint is probably not going to be received well when there's no stable alternative we can point people to.

@retep998
Copy link
Member

An allow-by-default lint is perfectly fine for now. Even a clippy lint would be fine.

@RalfJung RalfJung added the C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC label May 16, 2020
@RalfJung
Copy link
Member

Lint is implemented in #72270.

Dylan-DPC-zz pushed a commit to Dylan-DPC-zz/rust that referenced this issue May 26, 2020
add a lint against references to packed fields

Creating a reference to an insufficiently aligned packed field is UB and should be disallowed, both inside and outside of `unsafe` blocks. However, currently there is no stable alternative (rust-lang#64490) so all we do right now is have a future incompatibility warning when doing this outside `unsafe` (rust-lang#46043).

This adds an allow-by-default lint. @retep998 suggested this can help early adopters avoid issues. It also means we can then do a crater run where this is deny-by-default as suggested by @joshtriplett.

I guess the main thing to bikeshed is the lint name. I am not particularly happy with "packed_references" as it sounds like the packed field has reference type. I chose this because it is similar to "safe_packed_borrows". What about "reference_to_packed" or "unaligned_reference" or so?
cptpcrd added a commit to cptpcrd/simple_libc that referenced this issue May 31, 2020
Both read directly into an &mut [libc::epoll_event]. That struct is
packed, but it can be used safely if values are copied out and not
borrowed (see rust-lang/rust#46043 for
details).
@geofft
Copy link
Contributor

geofft commented Aug 10, 2020

So, this diagnostic notices creating a reference/pointer to a misaligned field. But isn't initializing a misaligned field also UB?

#[repr(packed(2))]
struct Foo(u16, String);

let f = Foo(3, "hello!".to_owned());

seems to be accepted without complaint, and at least on x86_64, it seems to do an unaligned write.

@retep998
Copy link
Member

No, that is not UB. Rust can easily see that you're assigning a value to a misaligned location and tell LLVM to generate the correct code for it. It is only the creation of references to misaligned locations that is bad because the reference does not preserve any alignment information.

@R030t1
Copy link

R030t1 commented Aug 19, 2020

Hi, I'm encountering this from bindgen generated files. I've given the docs to bindgen a look and don't see a good way to modify the output. Is there a recommended solution for this case?

@kupiakos
Copy link
Contributor

kupiakos commented Mar 5, 2021

This warning seems to be too strict for fields in a repr(packed(N)) struct where N > 1. This should be guaranteed safe:

#[repr(packed(2))]
struct Foo {
    x: u16,
}

fn main() {
    // Warning shown here, but x is guaranteed to be aligned!
    let _ = &Foo{x: 0}.x;
}

This is based on my reading of the Rust reference:

For packed, if the specified alignment is greater than the type's alignment without the packed modifier, then the alignment and layout is unaffected. The alignments of each field, for the purpose of positioning fields, is the smaller of the specified alignment and the alignment of the field's type.

The current safety rule is "only fields with align = 1 are safe to reference in a packed struct".
The rule should be extended to "only fields with align ≤ struct alignment are safe to reference in a packed struct".

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2021

I am not very familiar with packed(N). What does it do in cases like this?

#[repr(packed(2))]
struct Foo {
    p : u8,
    x: u16,
}

The relevant bit of code would be this one:

pub fn is_disaligned<'tcx, L>(

Currently, only types with layout.align.abi.bytes() == 1 are excluded; I guess this should instead check the packed(N) and exclude layout.align.abi.bytes() <= N.

@kupiakos
Copy link
Contributor

kupiakos commented Mar 8, 2021

In that case, Foo has the same size/align with and without the #[repr(packed(2))], because there are no fields with alignment > 2. It has a size of 4 (because of padding bits) and an alignment of 2, and it is safe to create a reference to all fields.

I've not sent in any rustc changes and am interested in contributing - could I send a PR fixing the warning?

@RalfJung
Copy link
Member

RalfJung commented Mar 9, 2021

I've not sent in any rustc changes and am interested in contributing - could I send a PR fixing the warning?

Yes you definitely could, that would be amazing. ❤️ Please make sure to also add some test cases that cover both cases where the lint absolutely must still fire, and cases where it should not fire any more. Probably best to add the tests for the unaligned_references lint since safe_packed_borrows is hopefully going to disappear (#82525). The corresponding test file would be src/test/ui/lint/unaligned_references.rs.

If you have any questions, feel free to ask! I might not have a lot of time, but on our Zulip you'll find lots of helpful people. :)

@RalfJung
Copy link
Member

This warning seems to be too strict for fields in a repr(packed(N)) struct where N > 1. This should be guaranteed safe:

I have implemented that with #83605.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
B-unstable Blocker: Implemented in the nightly compiler and unstable. C-future-incompatibility Category: Future-incompatibility lints C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.