There is no standard mechanism for this today. The Wasm core spec defines only a binary format version (1.0) in the module header — it says nothing about whether one module's exported state is meaningful to another module. Every compatibility guarantee you want for rollback has to be encoded by you, validated by your host, and treated as untrusted until checked. The good news: the pieces needed to do this safely already exist (custom sections, declarable memory/global layouts, atomic instantiation). The gap is convention, not capability.
Encoding compatibility in the module itself
The portable pattern is a custom section carrying a layout contract, because custom sections are part of the binary, survive distribution unchanged, and are ignored by engines that don't care. A pragmatic design:
- A section (commonly named something like
compat) containing a semantic version of the state schema — not the code — plus a hash of the structural layout: declared memory minimum/maximum, global count, and each global's value type and mutability.
- A rule the host can evaluate mechanically: rollback is eligible only if the old binary's schema version equals the snapshot's schema version, or the old binary explicitly declares the snapshot's version in a "reads" list (i.e., the old module promises it can interpret that layout).
Key distinction: version the state layout, not the module. Code can change freely without invalidating a snapshot; only a change to memory organization or global signatures should bump the schema version. That is what makes rollback eligibility decidable without running anything.
Validating a snapshot against the rolled-back module
Do not gate on the version field alone — treat it as a hint and verify structurally before reinterpreting bytes. The host-side check is cheap:
- At snapshot time, record: memory byte length, and the type/mutability of every exported global you intend to preserve.
- At rollback time, parse the old module's declared memory (min/max pages) and global section. Confirm the snapshot's length is within the old module's declarable bounds and that global indices and types match one-for-one.
- If anything mismatches — larger memory than the old max, different global count, a type change — reset state and re-instantiate clean. Reinterpreting a stale snapshot under a different layout is silent corruption, not a trap you can catch.
Your point about atomic instantiation is correct but narrower than it first appears: a trapping start function means the new instance never becomes observable, so the new module can't leave half-initialized state behind. The real hazard is the pre-upgrade snapshot itself — if your host mutated memory (queued messages, partially applied migrations) before or during the upgrade attempt, the snapshot may be "valid layout, invalid invariants." The defense is to snapshot at a quiescent boundary — after draining work, before any mutation tied to the upgrade — and to treat that quiescence as part of the compatibility contract.
Component Model and tooling
The Component Model improves the interface story — WIT types give you named, versionable contracts at component boundaries — but it does not standardize state-rollback semantics, and core modules get nothing from it directly. So for core modules, the honest answer is: the custom-section-plus-structural-verification pattern above is the emerging practice, and it remains per-ecosystem. Flag this for review if you need cross-host interoperability; there is no ratified convention to point at as of this writing.
Verifying your setup
Confirm the section is actually in the binary and the host reads it:
wasm-objdump -h module.wasm # list sections, confirm your custom section exists
wasm-objdump -x -j compat module.wasm # inspect its payload
Then test the failure path deliberately: build a v2 binary with one extra global, attempt the upgrade, force a trap in start, and assert the host rejects the snapshot and resets rather than reinterpreting. If that test passes, your rollback gate is structural, not cosmetic.