Fourth silent drop of the #1041 class in one session — found by resolving the RQ-59-DATASEG lane's open observation (RQ-59-STARTFN, #1046 work). This one is OUTSIDE the documented embedder contract: the contract reserves R9 as the globals-table BASE; nothing anywhere documents that the embedder must evaluate the module's global INITIALIZERS and seed the table's contents.
Repro (executed on the v0.59 tree, commit 18a4786)
(module
(global $g (mut i32) (i32.const 42))
(func (export "get") (result i32) global.get $g))
wasmtime: get() == 42.
synth compile globals_repro.wat --all-exports --relocatable --target cortex-m4 -o glob_arm.o
# exit 0, no warning
The emitted object, in full (objdump -s): .text = 10b5 d9f8 0000 10bd (push {r4,lr}; ldr.w r0,[r9]; pop {r4,pc}). The constant 42 (0x2A) appears nowhere in the ELF — no .data, no __synth_globals symbol, no record of the initializer. Whatever the embedder maps at R9 is what get returns; on a zeroed region (the only shape any in-tree harness ever maps on this path) get() == 0, a silently wrong value with a green build.
Why this is OUTSIDE the contract (unlike element segments, which are inside)
What the repo actually documents about R9 on the relocatable path:
select_with_stack.rs: "Load global value from globals table (R9 = globals base)." — the BASE register only.
i64_globals_643_differential.py (the only relocatable-path globals harness): "one unicorn instance per path with R9 pointed at a zeroed scratch region" and, at the map site, "# zeroed globals table (inits are 0)" — the harness works only because that fixture's inits ARE 0, and its author knew it (the parenthetical exists for exactly this reason). It never claims the embedder evaluates initializers; it sets every tested value via set before get.
- README / FEATURE_MATRIX: "R9-based globals" — mechanism, not init ownership.
Contrast the data-segment contract that made #1041 a refusal-with-flag rather than a fourth-class miscompile: multi_segment_static_data_differential.py states it in prose — "Memory 0 lives at the runtime R11 base and the embedder populates its init segments (the object carries no ROM image on this path)". There is no analogous sentence for global initializers anywhere in the repo.
And every OTHER path treats nonzero global inits as synth's job or declines:
| path |
nonzero global initializer |
| ARM self-contained (default) |
MATERIALIZED by the image's own startup — #649 fixed exactly this class here ("nonzero i64.const global initializers silently zeroed" was filed and closed as a bug, not documented as a contract) |
ARM --native-pointer-abi |
shipped as .data slots with init values (#237) |
| AArch64 |
shipped: __synth_globals .data region "carrying its decoded constant initializer"; FEATURE_MATRIX explicitly says "The globals region … explicitly NOT preconditions: synth EMITS both" (#851 lane L3) |
| RISC-V |
LOUD-SKIP: "the RISC-V selector has NO global lowering, so every global-touching export must LOUD-SKIP … never silently truncate" (#643 harness contract) |
| f32/f64/v128 globals, any path |
loud-skip (GI-FPU-001 #648, #680) — because "the initializer would be silently zeroed" |
ARM plain --relocatable |
nothing ships, nothing declines, nothing documents — exit 0 |
The plain relocatable path is the only square in the matrix where a nonzero initializer neither reaches the artifact nor produces a decline nor is assigned to the embedder in writing. #649's own fix note shows the class was recognized as a silent-wrong-value bug — it was closed on the self-contained path and never revisited here.
The elem-segment sibling observation resolves the OTHER way (for the record)
The same RQ-59-DATASEG report flagged active element-segment table images on this path. Those are INSIDE the documented contract: call_indirect_594_differential.py — "ArmOp::CallIndirect contract (same as the Thumb-2 path): R11 holds the function-pointer table base; entry i is a 4-byte code address. The harness builds that table with func_0's address" — and the encoder comment "Table base setup must be done by caller/runtime." The table's entries are final code addresses that only exist after linking, the object exports every func_N the segments name (reachability pulls them in), and the embedder holds the module's segment layout; building the table is inherently the integrator's step, and it is written down. (Residual worth a doc line someday: the #676 heterogeneous-table type-id sidecar's structural class ids are derivable but their derivation rule lives only in wasm_decoder.rs doc comments.)
Honest fix shapes (the #1041 menu)
(a) refuse loudly on the plain relocatable path when any defined global has a nonzero (or non-const-zero) initializer, unless the path materializes the region (--native-pointer-abi) — with an explicit --embedder-global-init-style acknowledgment flag if harnesses need it (the #952/#1049 shape, bytes identical);
(b) ship the init image (an ARM __synth_globals-style .data region mirroring aarch64 #851 L3) — capability work, v0.60 material alongside VCR-REACH-002.
(a) is the v0.59-sized fix. Zero-initialized globals (the overwhelmingly common $__stack_pointer-after-set case is NOT zero — note gust_kernel's shadow-stack top is 0x100000, but that module ships self-contained) need care: refusing only NONZERO inits keeps the #643 fixture and the zeroed-scratch harnesses green.
Refs #1041 (class), #1046 (third instance, fix in flight), #649 (same defect, fixed on the self-contained path only), #851 (aarch64 ships the image), #643 (the harness whose "inits are 0" parenthetical marks the hole).
Fourth silent drop of the #1041 class in one session — found by resolving the RQ-59-DATASEG lane's open observation (RQ-59-STARTFN, #1046 work). This one is OUTSIDE the documented embedder contract: the contract reserves R9 as the globals-table BASE; nothing anywhere documents that the embedder must evaluate the module's global INITIALIZERS and seed the table's contents.
Repro (executed on the v0.59 tree, commit 18a4786)
wasmtime:
get() == 42.The emitted object, in full (
objdump -s):.text=10b5 d9f8 0000 10bd(push {r4,lr}; ldr.w r0,[r9]; pop {r4,pc}). The constant 42 (0x2A) appears nowhere in the ELF — no.data, no__synth_globalssymbol, no record of the initializer. Whatever the embedder maps at R9 is whatgetreturns; on a zeroed region (the only shape any in-tree harness ever maps on this path)get() == 0, a silently wrong value with a green build.Why this is OUTSIDE the contract (unlike element segments, which are inside)
What the repo actually documents about R9 on the relocatable path:
select_with_stack.rs: "Load global value from globals table (R9 = globals base)." — the BASE register only.i64_globals_643_differential.py(the only relocatable-path globals harness): "one unicorn instance per path with R9 pointed at a zeroed scratch region" and, at the map site, "# zeroed globals table (inits are 0)" — the harness works only because that fixture's inits ARE 0, and its author knew it (the parenthetical exists for exactly this reason). It never claims the embedder evaluates initializers; it sets every tested value viasetbeforeget.Contrast the data-segment contract that made #1041 a refusal-with-flag rather than a fourth-class miscompile:
multi_segment_static_data_differential.pystates it in prose — "Memory 0 lives at the runtime R11 base and the embedder populates its init segments (the object carries no ROM image on this path)". There is no analogous sentence for global initializers anywhere in the repo.And every OTHER path treats nonzero global inits as synth's job or declines:
--native-pointer-abi.dataslots with init values (#237)__synth_globals.dataregion "carrying its decoded constant initializer"; FEATURE_MATRIX explicitly says "The globals region … explicitly NOT preconditions: synth EMITS both" (#851 lane L3)--relocatableThe plain relocatable path is the only square in the matrix where a nonzero initializer neither reaches the artifact nor produces a decline nor is assigned to the embedder in writing. #649's own fix note shows the class was recognized as a silent-wrong-value bug — it was closed on the self-contained path and never revisited here.
The elem-segment sibling observation resolves the OTHER way (for the record)
The same RQ-59-DATASEG report flagged active element-segment table images on this path. Those are INSIDE the documented contract:
call_indirect_594_differential.py— "ArmOp::CallIndirect contract (same as the Thumb-2 path): R11 holds the function-pointer table base; entry i is a 4-byte code address. The harness builds that table with func_0's address" — and the encoder comment "Table base setup must be done by caller/runtime." The table's entries are final code addresses that only exist after linking, the object exports everyfunc_Nthe segments name (reachability pulls them in), and the embedder holds the module's segment layout; building the table is inherently the integrator's step, and it is written down. (Residual worth a doc line someday: the #676 heterogeneous-table type-id sidecar's structural class ids are derivable but their derivation rule lives only inwasm_decoder.rsdoc comments.)Honest fix shapes (the #1041 menu)
(a) refuse loudly on the plain relocatable path when any defined global has a nonzero (or non-const-zero) initializer, unless the path materializes the region (
--native-pointer-abi) — with an explicit--embedder-global-init-style acknowledgment flag if harnesses need it (the #952/#1049 shape, bytes identical);(b) ship the init image (an ARM
__synth_globals-style.dataregion mirroring aarch64 #851 L3) — capability work, v0.60 material alongside VCR-REACH-002.(a) is the v0.59-sized fix. Zero-initialized globals (the overwhelmingly common
$__stack_pointer-after-set case is NOT zero — note gust_kernel's shadow-stack top is 0x100000, but that module ships self-contained) need care: refusing only NONZERO inits keeps the #643 fixture and the zeroed-scratch harnesses green.Refs #1041 (class), #1046 (third instance, fix in flight), #649 (same defect, fixed on the self-contained path only), #851 (aarch64 ships the image), #643 (the harness whose "inits are 0" parenthetical marks the hole).