Repository navigation
Allocate env ids per application and let stores terminate each other's envs - #108
Merged
Merged
Conversation
Native env ids and owner-scope ids were allocated per NapiEnv, which means per Wasmer store, and every guest import resolved them only through the caller's store. A WASIX application runs one store per thread over one shared guest memory, so two threads got the same ids for different environments. A scope token from another thread could alias, and release, the caller's own environment. And no guest could terminate or interrupt an environment owned by another thread of its own application, such as a runaway worker. The application's env registry, already shared by its stores for NapiRuntimeControl::terminate_all(), becomes an EnvRegistry: the next native env id and a map from env id to bridge env. Native ids are allocated there and never reused. An env has one owner scope, so its scope id is its env id. The host-JS backend keeps its own allocation. unofficial_napi_terminate_execution and unofficial_napi_request_interrupt resolve their target through that registry (NapiEnv::with_control_env), holding its lock only while the request is handed to the bridge, so the target cannot be released meanwhile. The bridge accepts both from a foreign thread. The interrupt callback runs later on the target's owner thread, in the target store's callback context. Every other import, including cancel and release, still resolves only through the caller's own store. Separate applications keep separate registries. tests/concurrent_stores.rs checks that ids are unique per application and not reused, that a foreign scope token cannot release the caller's env, and that a worker store's env running JavaScript on another thread can be interrupted (in its own store) and terminated from the main store, but not used, cancelled or released by it. Without this change both tests fail: the two stores get the same env id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An interrupt runs a guest function chosen by the caller (a table index) in the target env's instance. Letting any store of the application interrupt any env was only sound if every store sharing a NapiCtx were a thread of the same module, sharing its table and memory. NapiRuntimeHooks makes no such promise, and env ids are sequential. So unofficial_napi_request_interrupt resolves its target through the caller's own store again, as on main. Terminate remains the one cross-store control: it runs no guest code. NapiEnv::terminate_app_env replaces the general with_control_env closure. NapiRuntimeHooks documents the trust model: one context per application, and unrelated untrusted guests get separate contexts. another_store_can_terminate_but_not_interrupt_or_use_an_env now checks that another store's interrupt is refused, and that the worker can still interrupt its own env. It fails on the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The env registry lock is shared by every store of an application. Panicking while holding it poisoned it, so every later env creation, release and terminate_all of the application panicked too. Release it before panicking. Also drop "interrupt" from two comments: since the previous commit, interrupts stay within the caller's own store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
syrusakbary
force-pushed
the
feat/concurrent-stores
branch
from
October 28, 2026 03:13
3ca9482 to
37a4643
Compare
syrusakbary
added a commit
that referenced
this pull request
Oct 28, 2026
Brings in #108 (env ids per application; stores can terminate each other's envs) and #124 (an env can't be released from guest code it called into). Releasing an env tears down its agent on this branch, so #124's count of guest code in progress is per agent: guest code entered through any env of the agent keeps the isolate's engine frames on the stack. GuestCallScope counts on the agent, and DisposeBridgeState and snapi_bridge_unofficial_env_release_refused refuse while it is non-zero or the agent is closing. host-JS: #124's thread-wide GUEST_CALL_DEPTH and call_guest come in; ACTIVE_HOST_JS_ENV, which this branch removed, stays out. docs/realm-bound-env.md records #108 as landed and #124's count as agent teardown state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
syrusakbary
added a commit
that referenced
this pull request
Oct 28, 2026
Brings in #123, #108 and #124 through Phase 0. Rust: #108 allocates native env ids per application, so a realm env takes its id from the application's registry too, and never shares one with another store's env. It still gets no registry entry. Terminate resolves through the registry since #108, so terminating one of the caller's realm envs goes through its principal env, which stops every env of the isolate as before. host-JS: releasing an env first asks whether guest code is running (#124), then releases its realm envs as Phase 1 does through free_env. The bridge needed no change: #124's count of running guest calls is per agent, so it covers realm envs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #107, now in
main: stopping an environment from another thread only helps if the running environment does not hold a process-wide lock. This branch is rebased ontomainand targets it.Problem
Native env ids and owner-scope ids were allocated per
NapiEnv, that is, per Wasmer store. Every guest import resolved them only through the caller's store. A WASIX application with worker threads has one store per thread, all sharing one linear memory. As a result:1. An id stored in shared guest memory then names a different environment depending on which thread reads it.release_envwith another thread's token can release the caller's own environment.NapiRuntimeControl::terminate_all(), which stops everything.Change
Rust only:
src/env.rs,src/ctx.rs,src/guest/napi.rs.wasm32+js) backend keeps its current allocation.unofficial_napi_terminate_executionresolves its target through that registry (NapiEnv::terminate_app_env), so any store of the application can terminate any of its environments.napi_invalid_arg. An interrupt runs a guest function the caller picks (a table index) in the target's instance. Allowing that across stores is only sound when every store sharing aNapiCtxis a thread of the same module, and nothing guarantees that.NapiRuntimeHooksnow documents it: oneNapiCtxper application, and separate contexts for unrelated untrusted guests. DifferentNapiCtxs keep separate registries and cannot reach each other's environments.Tests
tests/concurrent_stores.rsreuses the WAT guest from #107.env_ids_are_unique_per_application. It checks that:another_store_can_terminate_but_not_interrupt_or_use_an_env. A worker store on its own thread interrupts its own env (the callback runs in its own store), waits inAtomics.wait, then spins. Meanwhile the main store, on the test thread:napi_invalid_argfornapi_get_undefined, interrupt, cancel and release on the worker's env;Afterwards the retired id is rejected, and the main env was never stopped.
Both tests fail on
mainwithout these commits, because the two stores get the same env id1. The interrupt check also fails on the first commit, which let another store interrupt. This branch adds the suite to the CI job from #107.After rebasing onto
main(with #121, #122 and #123): lib tests 54/54,concurrent_envs10/10,concurrent_stores2/2 andguest_callbacks4/4; clippy shows no warnings.Before the rebase (macOS arm64, Rust 1.96.1, V8 11.9.9 prebuilt):
concurrent_stores: 2/2, plus 20/20 repeated runs of the whole binary.concurrent_envs: 10/10, plus 20/20 repeated runs.v8_host_control: 3/3.snapi_jswasm-bindgen tests: 6/6.cargo fmtis clean, and clippy shows no new warnings.Behavior changes
1will see other values; the API never guaranteed that.Known follow-up (not changed here)
Message ids are process-wide, sequential and carry no owner check. This is already the case on
main; see #107.Relation to #93
#93 changes neither id allocation nor cross-store control: its terminate and interrupt imports still resolve through the caller's store. This PR does not depend on #93's other fixes (fatal-error exits, callback exit propagation, external-buffer ownership, reentrant release rejection). It needs only one thing from the bridge: accepting a terminate from another thread while the target runs JavaScript. #107 provides that, and #93's terminate path would too.
Size: 5 files, +274/−46 against
main:env.rs+80/−32,ctx.rs+14/−10,guest/napi.rs+2/−2)🤖 Generated with Claude Code