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

repr(packed) allows invalid unaligned loads #27060

Closed
huonw opened this issue Jul 16, 2015 · 163 comments · Fixed by #39683
Closed

repr(packed) allows invalid unaligned loads #27060

huonw opened this issue Jul 16, 2015 · 163 comments · Fixed by #39683
Assignees
Labels
A-mir Area: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.html B-RFC-approved Feature: Approved by a merged RFC but not yet implemented. B-unstable Feature: Implemented in the nightly compiler and unstable. C-bug Category: This is a bug. C-future-compatibility Category: Future-compatibility lints E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion. I-crash Issue: The compiler crashes (SIGSEGV, SIGABRT, etc). Use I-ICE instead when the compiler panics. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@huonw
Copy link
Member

huonw commented Jul 16, 2015

This is now a tracking issue for RFC 1240, "Taking a reference into a struct marked repr(packed) should become unsafe", but originally it was a bug report that led to the development of that RFC. The original miscompilation did not involve references, but it has been fixed; the reference case is the one that remains open. A lot of the discussion is hence outdated; this is the current state.


Original Issue Description

(This code actually works fine on today's nightly.)

#![feature(simd, test)]

extern crate test;

// simd types require high alignment or the CPU faults
#[simd]
#[derive(Debug, Copy, Clone)]
struct f32x4(f32, f32, f32, f32);

#[repr(packed)]
#[derive(Copy, Clone)]
struct Unalign<T>(T);

struct Breakit {
    x: u8,
    y: Unalign<f32x4>
}

fn main() {
    let val = Breakit { x: 0, y: Unalign(f32x4(0.0, 0.0, 0.0, 0.0)) };

    test::black_box(&val);

    println!("before");

    let ok = val.y;
    test::black_box(ok.0);

    println!("middle");

    let bad = val.y.0;
    test::black_box(bad);

    println!("after");
}

Will print, on playpen:

before
middle
playpen: application terminated abnormally with signal 4 (Illegal instruction)

The assembly for the ok load is:

    movups  49(%rsp), %xmm0
    movaps  %xmm0, (%rsp)
    #APP
    #NO_APP

But for the bad one, it is

    movaps  49(%rsp), %xmm0
    movaps  %xmm0, (%rsp)
    #APP
    #NO_APP

Specifically, the movups (unaligned) became a movaps (aligned), but the pointer isn't actually aligned, hence the CPU faults.

@huonw huonw added I-crash Issue: The compiler crashes (SIGSEGV, SIGABRT, etc). Use I-ICE instead when the compiler panics. I-nominated T-lang Relevant to the language team, which will review and decide on the PR/issue. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 16, 2015
@Gankra
Copy link
Contributor

Gankra commented Jul 16, 2015

I... honestly thought that was expected behaviour. What can possibly be done about this is general, other than making references into packed fields a different type (or forbidding them)?

@eefriedman
Copy link
Contributor

This is UB, so it's clearly not expected... but yes. it's more of a design flaw than an implementation issue.

Note that it's possible to make code like this misbehave even without SIMD, although it's a bit trickier; LLVM performs optimizations based on the alignment of loads.

@huonw
Copy link
Member Author

huonw commented Jul 16, 2015

Yeah, I only used SIMD because it was the simplest way to demonstrate the problem on x86. I believe platforms like ARM are generally stricter about load alignments, so even, say, u16 may crash in simple cases like the above.

@retep998
Copy link
Member

Is there a way to tell LLVM that the value could be unaligned, and so LLVM should emit code that tries to read it in a safe way for the given architecture, even if it results in slower code?

@vadimcn
Copy link
Contributor

vadimcn commented Jul 21, 2015

@retep998: That's exactly what LLVM should have done for a packed struct. This looks like a LLVM codegen bug to me.

@dotdash
Copy link
Contributor

dotdash commented Jul 21, 2015

@vadimcn this is entirely our (or rather: my) fault. We used to not emit alignment data at all, which caused misaligned accesses for small aggregates (see #23431). But now we unconditionally emit alignment data purely based on the type that we're loading, ignoring where that value is stored. In fact at the point where we create the load, we currently don't even know where that pointer comes from and can't properly handle packed structs.

@retep998
Copy link
Member

Perhaps we need some sort of alignment attribute that we can attach to pointers?

@huonw
Copy link
Member Author

huonw commented Jul 21, 2015

In general we have no idea where a reference comes from, e.g. fn foo(x: &f32x4) { let y = *x; ... in some external crate can't know if we happen to pass in &val.y.0 in the code above. The only way to codegen to handle unaligned references properly is to assume pointers are never aligned, which would be extremely unfortunate. We don't currently have type-attributes, so it would be somewhat strange to introduce one here, instead of using types (for example).

@vadimcn
Copy link
Contributor

vadimcn commented Jul 22, 2015

Can we make creation of references into packed structs unsafe?

@nikomatsakis
Copy link
Contributor

triage: P-high

@rust-highfive rust-highfive added P-high High priority and removed I-nominated labels Jul 23, 2015
@nikomatsakis
Copy link
Contributor

Seems clear there's a bug here. First step is to evaluate how widely used packed is, so huon is going to do a crater run with #[repr(packed)] feature gated.

huonw added a commit to huonw/rust that referenced this issue Jul 24, 2015
There are some correctness issues due to unaligned internal fields and
references. cc rust-lang#27060.
huonw added a commit to huonw/image that referenced this issue Jul 24, 2015
There's some correctness issues with this, so there may be breaking
changes in future. See rust-lang/rust#27060.
huonw added a commit to huonw/X11Cap that referenced this issue Jul 24, 2015
This struct is laid out the same way with or without `packed`, since
it's just a few bytes.

The removal is good because there's some correctness issues with it, so
there may be breaking changes to it in future and removing it now will
avoid them all together. See
rust-lang/rust#27060.
huonw added a commit to huonw/image that referenced this issue Jul 24, 2015
This struct never seems to be used in a way that requires being packed.

The removal is good because there's some correctness issues with it, so
there may be breaking changes to it in future and removing it now will
avoid them all together. See
rust-lang/rust#27060.
huonw added a commit to huonw/stemmer-rs that referenced this issue Jul 24, 2015
This struct is laid out the same way with or without `packed`, since
it is empty.

The removal is good because there's some correctness issues with it, so
there may be breaking changes to it in future and removing it now will
avoid them all together. See
rust-lang/rust#27060.
huonw added a commit to huonw/rust-openal that referenced this issue Jul 24, 2015
These structs are laid out the same way with or without `packed`, since
they're just repeats of a single element type. That is, they're always
going to be three contiguous `f32`s.

The removal is good because there's some correctness issues with it, so
there may be breaking changes to it in future and removing it now will
avoid them all together. See
rust-lang/rust#27060.
@RalfJung
Copy link
Member

RalfJung commented Feb 25, 2021

With #82525, we are finally reaching the finish line to fixing this problem: once that lands, creating a reference to a packed field will cause a future-incompat warning in any context, even inside unsafe blocks, and the warning will link to #82523 which explains what to do. Then it is just a matter of slowly cranking up the lint from warn-by-default to err-by-default to hard error (or however far the lang team is willing to go ;).

Dylan-DPC-zz pushed a commit to Dylan-DPC-zz/rust that referenced this issue Mar 27, 2021
…ochenkov

make unaligned_references future-incompat lint warn-by-default

and also remove the safe_packed_borrows lint that it replaces.

`std::ptr::addr_of!` has hit beta now and will hit stable in a month, so I propose we start fixing rust-lang#27060 for real: creating a reference to a field of a packed struct needs to eventually become a hard error; this PR makes it a warn-by-default future-incompat lint. (The lint already existed, this just raises its default level.) At the same time I removed the corresponding code from unsafety checking; really there's no reason an `unsafe` block should make any difference here.

For references to packed fields outside `unsafe` blocks, this means `unaligned_refereces` replaces the previous `safe_packed_borrows` warning with a link to rust-lang#82523 (and no more talk about unsafe blocks making any difference). So behavior barely changes, the warning is just worded differently. For references to packed fields inside `unsafe` blocks, this PR shows a new future-incompat warning.

Closes rust-lang#46043 because that lint no longer exists.
@XAMPPRocky
Copy link
Member

@RalfJung Now that #82525 has been merged should we close this issue?

@RalfJung
Copy link
Member

RalfJung commented Apr 6, 2021

This issue here is only truly fixed once that lint has been turned into a hard error.

@dvc94ch
Copy link
Contributor

dvc94ch commented Jul 19, 2021

Instead of a warning/error, why can't rust generate unaligned loads and stores automatically? When using zerocopy structs I think packed is the only layout that is guaranteed to be the same across architectures. It would be really nice if rust could just do an unaligned load/store when necessary.

@mathstuf
Copy link
Contributor

unaligned load/store when necessary.

This would mean that all reads and writes through &T must be done with unaligned instructions. If I pass &u64 to a function, it can currently assume it is aligned. That is not the case for &packed_struct.some_u64 and without callees knowing all callers, code would all need to be done through unaligned loads and stores. A quick search shows this post which says that unaligned instructions can be twice as slow (particularly when staddling a cache line).

@dvc94ch
Copy link
Contributor

dvc94ch commented Jul 19, 2021

I guess defining getters/setters for all fields isn't that bad. Is there something that can be done to optimize unaligned accesses on architectures were it happens to be aligned? Or does that also give a 2x penalty?

@mathstuf
Copy link
Contributor

Even if it doesn't have that penalty, you now have:

  • more instructions; and
  • a conditional jump.

Both of which are very bad in tight loops (instruction cache pressure and branch prediction shenanigans). Speculation is also probably poking its head in here somewhere. I think if you want to pass around unaligned references, the best you have is:

let local_copy = packed_struct.some_u64;
f(&local_copy);

let mut local_copy = packed_struct.some_u64;
f_mut(&mut local_copy);
packed_struct.some_u64 = local_copy;

to ensure that any unaligned loads or stores are explicit (well, assuming one knows that packed_struct contains unaligned fields).

@dvc94ch
Copy link
Contributor

dvc94ch commented Jul 19, 2021

Sorry for my dumb questions

let local_copy = packed_struct.some_u64;
packed_struct.some_u64 = local_copy;

These are not undefined behaviour? I'd expect the . to add an offset to the struct pointer which would be undefined behaviour.

EDIT: I guess I'll just assume that's not the case. Thanks!

@RalfJung
Copy link
Member

Those are indeed not UB. Accessing packed places is okay since the required alignment is directly visible in the place expression.

What is UB is to do the same through a reference, where there is no more any indication in the code that things might not be aligned.

@DemiMarie
Copy link
Contributor

Even if it doesn't have that penalty, you now have:

  • more instructions; and
  • a conditional jump.

That is architecture-dependent, though. On platforms that do not have a strict alignment requirement at the hardware level, there should be a way to take advantage of this.

@RalfJung
Copy link
Member

RalfJung commented Jul 19, 2021

FWIW, and I don't know to what extend that is actually relevant in terms of performance, but proper alignment is not just exploited by load/store instructions (which might require this on the hardware level), it also informs the optimizer about possible values -- the compiler will assume that the last few bits of a pointer are zero, and use that for its optimizations. There are also proposal to make e.g. all the values 0-3 a "nice" in &i32, thus making Option<Option<&i32>> still fit into a single word.

And there is unsafe code that likes to pack a pointer and a few bits of information together in one pointer-sized datum, which it can by exploiting proper alignment. If &i32 can be misaligned, those tricks would not work on arbitrary references any more.

@mathstuf
Copy link
Contributor

Even if it doesn't have that penalty, you now have:

  • more instructions; and
  • a conditional jump.

That is architecture-dependent, though. On platforms that do not have a strict alignment requirement at the hardware level, there should be a way to take advantage of this.

So…Rust code will become a maze of #ifdef (spelled with #[cfg], but a maze nonetheless) to support various platforms? If you're writing platform-specific code anyways, I would recommend:

  • writing the assembly directly; and
  • making a crate to host it (with portable fallbacks where possible).

You'll probably need pointers rather than references, but that should give an idea of the knives you're juggling at that point :) .

@DemiMarie
Copy link
Contributor

Even if it doesn't have that penalty, you now have:

  • more instructions; and
  • a conditional jump.

That is architecture-dependent, though. On platforms that do not have a strict alignment requirement at the hardware level, there should be a way to take advantage of this.

So…Rust code will become a maze of #ifdef (spelled with #[cfg], but a maze nonetheless) to support various platforms? If you're writing platform-specific code anyways, I would recommend:

The main case for this I can think of is SIMD code, which is architecture-specific anyway. Nevertheless, such code could use a suitable wrapper around a raw pointer.

This is just one instance of a larger problem: there are many APIs that take references, but which might not need the full guarantees references provide. For instance, mmap can be made reasonably safe (SIGBUS at worst) by returning the equivalent of &[Atomic<u8>], but no API in the ecosystem takes that ― even ones (such as File!) that could do so safely. This forces either code duplication or a bunch of extra copies.

@mathstuf
Copy link
Contributor

Aren't SIMD instructions typically even more alignment-sensitive (at least for performance)?

As for externally-managed resources, Rust doesn't have a good way to manage them other than sticking Result on all APIs (or hoping everything is sensible enough to avoid SIGBUSsing you). I have the same issue with resources going out from under me in the keyutils crate due to timeouts or other processes revoking keys in various namespaces. I think when playing with such things, it's fine to request that the boundary does some extra work to ensure that alignment is as expected for everyone else (so you might need to toss some bytes from the beginning of your mmap allocation, but that's part of the game for all the other benefits Rust reaps from such assumptions).

@mgrech
Copy link

mgrech commented Mar 27, 2022

I can't help but feel very disappointed by the current state of affairs regarding packed structs and the direction Rust is taking here. Coming from a C++ background, I just started learning Rust for a reverse engineering weekend project where I needed to be able to read proprietary file formats with all kinds of weird layouts and I wanted to be declarative, which meant using packed structs. Unfortunately rustc would throw warnings about references to fields of packed structs, which is how I came across this issue.

It has been argued that you can always use <something else> instead of packed structs. That is true, but the same argument can be made for anything. Why use Rust at all, when I could just use assembly? The point is that packed structs are the most well-known, obvious, convenient, maintainable, easy to teach way to interact with externally given data layouts and if Rust wants to be a good language for low-level tasks it needs to support packed structs well. The current situation is not good enough and turning those warnings into errors makes it even worse.

From my personal experience working with LLVM I know that it supports an explicit alignment specifier on loads and stores. Therefore, it can in principle generate the correct instruction sequences for any load - including unaligned ones - portably. Hence this a Rust issue, not an LLVM issue. A solution has already been suggested, but seemingly ignored: Add alignment to the type system as an additional attribute, e.g. align(1) u32 would be an unaligned u32 and the corresponding reference type is &align(1) u32. By putting the alignment in the type Rust can emit the correct alignment information on any load and store when generating LLVM IR. A lighter alternative might be to have just an unaligned specifier, e.g. unaligned u32 and &unaligned u32, but I believe explicit alignment values might be useful in other areas too such as SIMD, where higher-than-usual alignments are needed.

I would be happy to create an RFC around this idea if the Rust team is interested, though I'm not sure that I'm the right person for this, given my limited experience with Rust. Regardless, I do hope that something can be done about this. It would be unfortunate if Rust's support for packed structs was just as lacking as C/C++'s.

@RalfJung
Copy link
Member

RalfJung commented Mar 27, 2022

A solution has already been suggested, but seemingly ignored: Add alignment to the type system as an additional attribute, e.g. align(1) u32 would be an unaligned u32 and the corresponding reference type is &align(1) u32.

Yes, this would be a principled approach to solving the problem. That is also a very heavy hammer for a niche feature, so the question is whether that is complexity budget well spent. Meanwhile in this issue we are working on at least fixing the soundness gap while other people (if they want) can work on more fundamental approaches to handling insufficiently aligned data in Rust.

Put differently: even if we had alignment annotation on reference types, we would still have to break old code that creates references to packed fields. So your proposal does not even help make progress on the issue that is being discussed here. This is not an issue to discuss "making packed structs more ergonomic in Rust in general", it is specifically about rejecting code that creates references (with the current reference types, i.e. fully aligned references) to fields of packed structs.

If people care about having support for unaligned references in Rust, that is certainly worth discussing, but not in this issue. :) This is a language extension idea so it could be discussed on Zulip or IRLO to gauge what would have to go into a pre-RFC.

The point is that packed structs are the [lots of adjectives] way to interact with externally given data layouts and if Rust wants to be a good language for low-level tasks it needs to support packed structs well.

As you say yourself later, Rust is no worse off than C/C++ -- if anything, Rust is better off since at least the compiler tells you when you are using packed structs the wrong way. So are you saying C/C++ are not "good language[s] for low-level tasks"? No this is not a question I actually would like to see discussed in this thread, I just want to ask you to please refrain from unnecessary rhetoric like this. (There are also tons of other great ways to deal with externally defined data layouts, and with serde Rust has world-class support for them.)

@RalfJung
Copy link
Member

In fact, given that we have #82523 to track the future-incompat issue, I wonder if we should close this one?

@oli-obk oli-obk closed this as completed Mar 28, 2022
@mgrech
Copy link

mgrech commented Mar 28, 2022

Yes, this would be a principled approach to solving the problem. That is also a very heavy hammer for a niche feature, so the question is whether that is complexity budget well spent. Meanwhile in this issue we are working on at least fixing the soundness gap while other people (if they want) can work on more fundamental approaches to handling insufficiently aligned data in Rust.

Put differently: even if we had alignment annotation on reference types, we would still have to break old code that creates references to packed fields. So your proposal does not even help make progress on the issue that is being discussed here. This is not an issue to discuss "making packed structs more ergonomic in Rust in general", it is specifically about rejecting code that creates references (with the current reference types, i.e. fully aligned references) to fields of packed structs.

If people care about having support for unaligned references in Rust, that is certainly worth discussing, but not in this issue. :) This is a language extension idea so it could be discussed on Zulip or IRLO to gauge what would have to go into a pre-RFC.

Makes sense. I was under the impression that the Rust team wants to disallow references to packed struct fields simply because it is considered "wrong", not because of soundness. I now understand that this is not actually the case and there is still room for discussion on this topic, which I will try to take to the other channels you mentioned. Apologies for being off-topic here, I had difficulties finding the right place to ask about this as someone new to the Rust project.

@SimonSapin
Copy link
Contributor

Using struct definitions instead of manually computing offsets can be a very nice way to parse some binary formats, but in addition to alignment and padding you also need to worry about endianness and only using types where all bit patterns are valid. Crates like https://crates.io/crates/zerocopy and https://crates.io/crates/bytes-cast provide derivable traits to check safety properties of your structs, and some primitives like U32Be that wraps [u8; 4] (so no alignment requirement) and whose APIs do the appropriate conversions.

@jrose-signal

This comment was marked as outdated.

hehaoqian pushed a commit to hehaoqian/rust_reference that referenced this issue Jul 1, 2023
Issue rust-lang/rust#27060 has been resolved, so it is no longer possible to safely create unaligned pointers to packed struct fields.
@rust-lang rust-lang deleted a comment Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-mir Area: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.html B-RFC-approved Feature: Approved by a merged RFC but not yet implemented. B-unstable Feature: Implemented in the nightly compiler and unstable. C-bug Category: This is a bug. C-future-compatibility Category: Future-compatibility lints E-mentor Call for participation: This issue has a mentor. Use #t-compiler/help on Zulip for discussion. I-crash Issue: The compiler crashes (SIGSEGV, SIGABRT, etc). Use I-ICE instead when the compiler panics. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.