fix(wasm_runtime): issue WASM instance ids from one process-global allocator - #5251
fix(wasm_runtime): issue WASM instance ids from one process-global allocator#5251sanity wants to merge 2 commits into
Conversation
…locator
`MEM_ADDR`, `DELEGATE_ENV` and `CONTRACT_IO` are process-GLOBAL DashMaps
keyed by WASM instance id, so that id namespace is process-global too.
But `WasmEngine::create_instance` took a caller-supplied `id: i64`, and
only `RunningInstance::new` drew one from the shared `INSTANCE_ID`
counter.
The engine unit tests in `wasmtime_engine.rs` chose their own ids
(`0..10_001`, `0..STORE_REFRESH_THRESHOLD`, and literals such as `999`).
Their `drop_instance` calls then `MEM_ADDR.remove(&id)` an entry
belonging to a LIVE delegate or contract instance in a concurrently
running test that had been issued the same id from the counter. Every
host function on the victim instance takes its "no MEM_ADDR entry"
branch and returns `ERR_NOT_IN_PROCESS`, which stdlib collapses into
"not found": `SecretResult(None)` for the secret tests, `error_code: -1`
for the V2 delegate contract tests.
This is why the failure needs the `wasm_runtime::` or full-suite filter
and never reproduces under `wasm_runtime::delegate` alone -- the
offending engine tests are filtered out there. It is a real cross-test
bug, not flakiness.
Captured directly rather than inferred:
FLAKEDBG set_secret id=77 ... val_len=1048576 ok=true
FLAKEDBG refresh_mem_addr NO-MEM_ADDR-ENTRY id=89
FLAKEDBG get_secret EARLY no MEM_ADDR id=89
panicked ... Expected SecretResult(Some(...)), got SecretResult(None)
`get_secret_len` for id=89 succeeded, so the entry existed and then
vanished between two host calls on the same live instance.
Fix makes the collision unrepresentable rather than merely unlikely:
- the single `NEXT_INSTANCE_ID` allocator now lives in `native_api.rs`,
next to the global maps whose keyspace it defines, exposed as
`next_instance_id()`
- `WasmEngine::create_instance` no longer accepts an id; it allocates
one and returns it in the `InstanceHandle`, so no caller can supply one
- `RunningInstance::new` reads `handle.id`; the local `INSTANCE_ID`
static is gone
New regression test `instance_ids_are_globally_unique_across_engines`:
engine A holds a live instance with a recorded MEM_ADDR entry while
engine B churns 64 instances; asserts B is never issued A's id and that
A's entry survives. Mutation-tested -- reverting `create_instance` to a
per-engine id fails it.
Pre-fix rate was 3 failures in 9 `cargo test -p freenet --lib
wasm_runtime::` runs.
Closes #4213
Closes #5023
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHwV1j9kGJEa5D6CxAyb6T
Rule Review: No issues foundRules checked: git-workflow.md, code-style.md, testing.md, contracts.md This PR removes the caller-supplied instance id from
No rule violations detected. Rule review against |
… call Review findings on #5251: - The paragraph in native_api.rs read as though every id reaching MEM_ADDR came from the allocator. It does not: four host functions take an instance id as a guest-supplied WASM parameter and look it up in the same maps. Say so explicitly rather than leaving a future reader with the wrong invariant. - Name the runner the collision actually needs. It requires two tests in one process, so it bites `cargo test`; CI runs `cargo nextest` (a process per test) and was never affected. Recording it as a general truth would send the next investigator down the wrong path if a delegate test flakes in CI. - Add `create_instance_allocates_its_own_instance_id`, a bounded-region source pin. The type system stops a CALLER passing an id, but nothing stopped `create_instance` itself from reverting to a per-engine counter, which is the same collision in a different place. Mutation-tested against exactly that change. - Trim the regression test's rustdoc to the property it actually pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JHwV1j9kGJEa5D6CxAyb6T
Review (Full tier: WASM runtime is a high-risk surface)Two independent blind Claude reviewers read the checked-out code: a skeptical lens (adversarial bug hunt) and a code-first lens (read the code before the description, flag intent/implementation mismatches). No external model pass, per the current review policy. The code-first reviewer found no discrepancies and independently confirmed the pin test in Fixed in c986d9a1. (High) The doc comment recorded the cause as more general than it is. The collision needs the two tests to share a process. CI runs 2. (Medium) "no caller can supply an id of its own" was too strong. Four host functions take an instance id as a guest-supplied WASM parameter and look it up in the same global maps: 3. (Medium) The regression test's rustdoc oversold it. A caller-supplied id is now a compile error, not something a runtime test can catch. The rustdoc now claims only what the test pins: the allocator is process-global, not per-engine. 5. (Low) Enforcement moved from one site to every backend, with no pin. Added Not changed, with reasons4. (Low) Gate 6. (Low) Also verified by the reviewers
Verification14 consecutive full For contrast, before the fix the narrower A re-verification campaign is running on [AI-assisted - Claude] |
Re-verification on
|
| runs | wasm_runtime failures |
other failures |
|---|---|---|
| 12 | 0 | 1 (topology::small_world_rand::tests::chi_squared_test, a statistical test unrelated to this change) |
Combined with the 14 runs on f222c81b9 reported above: 26 full-suite runs, zero occurrences of the flake. For contrast, before the fix the narrower cargo test -p freenet --lib wasm_runtime:: loop reproduced it 3 times in 9 runs on the same machine.
The other failures seen across the 26 runs were all pre-existing and unrelated: cross_connection_median_returns_some_when_a_peer_has_inflation (#5039, root cause already identified there), per_callsite_concurrent_writers_and_summary_no_deadlock (newly diagnosed in a comment on #5039), and the chi-squared test above.
[AI-assisted - Claude]
Problem
wasm_runtime::delegate::test::test_large_secret_dataand its siblings fail intermittently under the full parallel lib suite and never in isolation. Tracked as #4213 and #5023, with two distinct symptom shapes:test_large_secret_data/test_store_and_retrieve_secret:Expected SecretResult(Some(...)), got SecretResult(None)test_v2_delegate_update_existing_state:error_code: -1/ContractNotFoundBoth have the same cause.
MEM_ADDR,DELEGATE_ENVandCONTRACT_IOare process-global DashMaps keyed by WASM instance id, so the id namespace is process-global too. ButWasmEngine::create_instancetook a caller-suppliedid: i64, and onlyRunningInstance::newdrew one from the sharedINSTANCE_IDcounter. The engine unit tests inwasmtime_engine.rspicked their own ids:0..10_001intest_instance_limit_override_allows_many_instances0..STORE_REFRESH_THRESHOLD(500) in the three store-refresh tests999,0,1Those tests'
drop_instancecalls doMEM_ADDR.remove(&id). Running in the same test binary as the delegate tests, they removed the entry belonging to a live delegate or contract instance in a concurrently-running test that had been issued the same id from the counter. Every host function on the victim instance then took its "no MEM_ADDR entry" branch and returnedERR_NOT_IN_PROCESS, which the stdlib'sDelegateCtxcollapses into "not found" (result < 0→None).That is why the flake needs the
wasm_runtime::or full-lib filter and has never reproduced underwasm_runtime::delegatealone (#5023 reports 24 consecutive clean runs there): the offending engine tests are filtered out.Scope: the collision needs two tests to share a process, so it bites
cargo test, which is what AGENTS.md tells contributors to run and what both issues reported. CI runscargo nextest(ci.yml:546-556), which gives each test its own process, so CI was never affected.Evidence
Captured with temporary instrumentation on the host-function early-return paths, which are otherwise
tracing::warn!-only and invisible in a test run:get_secret_lenfor id 89 had already succeeded (no miss logged), so the entry existed and then vanished between two host calls on the same live instance. Both observed ids fall inside the0..500and0..10_001ranges the engine tests iterate.Rate before the fix on this machine: 3 failures in 9
cargo test -p freenet --lib wasm_runtime::runs, consistent with the ~1/8 in #5023.Approach
Renumbering the offending tests would fix today's collision and leave the hazard for the next test that picks an id. Instead the collision is made unrepresentable:
native_api.rsowns the singleNEXT_INSTANCE_IDallocator, sited next to the global maps whose keyspace it defines, exposed asnext_instance_id().WasmEngine::create_instanceno longer takes anid. It allocates one and returns it in theInstanceHandle, so no caller can supply one.RunningInstance::newreadshandle.id; the localINSTANCE_IDstatic is gone.Production behaviour is unchanged: the same counter semantics, the same monotonic ids, the same single production caller (
runtime.rs:73, still wrapped inclassify_resultper the #4864 invariant).This closes the
create_instancesurface only. The WASM ABI is a separate id surface, called out in theNEXT_INSTANCE_IDdocs so the invariant is not over-read.Testing
Two new tests:
instance_ids_are_globally_unique_across_engines: engine A holds a live instance with a recordedMEM_ADDRentry while engine B churns 64 instances the way the store-refresh and instance-limit tests do. Asserts B is never issued A's id and that A's entry survives. Mutation-tested: withcreate_instancereverted to a per-engine id (self.lifetime_instances as i64) it fails withleft: 0, right: 0.create_instance_allocates_its_own_instance_id: bounded-region source pin, following thecreate_instance_recovers_store_on_guest_entry_failureconvention already in this file. The type system stops a caller passing an id, but nothing stoppedcreate_instanceitself from reverting to a per-engine counter. Mutation-tested against exactly that change.Verification: 14 consecutive full
cargo test -p freenet --libruns with zerowasm_runtimefailures. Two of the 14 hit unrelated pre-existing flakes (cross_connection_median_returns_some_when_a_peer_has_inflation, already root-caused in #5039, andper_callsite_concurrent_writers_and_summary_no_deadlock, newly diagnosed in a comment there); the other 12 were fully green. A re-verification campaign on the final commit is in progress and the tally is posted in the discussion.cargo fmtclean.cargo clippy -- -D warningsreports only pre-existing findings, none in the four files touched.Closes #4213
Closes #5023
[AI-assisted - Claude]