# vm2: Host Promise rejection from an exposed constructor can terminate the vm2 host process

vm2's construct trap forwards a host constructor's rejected Promise without marking it handled, so one guest expression crashes the Node host process.

- Payout: Research
- Severity: high
- Program: vm2
- Found by (vendor credit): rexpository
- Found by: rexpository
- Disclosed: 2026-10-05
- CVE: CVE-2026-100722
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-2v2p-6j97-cjg9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-2v2p-6j97-cjg9
- Tags: area:oss domain:oss impact:dos kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:regression technique:source-review vuln:dos

**Target:** vm2 sandbox bridge (BaseHandler.construct) in Node.js
**Root cause:** BaseHandler.apply calls markHostPromiseHandled(ret) on host-returned Promises, but the adjacent BaseHandler.construct path returns thisFromOtherWithFactory without the same sanitizer, so a rejected host Promise crosses the bridge unhandled.
**Steps:**
1. Expose a constructable host function whose constructor returns a rejected native Promise: `function HostReject() { return Promise.reject(new Error('constructed-host-boom')); }`
2. Create `new VM({ sandbox: { HostReject } })` and run guest code `new HostReject(); 1` via `vm.run`.
3. Run under `NODE_OPTIONS=--unhandled-rejections=strict node --abort-on-uncaught-exception construct-promise-poc.js vulnerable`; process aborts (exit 139) before printing ALIVE.
4. Control: guest code `new HostReject().catch(function () {}); 1` survives, confirming the unhandled rejection is the cause.
**How found:** Noticed GHSA-gjq8 hardened the apply boundary with markHostPromiseHandled but the adjacent construct return path lacked the equivalent call; tested the same host-rejection scenario through `new` and compared against a `.catch()` control.
**Exploitation:** Attacker submits one expression to VM.run; the constructed host Promise rejection is promoted to an uncaught exception under Node's strict unhandled-rejection policy, terminating the host process (plugin worker, notebook kernel, queue consumer).
**Insight:** When a fix adds a sanitizer at one bridge trap (apply), audit sibling traps (construct, get, set) for the same missing invariant — the fix surface is per-trap, not per-bug-class.

