Medium CVSS 4 firefox UAF 🔧 Commit mapped

Overview

Medium
Severity
4
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionParsing a JavaScript module as JSON could, under some circumstances, cause cross-compartment access, which may result in a use-after-free.
ComponentSpiderMonkey
Bug ClassUAF
Tracker1929623
Fix commit481505b2cb40 (firefox) +24/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedNils Bars
Disclosed2025-01-07

Background

Synthetic module
an internally-generated ModuleObject (such as a JSON module) whose evaluation is handled by SyntheticModuleEvaluate rather than by running user bytecode.
Compartment
SpiderMonkey’s memory/security boundary grouping objects that may share direct references; touching an object from the wrong compartment (a cross-compartment access) is unsafe and can dereference freed memory.
`MutableHandle<Value>`
a rooted, writable output slot passed by the caller into which a callee stores a result Value.
`AsyncFunctionReturned`
the helper that resolves an async/module result PromiseObject with a supplied Value.

Root Cause Analysis

When evaluating a JSON module, SyntheticModuleEvaluate received the caller’s MutableHandle<Value> result output slot and passed that same result into AsyncFunctionReturned to resolve the module’s result promise. At that point result was the caller-provided out-parameter whose contents were not owned by the synthetic-module evaluation and, when moduleEvaluate was invoked across a compartment boundary (for example via with (newGlobal()) this.moduleEvaluate(...)), could hold a Value belonging to — or backed by memory freed in — a different compartment. Resolving the promise with that aliased slot performed a cross-compartment access on a possibly-dangling object, yielding a use-after-free.

The fix recognizes that JSON synthetic modules always produce undefined, so it resolves the promise with the compartment-safe constant JS::UndefinedHandleValue instead of the aliased result slot, and only writes the freshly-created resultPromise back through the (renamed) rval output at the end. Because a constant undefined handle carries no cross-compartment pointer, the invalid access can no longer occur.

Key insight
The single mistake was resolving the synthetic module’s result promise with the caller’s raw output slot, which could alias a cross-compartment (potentially freed) Value; the fix passes the invariant JSON result JS::UndefinedHandleValue instead, eliminating the dangerous alias.

Attack Path

  1. Create a foreign compartment Attacker script calls newGlobal to obtain an object living in a separate compartment from the current global.
  2. Reach evaluation cross-compartment Using with (b()) the attacker binds the foreign global’s scope and invokes this.moduleEvaluate(parseModule("45172", "", "json")), driving JSON-module evaluation across the compartment boundary.
  3. Enter synthetic evaluation Evaluation dispatches to SyntheticModuleEvaluate, which forwards the caller’s result output slot to AsyncFunctionReturned.
  4. Trigger cross-compartment access Resolving the result promise reads the aliased result slot, which references an object owned by / freed in the other compartment, dereferencing invalid memory.
  5. Achieve use-after-free The stale cross-compartment reference is used, producing a use-after-free that an attacker can shape for memory corruption.

Impact Assessment

An attacker running JavaScript in the content process gains a use-after-free via a controlled cross-compartment access during JSON-module evaluation, which can lead to memory corruption and potential exploitation. Reaching it requires the ability to evaluate a JSON module across a compartment boundary; in the shipping browser this is constrained, which is consistent with the moderate severity rating.

Files Changed

  • js/src/jit-test/tests/modules/evaluation-result.js
  • js/src/testcase.js
  • js/src/vm/Modules.cpp

Audit Directions

  • Output-slot aliasing
    Review other callees that pass a caller-supplied MutableHandle<Value> output parameter straight into resolution/consumption helpers before writing their own result, where the slot may still hold foreign data.
  • Cross-compartment evaluation entry points
    Audit module and promise resolution paths (AsyncFunctionReturned, synthetic/JSON module evaluation) for Values that may originate in another compartment when invoked via with/newGlobal-style cross-realm calls.
  • Invariant-known results
    Look for cases where a result is by construction constant (e.g. always undefined) yet a mutable, externally-sourced slot is used instead of a safe constant handle like JS::UndefinedHandleValue.
diff --git a/js/src/testcase.js b/js/src/testcase.js
new file mode 100644
index 00000000000..41fa48273cb
--- /dev/null
+++ b/js/src/testcase.js
@@ -0,0 +1,4 @@
+a = "".startsWith("")
+function b() { return newGlobal(b) }
+b.newCompartment = a
+with (b()) this.moduleEvaluate(parseModule("45172", "", "json"))
diff --git a/js/src/vm/Modules.cpp b/js/src/vm/Modules.cpp
index 1a017f0308b..67506114e47 100644
--- a/js/src/vm/Modules.cpp
+++ b/js/src/vm/Modules.cpp
@@ -46,9 +46,9 @@ using mozilla::Utf8Unit;
 
 static bool ModuleLink(JSContext* cx, Handle<ModuleObject*> module);
 static bool ModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module,
-                           MutableHandle<Value> result);
+                           MutableHandle<Value> rval);
 static bool SyntheticModuleEvaluate(JSContext* cx, Handle<ModuleObject*> module,
-                                    MutableHandle<Value> result);
+                                    MutableHandle<Value> rval);
 
 ////////////////////////////////////////////////////////////////////////////////
 // Public API
@@ -1493,8 +1493,8 @@ static bool InnerModuleLinking(JSContext* cx, Handle<ModuleObject*> module,
 
 static bool SyntheticModuleEvaluate(JSContext* cx,
                                     Handle<ModuleObject*> moduleArg,
-                                    MutableHandle<Value> result) {
-  // Steps 1-12 happens elsewhere in the engine.
+                                    MutableHandle<Value> rval) {
+  // Steps 1-12 happen elsewhere in the engine.
 
   // Step 13. Let pc be ! NewPromiseCapability(%Promise%).
   Rooted<PromiseObject*> resultPromise(cx, CreatePromiseObjectForAsync(cx));
@@ -1502,15 +1502,18 @@ static bool SyntheticModuleEvaluate(JSContext* cx,
     return false;
   }
 
+  // Since the only synthetic modules we support are JSON modules, result is
+  // always |undefined|.
+
   // Step 14. IfAbruptRejectPromise(result, pc) (Skipped)
 
   // 15. Perform ! pc.[[Resolve]](result).
-  if (!AsyncFunctionReturned(cx, resultPromise, result)) {
+  if (!AsyncFunctionReturned(cx, resultPromise, JS::UndefinedHandleValue)) {
     return false;
   }
 
   // 16. Return pc.[[Promise]].
-  result.set(ObjectValue(*resultPromise));
+  rval.set(ObjectValue(*resultPromise));
   return true;
 }
 
diff --git a/js/src/jit-test/tests/modules/evaluation-result.js b/js/src/jit-test/tests/modules/evaluation-result.js
new file mode 100644
index 00000000000..6020403a0f2
--- /dev/null
+++ b/js/src/jit-test/tests/modules/evaluation-result.js
@@ -0,0 +1,11 @@
+async function parseAndEvaluate(source, filename, kind) {
+    let m = parseModule(source, filename, kind);
+    moduleLink(m);
+    return await moduleEvaluate(m);
+}
+
+(async () => {
+  assertEq(await parseAndEvaluate("[]", "", "js"), undefined);
+  assertEq(await parseAndEvaluate("[]", "", "json"), undefined);
+})();
+drainJobQueue();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/js/src/jit-test/tests/modules/evaluation-result.js b/js/src/jit-test/tests/modules/evaluation-result.js
new file mode 100644
index 00000000000..6020403a0f2
--- /dev/null
+++ b/js/src/jit-test/tests/modules/evaluation-result.js
@@ -0,0 +1,11 @@
+async function parseAndEvaluate(source, filename, kind) {
+    let m = parseModule(source, filename, kind);
+    moduleLink(m);
+    return await moduleEvaluate(m);
+}
+
+(async () => {
+  assertEq(await parseAndEvaluate("[]", "", "js"), undefined);
+  assertEq(await parseAndEvaluate("[]", "", "json"), undefined);
+})();
+drainJobQueue();
Loading diff…