RFA-592 · Case file with fixtures · Case 564 of 694 · Compiler evidence
A Rust Type Can Have Only One Packed Alignment Contract
Packing is one concrete layout constraint, not a list of fallbacks. Select one ABI-backed value and treat every packed field access as alignment-sensitive.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Representation hints were treated as ordered refinements even though each active packed hint imposes a conflicting concrete field-alignment rule.
- First discriminating check
- Inspect cfg-expanded attributes and choose one packing value only from an authoritative ABI, then audit all unaligned field access.
repr(packed) changes a type's field-alignment constraints. Adding repr(packed(2)) does not refine the first annotation; it asks for a second conflicting packing contract. The failing fixture combines both and receives E0634.
Representation attributes are requirements, not preferences
Packing values are not tried in order. Rust needs one deterministic layout rule for the type. Bare packed is effectively a particular maximum field-alignment constraint, while packed(2) chooses another.
The official E0634 page says to provide a single packing hint. The fixture's conflict can also arise from separate attributes or cfg_attr branches that unexpectedly become active together.
I search every attribute after macro and configuration expansion. Fixing only the visible line may leave a target-specific duplicate in CI.
The repair chooses one documented ABI
The repaired fixture uses repr(C, packed(2)) and asserts alignment. The C component requests C-compatible field ordering rules, while packed(2) lowers field alignment. This combination is appropriate only if an external layout actually requires it.
The assertion is evidence for the current target, not a complete FFI proof. I also check total size, offsets, endianness, integer widths, and the foreign declaration. A wire format is not automatically a C memory layout.
Rust's Reference documents alignment modifiers and restrictions on combining packed and aligned representations. I use those documented guarantees rather than assuming compiler implementation details.
Packed fields make references dangerous
Packing can place a field at an address that does not meet the field type's normal alignment. Creating a normal reference to an unaligned field is invalid, even if code later casts it to a raw pointer. Some apparently harmless formatting or assertion macros borrow their arguments and can therefore trigger errors around packed fields.
I copy a Copy field into a properly aligned local before ordinary use, or form a raw pointer without an intermediate reference and use operations such as read_unaligned under a reviewed unsafe proof. Writes and non-Copy values require further care.
This is why I do not apply packed as a casual memory optimisation. Saving a few padding bytes can spread unsafe, unaligned access throughout the code and reduce performance on hardware that handles it poorly.
Parsing bytes is often safer than borrowing a packed struct
For network packets and file formats, explicit byte parsing handles endianness, length, invalid discriminants, and versioning. Reinterpreting input as a packed Rust struct must additionally prove alignment, validity of every field, padding behaviour, and complete initialisation.
An integer field may contain any bit pattern, but bool, references, and enums have stricter validity rules. Foreign bytes cannot become those Rust types before validation.
I use a small wire decoder that reads fields into normal aligned domain values. This separates stable protocol layout from Rust's in-memory model and usually gives better errors.
Conditional layout needs a complete matrix
Target-specific layouts may use cfg_attr, but predicates must be mutually exclusive. I compile every supported target combination and place layout assertions near the type. Cross-compilation can check compilation and static assertions, while hardware or emulator tests cover actual foreign interaction.
Changing packing on a public FFI type is a compatibility change. Every caller allocating, embedding, or passing the type may depend on size and alignment. I version the interface or coordinate both sides.
Alignment reductions can change atomic assumptions
I never place ordinary atomic operations onto storage that may be unaligned. Atomic types carry alignment and target-support requirements beyond having the same byte width as an integer. A packed foreign record containing an integer is not automatically safe to view as an AtomicU32, and volatile access is not a replacement for atomic synchronization.
When shared memory is involved, I separate the byte-level layout from the synchronization protocol. I copy or decode fields into properly aligned Rust values and use atomics only at addresses constructed for their contract. Tests on one x86 machine are not proof for architectures that trap or split unaligned operations. This review prevents a space-saving representation choice from becoming a concurrency bug.
My E0634 checklist
- How many active
packedhints reach the type after cfg and macro expansion? - What exact external ABI or measured need requires packing?
- Is one packing value documented by the authoritative foreign declaration?
- Are size, alignment, offsets, widths, and endianness verified?
- Can any code create a normal reference to an unaligned field?
- Would explicit byte parsing avoid invalid-value and alignment hazards?
- Are target predicates exclusive and tested across the support matrix?
- Is a layout change coordinated as an ABI compatibility change?
The core principle is that packing is a single low-level promise with consequences at every access. E0634 prevents contradictory promises. I choose one only from external evidence and keep the resulting unsafe alignment work contained.