skip to content

Your metrics daemon panics only on the 32-bit ARM build — how do you diagnose it and stop the class recurring?

level: seniorimportance: should knowfreq 25%

answer

  1. read the panic before theorising
  2. the loud bug has quiet siblings
  3. a build you never run is not a target
  4. execute the suite under a 32-bit GOARCH
  5. structural fix beats field reordering

basics

~20 s

Read the panic: an unaligned 64-bit atomic points at a 64-bit struct field on a 32-bit target. Then hunt its silent siblings, int truncation above all, and add a 32-bit GOARCH build, vet and test leg to CI.

solid answer

~50 s

The panic text and stack carry most of the diagnosis: an unaligned 64-bit atomic operation on 386 or arm means a 64-bit field that is not on an 8-byte boundary. Fix it structurally by moving the counter to `atomic.Uint64` rather than reordering fields, because a reshuffle is undone by the next edit to the struct. Then treat the panic as the loud member of a family and go looking for the quiet ones: `int64`-to-`int` conversions on sizes, offsets and timestamps, comparisons against `math.MaxInt`, and any code that reinterprets bytes. Finally make the target real in CI. `GOOS=linux GOARCH=arm go vet ./...` and `go build` catch compile-time overflow, but only running the suite — `GOARCH=386 go test ./...` on the amd64 runner, or the arm binaries under an emulator via `go test -exec` — exercises the alignment path. A target you compile but never run is not a supported target.

code

text · 9 lines
text
# type-check and compile for each supported target
GOOS=linux GOARCH=arm go vet ./...
GOOS=linux GOARCH=arm go build ./...

# actually execute the suite with 32-bit widths and 4-byte alignment
GOOS=linux GOARCH=386 go test ./...

# for the real arm target, run the test binaries under an emulator
GOOS=linux GOARCH=arm go test -exec /usr/local/bin/run-under-emulator ./...

go deeper

for a junior

Know that a crash appearing only on one GOARCH is a portability bug, not random flakiness, and that the panic message names the mechanism. Being able to reproduce it with a 32-bit build is the expectation.

for a middle

Walk the mechanics: why a 64-bit field is only 4-byte aligned on 32-bit ports, why the same code is fine on amd64, and which commands reproduce it locally.

for a senior

Show judgment. Choose the structural fix over the reshuffle, go looking for the silent truncation the same target causes, and describe the CI leg that turns a supported architecture into a tested one.

for a principal

Decide the policy: which architectures get a required test leg, what a support tier promises, and whether the cost of keeping the 32-bit fleet is worth the constraint it imposes on every future struct.

## Start with what the panic already told you A panic naming an unaligned 64-bit atomic operation, on a build whose `GOARCH` is `arm` or `386`, is nearly self-diagnosing. The stack frame names the function; the function names the struct; the struct has a 64-bit field somewhere after a 32-bit one. On 32-bit ports a 64-bit field needs only 4-byte alignment, while the 64-bit atomic operations need 8, so the field is legal to declare and illegal to touch atomically. The 64-bit builds you develop and test on align it to 8 bytes for free, which is exactly why nobody saw it. Resist the quick fix. Moving the field to the front of the struct works and is guaranteed, but the guarantee covers only the first word of an allocation, so the next person who adds a field above it, embeds the struct, or puts it in a slice of an odd size reintroduces the crash. Declaring the field as `atomic.Uint64` moves the requirement into the type, where a refactor cannot undo it, and has the side benefit of making non-atomic reads of the counter impossible to write by accident. ## Then look for the quiet siblings A 32-bit target changes two things at once: alignment, which fails loudly, and integer width, which fails silently. Having found the loud one, assume the quiet one is there too and go looking: - `int(x)` where `x` is an `int64` byte count, file offset, database identifier or nanosecond timestamp. On the 32-bit build the high bits vanish, no panic, no vet warning, and the daemon simply reports numbers that are too small. - Comparisons and clamps against `math.MaxInt`, which is a different number per target. - Anything assuming a pointer or `uintptr` is 8 bytes, or that a buffer can exceed 2 GiB — `len` returns `int`, so a slice on a 32-bit build tops out around 2^31-1 elements. - Byte-level code that relies on the machine's order or on unaligned multi-byte loads. The search is cheap and mechanical: grep for `int(` around size and time arithmetic, and read the exported structs for fields typed `int` that hold quantities. ## Make the target real in CI The root cause of the incident is not the struct; it is that a supported architecture was never executed. Three levels, in ascending value: 1. **Compile it.** `GOOS=linux GOARCH=arm go build ./...` proves the code exists for the target and catches untyped constants that overflow a 32-bit `int`. 2. **Vet it under the target.** `GOARCH=386 go vet ./...` type-checks the packages with 32-bit widths, so overflowing constants and impossible conversions surface at merge time. 3. **Run it.** `GOARCH=386 go test ./...` on an amd64 Linux runner usually executes directly and reproduces both 32-bit `int` and 4-byte alignment of 64-bit fields, which covers most of this bug class without any device in the loop. For the real `arm` target, `go test -exec <emulator-wrapper> ./...` runs the compiled test binaries under emulation. Step 3 is the one that would have caught this panic, and it is the one teams skip. ## What will not catch it Worth saying explicitly, because candidates reach for these: the race detector instruments memory accesses for happens-before violations and knows nothing about alignment; there is no standard vet check for 64-bit atomic alignment; and building for the target is not running for the target. Coverage on amd64 is also no signal at all, because the same line is exercised there and behaves correctly. ## Close the loop A good answer ends with prevention that does not depend on memory: typed atomics for every counter, explicit `int64` for every size, offset and timestamp in exported types, a named `ByteOrder` on every frame, and the 32-bit CI leg made required rather than advisory. Each of those turns a rule someone must remember into a shape the compiler or the pipeline enforces — and that is the difference between fixing an incident and retiring its class.

  • Why is a GOARCH=386 test run on an amd64 runner usually enough when the devices are ARM?
    Because this class is about word size and alignment rather than the instruction set: 386 gives you 32-bit `int`, 32-bit `uintptr` and 4-byte alignment of 64-bit fields, which reproduces both the truncation and the unaligned-atomic panic. It does not reproduce anything ARM-specific, so keep a build-and-vet leg for the real GOARCH as well.
  • The same daemon also reports byte totals that are too small on those devices, without panicking. Where do you look?
    At conversions to `int`. A total accumulated as `int64` and then stored, logged or encoded as `int` loses its high bits on a 32-bit build, and nothing reports it — not a panic, not the race detector, not vet. Search for `int(...)` around sizes and durations and add a test that pushes a value above 2^31 through the same path.
  • What would you change so the next engineer cannot reintroduce it?
    Make the safe shape the default: typed atomics for every counter, `int64` for every size, offset and timestamp in exported structs, and a named ByteOrder on every frame. Then make the 32-bit leg a required check rather than an advisory one, so a regression blocks the merge instead of the release.

saying these in an interview costs you the question

  • Blames the ARM CPU rather than type widths and alignment
  • Reorders struct fields and considers it fixed
  • Assumes -race would have caught the misalignment
  • Cross-compiles the target but never runs its tests
  • Treats a target-specific panic as a flake
  • Stops at the panic and never looks for silent truncation