Skip to content

default --cortex-m self-contained image silently DROPS active (data) segments: .linear_memory is NoBits, initializer bytes absent -> every load from an initialized region reads 0 (silent miscompile) #758

Description

@avrabe

Summary

The default self-contained --cortex-m image (the documented synth compile input.wat --cortex-m -o firmware.elf "complete binary") silently drops active (data …) segments: .linear_memory is
emitted as NoBits (BSS, zeroed) and the initializer bytes are never shipped or copied at startup, so
every load from an initialized region reads 0 instead of the data. Valid ELF, no warning — a
silent miscompile of any module with a string table, lookup table, or const array.

Verified on synth 0.43.0 (6c23833).

Reproduction

(module (memory (export "mem") 1)
  (data (i32.const 256) "\11\22\33\44\55\66\77\88")
  (func (export "lo")(result i32)(i32.load (i32.const 256)))
  (func (export "hi")(result i32)(i32.load (i32.const 260))))
$ synth compile t.wat -t cortex-m3 --cortex-m -o tc.elf
wasmtime (oracle) synth --cortex-m image
lo (load @256) 1144201745 (0x44332211) 0
hi (load @260) -2005440939 (0x88776655) 0

Evidence the data is dropped (static — decisive)

  • The initializer bytes 11 22 33 44 55 66 77 88 are absent from the entire ELF (xxd tc.elf | grep 11223344 → nothing).
  • .linear_memory ships as BSS/NoBits: llvm-objdump -h.linear_memory 00010000 20000000 BSS.
  • The RW LOAD segment has filesz 0x0 (memsz 0x10000): readelf -lLOAD 0x20000000 … 0x00000 0x10000 RW — 64 KB of zeroed memory, no initialized content.

So a load from offset 256 reads zeroed BSS — the data was never in the image.

Root cause (source)

crates/synth-cli/src/main.rs:

The load lowering itself is correct — this is purely missing data initialization. Positive
control: (i32.store 256 X)(i32.load 256) returns X, and synth_add_offset_probe (offset loads)
passes; the address computation is fine. Only the segment content never reaches the image.

Impact & scope

  • --cortex-m is a promoted, documented path ("complete binary for Renode/QEMU"; top-level help
    example). Any firmware with a string/lookup/const table silently reads zeros — offset-independent,
    every value type (i32/i64/f32/f64).
  • Silent: valid ELF, no diagnostic (verbose stderr mentions only page count).

Distinct from the native-pointer .data work (#345/#354/#356/#678), from #649 (non-i32 global
initializers), and from #406 (multi-memory / extra-memory segment drop) — this is the memory-0 data
segment
on the default self-contained path.

Suggested fix

Ship active data segments in the self-contained image: either emit .linear_memory (or a companion
.data) as ProgBits with the initializer bytes at the segment offsets and set the RW segment
filesz accordingly, or store the bytes in flash and emit a data-copy loop in Reset_Handler (the
standard MCU .data init). If a segment genuinely cannot be shipped, fail loudly rather than emit a
zero-reading image.

Reported by the pulseengine-challenge harness (research-agent lead; clean-room reverified by ELF
static inspection + source + wasmtime oracle; load lowering confirmed correct via positive control).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions