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 -l → LOAD 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).
Summary
The default self-contained
--cortex-mimage (the documentedsynth compile input.wat --cortex-m -o firmware.elf"complete binary") silently drops active(data …)segments:.linear_memoryisemitted as
NoBits(BSS, zeroed) and the initializer bytes are never shipped or copied at startup, soevery 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
lo(load @256)1144201745(0x44332211)hi(load @260)-2005440939(0x88776655)Evidence the data is dropped (static — decisive)
11 22 33 44 55 66 77 88are absent from the entire ELF (xxd tc.elf | grep 11223344→ nothing)..linear_memoryships as BSS/NoBits:llvm-objdump -h→.linear_memory 00010000 20000000 BSS.filesz 0x0(memsz 0x10000):readelf -l→LOAD 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::4989let linear_memory_section = Section::new(".linear_memory", ElfSectionType::NoBits)— theself-contained builder reserves linear memory as NoBits (no file data), and the generated
Reset_Handler(generate_minimal_startup, ~:5839) has no data-copy loop.:3714 split_linmem_bss,:3745) but is gated onnative_layout.is_some()(the native-pointer layout, cf. dissolved --relocatable objects carry full wasm linmem (64KB .data) + absolute MOVW relocs — MCU-unshippable + link-fragile (gale mutex silicon fault) #345/elf: high-offset init segment defeats the #345 .bss split — stack_push dissolves to a 64KB PROGBITS .data (MCU-unshippable) #354/fix(elf): #354 — per-region .bss/.data split for high-offset init segments #356). For the defaultwasm-static
--cortex-m(no native layout) with non-emptydata_segments, that path is not taken,so the segment is dropped.
The load lowering itself is correct — this is purely missing data initialization. Positive
control:
(i32.store 256 X)(i32.load 256)returnsX, andsynth_add_offset_probe(offset loads)passes; the address computation is fine. Only the segment content never reaches the image.
Impact & scope
--cortex-mis a promoted, documented path ("complete binary for Renode/QEMU"; top-level helpexample). Any firmware with a string/lookup/const table silently reads zeros — offset-independent,
every value type (i32/i64/f32/f64).
Distinct from the native-pointer
.datawork (#345/#354/#356/#678), from #649 (non-i32 globalinitializers), 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) asProgBitswith the initializer bytes at the segment offsets and set the RW segmentfileszaccordingly, or store the bytes in flash and emit a data-copy loop inReset_Handler(thestandard MCU
.datainit). If a segment genuinely cannot be shipped, fail loudly rather than emit azero-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).