# vm2: Host-returned Promise rejection can bypass vm2's unhandled-rejection hardening and terminate the host process

vm2 bridge returns host-realm rejected Promises to sandbox without marking them handled, so ignoring one triggers Node's unhandled-rejection exit and kills the host process.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: rexpository
- Disclosed: 2026-10-05
- CVE: CVE-2026-92954
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gjq8-xm47-88rc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gjq8-xm47-88rc
- Tags: area:cloud domain:infra domain:oss impact:dos kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:dos

**Target:** Node.js sandbox library (VM/NodeVM bridge)
**Root cause:** The localPromise hardening only attaches a swallow tail to sandbox-created Promises; host Promises crossing the bridge are wrapped for later .then/.catch but the raw host Promise is never marked handled, so an ignored rejection reaches Node's default unhandled-rejection throw.
**Steps:**
1. Create `new VM({ sandbox: { hostReject: () => Promise.reject(new Error('host-boom')) } })`.
2. Run `vm.run('hostReject(); 1')` without attaching `.catch()` to the returned Promise.
3. Observe process exits with `Error: host-boom` before a `setTimeout` log prints.
4. NodeVM variant: `new NodeVM({ require: { external: false, builtin: ['events'] } })`.
5. In sandbox: `const ee = new events.EventEmitter(); events.once(ee, 'message'); ee.emit('error', new Error('event-boom'));`.
6. Process exits status 1 with `triggerUncaughtException(err, true /* fromPromise */)`.
7. Control: attaching `.catch(() => {})` to the returned Promise keeps the process alive.
**How found:** Noticed the GHSA-hw58 fix only hardened sandbox-created localPromise instances via a swallow tail, then reasoned that host Promises returned across the bridge apply path never get a host-side rejection handler unless sandbox code attaches .then/.catch; confirmed by finding the events builtin wrapper re-exports host EventEmitter.once as a ready-made rejected-Promise primitive.
**Exploitation:** Attacker with sandbox code execution calls any embedder-exposed host function returning a rejected Promise (or events.once on an emitter that emits error) and ignores the result; Node's --unhandled-rejections=throw terminates the host worker. Replayable after each restart, so a single small request repeatedly kills multi-tenant workers.
**Insight:** When auditing sandbox/membrane fixes, check both directions of the boundary: a hardening applied to objects created inside the sandbox often misses host-realm objects crossing in. Look for builtin wrappers that re-export host async APIs (events.once) as free rejected-Promise generators.

