A 32-Byte Header That Copies 8: Array Decay and Wire Structs
A logger writes records to a socket using a 32-byte header. Two functions are involved:
void sendHeader(const char header[32]) {
char local[32];
std::memcpy(local, header, sizeof(header)); // line A
write(sock, local, sizeof(local));
}
struct RecHeader {
uint32_t length;
uint16_t flags;
char magic;
};
On the sending side sizeof(RecHeader) is 8, and the code writes the struct directly with write(sock, &h, sizeof h) before filling length and flags in host byte order. The receiving service, a big-endian 32-bit peer, rejects every record: it sees a length field of 0 or absurd values and flags that never match. Meanwhile the sender's unit tests pass because they compare only magic and the first record, whose payload is 64 zero bytes. Developers also notice that the first 8 bytes of each header arrive correctly and the remaining 24 bytes vary between runs. The build is x86_64 little-endian with -O2 and no warnings enabled.
1. Explain line A: why `sizeof(header)` is not 32, why the compiler does not warn by default, and how the remaining bytes become wrong. Give two concrete ways to make the length part of the type.
Read the worked answer
In a function parameter, const char header[32] is adjusted to const char* header; the 32 is documentation only. Therefore sizeof(header) is the size of a pointer, 8 on x86_64, not 32. Line A copies only the first 8 bytes into local; bytes 8 to 31 remain uninitialized. write then sends 32 bytes, so the first 8 are correct and the other 24 contain whatever was on the stack, which explains both the varying bytes and the fact that only the first 8 arrive reliably. This is also an information leak. The compiler does not warn by default because the code is well-formed; GCC's -Wsizeof-array-argument diagnoses exactly this pattern. Fixes: take const std::array<char,32>& and use header.size(); or use a template template<std::size_t N> void sendHeader(const char (&header)[N]) with static_assert(N == 32); or take std::span<const char,32>. Each preserves the length in the type and makes size queries correct. A raw pointer plus an explicit length parameter would also work but relies on the caller to supply the right value.
2. Using the struct definition and the little-endian x86_64 build, explain the actual byte layout of `RecHeader`, including padding, and explain how the big-endian peer derives wrong values for `length` and `flags`.
Read the worked answer
RecHeader has uint32_t at offset 0, uint16_t at offset 4, char at offset 6, one padding byte at offset 7, and size 8 because the alignment is 4 and the size is rounded up to a multiple of 4. The object has an indeterminate padding byte at offset 7, and write(sock, &h, sizeof h) transmits it; that is why the last byte of an 8-byte struct can vary. More importantly, the numeric fields are written in host byte order. On little-endian x86, length = 0x00000100 is transmitted as 00 01 00 00, which a big-endian peer interprets as 0x00010000 (65536), and flags = 0x0001 is read as 0x0100 (256). Single-byte magic is unaffected, so the peer can recognize the record but rejects the length and flags. The peer's reported length of 0 or absurd values is consistent with this byte swap. sizeof is 8 here only because of the chosen field order; reordering fields changes offsets, which is another reason not to treat the struct as the wire format.
3. Propose a portable serialization fix. Explain why `#pragma pack(1)` and a `static_assert` on the struct size are not sufficient.
Read the worked answer
Do not send the struct. Define the wire layout explicitly and serialize field by field into a byte buffer: std::array<std::uint8_t,32> out{}; then write a 32-bit big-endian length with htonl, a 16-bit big-endian flags with htons, and the magic byte at its documented offset. htons and htonl return network byte order; use ntohl and ntohs on receive. Initialize the whole buffer to zero so padding-like bytes are deterministic, and never transmit padding from a struct. #pragma pack(1) is not a fix: it removes padding but not the endianness problem, it can create unaligned accesses that are undefined or slow on some architectures, and it is compiler-specific. A static_assert(sizeof(RecHeader) == 8) only documents the in-memory type; it does not make the bytes portable and it breaks silently when a field is added. Compile with -Wall -Wextra -Wsizeof-array-argument -Wconversion and add a round-trip test that parses the serialized bytes with explicit shifts, independent of the host's endianness. That test would fail the current code on x86 before it ever reaches staging.