RFA-031 · Case file with fixtures · Case 3 of 694 · Compiler evidence
Printing a repr(packed) Field Creates an Unaligned Reference
A packed field can be read by value while borrowing the same field is invalid. See why formatting adds a hidden reference and when to copy or use read_unaligned.
- Reviewed
- Rust
- stable Rust
- Targets
- all targets; alignment impact is target-dependent
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Formatting and borrowing create a reference whose alignment promise cannot be satisfied by a potentially unaligned packed field.
- First discriminating check
- Copy the field into an aligned local before formatting; if needed, compare that with `addr_of!` plus `read_unaligned`.
This Rust error can look inconsistent:
#[repr(packed)]
struct Header {
tag: u8,
length: u32,
}
let header = Header { tag: 1, length: 64 };
let length = header.length; // accepted
println!("{}", header.length); // E0793
Both lines seem to read length. The real difference is that the first copies a value, while formatting normally borrows its argument. That hidden reference must satisfy the normal alignment rule for &u32. A packed field may not.
The Atlas keeps this distinction executable. The failing program first proves that both fields can be copied, then receives E0793 only when it creates &header.length. The repaired program verifies both the safe-copy path and an explicit raw-pointer read_unaligned path on Rust 1.98.1.
What packing changes
Rust normally inserts padding so each field starts at a suitable address. #[repr(packed)] lowers alignment and removes some or all padding. This is useful only for specific layout contracts, but it creates addresses that may be valid for bytes and invalid for an ordinary typed reference.
A reference is not only an address. &u32 promises, among other things, that its address is aligned for u32. Creating the reference breaks that promise immediately. It is undefined behavior even if code never dereferences the reference, and an unsafe block does not make the reference valid.
This is why rustc rejects the expression with E0793.
The smallest discriminating check
I force an explicit by-value copy before any trait call:
let length = header.length;
println!("{}", length);
Or, for a compact expression:
println!("{}", { header.length });
The braces make the field access produce a temporary value. Formatting borrows that aligned temporary instead of borrowing through the packed field.
If this compiles, the failure is about reference creation, not about the numeric value or Display implementation.
Why unsafe { &header.length } is still wrong
Unsafe code means the programmer must prove the required rules. It does not switch them off:
let reference = unsafe { &header.length }; // still invalid
On a machine where unaligned loads happen to work, a test may appear successful. That does not repair the invalid reference, and optimized code may rely on its alignment promise.
This distinction is important in reviews. “The CPU supports it” answers a hardware-load question. Rust is rejecting a language-level reference whose contract is stronger.
Use a raw pointer for true unaligned access
When the value cannot simply be copied through a safe field expression, create a raw pointer without first making a reference, then use an unaligned operation:
let pointer = std::ptr::addr_of!(header.length);
let length = unsafe { pointer.read_unaligned() };
The order matters. This is wrong:
let pointer = &header.length as *const u32;
It creates the forbidden reference before casting it. addr_of! or the raw-reference syntax &raw const header.length avoids that intermediate reference.
For a write, use a raw mutable pointer and write_unaligned when all other ownership and validity requirements are satisfied.
Packed is not a serialization strategy
I avoid using a packed struct merely to parse a network packet or file format. Matching a byte length is not enough. A format also defines byte order, valid bit patterns, versioning, and often checksums or reserved fields.
A safer parser reads bytes and converts each field explicitly:
fn parse_length(bytes: &[u8; 4]) -> u32 {
u32::from_le_bytes(*bytes)
}
This makes endianness visible and never lends a typed reference to possibly unaligned external storage.
Packed layout may still be required for a documented hardware or foreign interface. Then I keep the packed type at the boundary and copy fields into an aligned Rust representation before doing normal work.
Check the actual field alignment
The offset alone is not the complete question. I compare the field address with align_of::<T>() in a controlled test, without forming a reference:
let base = std::ptr::addr_of!(header) as usize;
let field = std::ptr::addr_of!(header.length) as usize;
assert_eq!(field - base, 1);
assert_ne!(field % std::mem::align_of::<u32>(), 0);
The final assertion depends on where the value itself is placed, so a robust fixture may allocate several values or verify the known ABI context. The compiler error is stronger than one address which happens to be aligned by luck.
The repair and regression check
For ordinary reading or formatting, I copy the field to a local. For raw storage that truly permits unaligned values, I use raw pointers with read_unaligned or write_unaligned. For protocol data, I parse from bytes. I never repair E0793 by manufacturing a reference in unsafe code.
The regression fixture should compile the valid forms and intentionally fail the borrowed form. If the layout crosses FFI, I also assert sizes and offsets on every supported target. This keeps the reason for the unusual representation visible instead of leaving packed as a mysterious attribute.