CVE-2025-6424
Overview
Background
- `FontFaceSet::Load`
- The DOM entry point for
FontFaceSet.load(font, text)inlayout/style/FontFaceSet.cpp. It locates theFontFaceobjects matching the requested font, kicks off their loading, and returns aPromisethat resolves when all matching faces have loaded. - `weakFaces` / `faces`
- The matching faces are first collected into
weakFaces(raw, non-owningFontFace*pointers), then copied into a localfacesarray that the subsequent loop iterates over. - `nsTArray` fallible vs. infallible append
- Gecko arrays support
AppendElement(x, fallible)(returns false on allocation failure) andAppendElement(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.
Attack Path
- Setup
Attacker page scripts a
FontFaceSetwith multiple matching@font-faceentries soFontFaceSet.load()iterates several faces. (web-reachable via the CSS Font Loading API) - Trigger loop
Calling
load()entersFontFaceSet::Load, which loops producing onePromiseper matching face and appending to the growingpromisesarray. - 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).
- Dangling access A subsequent iteration operates on storage that was reallocated or left inconsistent by the mid-loop append, dereferencing freed/stale memory.
- Primitive Use-after-free on the heap, reported as a potentially exploitable crash (CVSS 9.8); exploitability beyond crash is (inferred).
Impact Assessment
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 arraysLook for other loops in
layout/stylethat call back into DOM/JS-observable code while appending to a locally-growingnsTArraywithout pre-reserving capacity. - Fallible-append bail-out statesReview 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` loadingTrace the face-load path invoked inside the loop to confirm whether it can dispatch events or run microtasks that mutate the
FontFaceSet.
Patch
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);