Assertion failure: canSkip == true in [@ js::ExtractAwaitValue] when a Debugger onStep hook mutates the awaited promise between CanSkipAwait and MaybeExtractAwaitValue
Categories
(Core :: JavaScript: Debugger API, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: assertion, testcase)
Attachments
(4 files)
BytecodeEmitter::emitAwaitableExpr emits JSOp::CanSkipAwait followed by JSOp::MaybeExtractAwaitValue with no re-validation between them, and the interpreter's interrupt path runs DebugAPI::onSingleStep before dispatching the second op (Interpreter.cpp:1994), so a Debugger.Frame.onStep hook can add an own property to the awaited promise and make IsPromiseWithDefaultProperties false while the true pushed by CanSkipAwait is still on the operand stack. MaybeExtractAwaitValue consumes that stale boolean and calls js::ExtractAwaitValue, whose DEBUG re-check fails MOZ_ASSERT(canSkip == true) at js/src/builtin/Promise.cpp:8316 (reachable from a plain js --fuzzing-safe testcase); in a non-debug build the await instead completes synchronously from promise->value() after its precondition was invalidated.
Exploit Chain
- Create a debuggee global with newGlobal({newCompartment: true}) and attach a Debugger; set onEnterFrame so that frames for async function
fget an onStep hook. - In the debuggee, create a fulfilled promise
p = Promise.resolve(1)and runasync function f() { await null; await null; await p; }. The leading awaits ensureawait pexecutes in a resumed generator activation that is the sole activation, satisfying CanSkipAwait's activation checks (Promise.cpp:8235-8246). - Sweep the onStep count k so that on one attempt the hook fires exactly between JSOp::CanSkipAwait (which pushed true) and JSOp::MaybeExtractAwaitValue.
- The hook executes
g.p.then = 1, givingpan own property so promise->empty() is now false. - MaybeExtractAwaitValue reads the stale true and calls js::ExtractAwaitValue; its DEBUG re-check computes canSkip == false and MOZ_ASSERT(canSkip == true) fails at Promise.cpp:8316. In a non-debug build the value is extracted synchronously despite the invalidated precondition.
Affected Code
- js/src/frontend/BytecodeEmitter.cpp, line 6821-6828
- js/src/vm/Interpreter.cpp, line 1991-1998
- js/src/vm/Interpreter.cpp, line 3767-3791
- js/src/builtin/Promise.cpp, line 449-465
- js/src/builtin/Promise.cpp, line 8307-8329
Steps to Reproduce
- Build the SpiderMonkey debug shell with /app/mozconfigs/mozconfig.js.linux.debug on an unmodified tree.
- Run await_skip.js through js_shell_evaluator (equivalently: js --fuzzing-safe await_skip.js). No prefs, extra flags, or patches.
- Observe: Assertion failure: canSkip == true, at js/src/builtin/Promise.cpp:8316 in js::ExtractAwaitValue, called from js::Interpret (Interpreter.cpp:3787) inside AsyncFunctionResume / RunShellJobs.
Security Impact
- Severity: Low
- Attacker capability: Triggering the assertion requires the Debugger API, which the shell exposes under --fuzzing-safe but which is not available to web content. In debug builds the result is a deterministic assertion failure (process abort). In the non-debug ASAN shell the same testcase completes with no sanitizer report; the observed consequence is that the await resolves synchronously from promise->value() after the fast-path precondition has been invalidated. No memory corruption was demonstrated.
- Preconditions: Ability to run JS with access to the Debugger API (SpiderMonkey shell or privileged/devtools code) against the async function being stepped; the awaited value must be a fulfilled native promise with default properties at the time CanSkipAwait executes.
The full analysis report is attached (analysis.md).
| Reporter | ||
Comment 1•2 months ago
|
||
| Reporter | ||
Comment 2•2 months ago
|
||
| Reporter | ||
Comment 3•2 months ago
|
||
| Reporter | ||
Comment 4•2 months ago
|
||
Comment 5•2 months ago
|
||
This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.
Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.
Updated•2 months ago
|
Updated•2 months ago
|
Updated•2 months ago
|
Description
•