High CVSS 9.8 firefox UAF 🔧 Commit mapped

Overview

High
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionA use-after-free in FontFaceSet resulted in a potentially exploitable crash.
ComponentLayout
Bug ClassUAF
Tracker1966423
Fix commit094ddb8fc2c1 (firefox) +3/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedLJP and HexRabbit (DEVCORE Research Team)
Disclosed2025-06-24

Background

`FontFaceSet::Load`
The DOM entry point for FontFaceSet.load(font, text) in layout/style/FontFaceSet.cpp. It locates the FontFace objects matching the requested font, kicks off their loading, and returns a Promise that resolves when all matching faces have loaded.
`weakFaces` / `faces`
The matching faces are first collected into weakFaces (raw, non-owning FontFace* pointers), then copied into a local faces array that the subsequent loop iterates over.
`nsTArray` fallible vs. infallible append
Gecko arrays support AppendElement(x, fallible) (returns false on allocation failure) and AppendElement(x) (infallible; asserts capacity). SetCapacity(n, fallible) pre-reserves the backing buffer so later appends cannot reallocate.
`Promise::All`
Aggregates the per-face promises into a single promise returned to script.

Root Cause Analysis

FontFaceSet::Load builds a local promises array by looping over faces, calling each face’s load routine (which sets aRv and yields a Promise), and then appending that promise to promises with the fallible AppendElement(promise, fallible). The patch makes two coupled changes: before the loop it now also pre-reserves the promises buffer with promises.SetCapacity(weakFaces.Length(), fallible), and inside the loop it replaces the fallible append-and-bail with an infallible promises.AppendElement(promise). The net effect is that promises is sized once up front and its backing buffer is never reallocated while the loop is running.

The violated invariant is that the backing storage used by the loop must remain stable across each iteration. Producing a per-face Promise runs through the Promise/JS machinery and is reachable from author-observable state, so an iteration can re-enter and perturb the set during the loop (the diff does not show the body of the face-loading call, so the precise re-entrancy path is inferred). With the old incremental fallible append, each iteration could grow and reallocate promises, and a mid-loop failure path returned nullptr while partially-populated; the reservation + infallible append removes the reallocation entirely so no stale pointer into the moved buffer can be dereferenced. The advisory labels this a use-after-free (CWE-416); the diff substantiates a reallocation/ordering hardening around the promise accumulation rather than showing the freed object directly — the freed-memory step itself is (inferred) and not shown by the diff.

Key insight
The loop accumulated per-face promises into a dynamically-growing array whose backing buffer could reallocate (or be left half-built on a fallible-append failure) while face loading ran re-entrant, observable code; pre-reserving capacity and making the append infallible guarantees the buffer is allocated exactly once and never moves mid-loop.

Attack Path

  1. Setup Attacker page scripts a FontFaceSet with multiple matching @font-face entries so FontFaceSet.load() iterates several faces. (web-reachable via the CSS Font Loading API)
  2. Trigger loop Calling load() enters FontFaceSet::Load, which loops producing one Promise per matching face and appending to the growing promises array.
  3. Re-enter during load Producing a face’s promise runs observable JS/Promise machinery that mutates set/collection state while the loop is in progress (inferred — the load body is not in the diff).
  4. Dangling access A subsequent iteration operates on storage that was reallocated or left inconsistent by the mid-loop append, dereferencing freed/stale memory.
  5. Primitive Use-after-free on the heap, reported as a potentially exploitable crash (CVSS 9.8); exploitability beyond crash is (inferred).

Impact Assessment

This executes in the content/renderer process and needs no special privilege — any web page using the CSS Font Loading API can reach FontFaceSet.load(). The achieved primitive is a use-after-free (advisory) manifesting as a potentially exploitable crash; the diff concretely substantiates only the reallocation/ordering hazard in promises accumulation, so the step from crash to controlled corruption and RCE is (inferred), not proven by this diff. There is no additional user interaction or gating beyond loading attacker content.

Files Changed

  • layout/style/FontFaceSet.cpp

Audit Directions

  • Grow-during-loop in style/layout arrays
    Look for other loops in layout/style that call back into DOM/JS-observable code while appending to a locally-growing nsTArray without pre-reserving capacity.
  • Fallible-append bail-out states
    Review sites that return nullptr/throw from the middle of a partially-populated array for leaving objects or callers in an inconsistent lifetime state.
  • Re-entrancy of `FontFace` loading
    Trace the face-load path invoked inside the loop to confirm whether it can dispatch events or run microtasks that mutate the FontFaceSet.
diff --git a/layout/style/FontFaceSet.cpp b/layout/style/FontFaceSet.cpp
index 984bf792211..0891519f866 100644
--- a/layout/style/FontFaceSet.cpp
+++ b/layout/style/FontFaceSet.cpp
@@ -148,7 +148,8 @@ already_AddRefed<Promise> FontFaceSet::Load(JSContext* aCx,
     if (aRv.Failed()) {
       return nullptr;
     }
-    if (!faces.AppendElements(weakFaces, fallible)) {
+    if (!faces.AppendElements(weakFaces, fallible) ||
+        !promises.SetCapacity(weakFaces.Length(), fallible)) {
       aRv.Throw(NS_ERROR_FAILURE);
       return nullptr;
     }
@@ -159,10 +160,7 @@ already_AddRefed<Promise> FontFaceSet::Load(JSContext* aCx,
     if (aRv.Failed()) {
       return nullptr;
     }
-    if (!promises.AppendElement(promise, fallible)) {
-      aRv.Throw(NS_ERROR_FAILURE);
-      return nullptr;
-    }
+    promises.AppendElement(promise);
   }
 
   return Promise::All(aCx, promises, aRv);
Loading diff…