Closed Bug 2043298 Opened 5 months ago Closed 4 months ago

Assertion failure: this->is<T>() in FinishLoadingImportedModuleFailed via Debugger

Categories

(Core :: JavaScript Engine, defect, P2)

x86_64
Linux
defect

Tracking

()

RESOLVED FIXED
154 Branch
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)

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).

Blocks: sm-security
Severity: -- → S3
Priority: -- → P2

Bug 2044167 would remove onPromiseSettled

(In reply to Matthew Gaudet (he/him) [:mgaudet] from comment #2)
Let's do that.

Depends on: 2044167

Fixed by removal of onPromiseSettled API in bug 2044167.

Status: NEW → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Assignee: nobody → bthrall
Target Milestone: --- → 154 Branch
Group: javascript-core-security → core-security-release
QA Whiteboard: [sec] [qa-triage-done-c155/b154]
Whiteboard: [adv-main154+r]
Whiteboard: [adv-main154+r] → [adv-main154+r][fixed by 2044167]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: