All writeups
Mozilla FirefoxGraphics, Text
sec-high CVE-2026-74945 RESOLVED FIXED

Uninitialized heap disclosure through a crafted web font

Vendor
Mozilla Firefox
Component
Core / Graphics: Text
Class
Uninitialized Memory Disclosure
CWE
908, 200
CVSS
6.5 Medium (Mozilla)
Fixed in
Firefox 154, ESR 115.39, ESR 140.14, ESR 153.1; Thunderbird 140.14, 153.1, 154
Interaction
None, page load only

Summary

Firefox sanitizes web fonts through OTS before use. OTS's expanding output stream allocates with a raw reallocator that never zeroes memory, and its Seek accepts any position up to the allocation size without zero-filling the gap. When a font's cmap carries a format-14 subtable with a large nonDefaultUVSOffset, OTS seeks forward and leaves an uninitialized-heap gap inside the emitted font. A companion format-4 subtable maps U+FFFF into that gap, so measuring the character with canvas measureText returns an advance derived from uninitialized heap, about two attacker-recoverable bytes per lookup over a roughly 64 KB window.

Root cause

Three independent defects line up. The OTS output allocator never zeroes memory. gfxOTSExpandingMemoryStream::Seek does not zero the skipped region, and forget() sizes the buffer to the highest offset reached, so any gap is handed to the caller carrying uninitialized heap. Finally the format-4 glyph lookup bounds-checks against the remainder of the whole cmap table rather than the subtable, so a jump into the neighbouring subtable's uninitialized bytes still passes the check.

Proof of concept

  • Serve a WOFF or OTF via @font-face whose cmap pairs a format-4 subtable, with an odd id_range_offset on the last 0xFFFF segment, with a format-14 subtable carrying a large nonDefaultUVSOffset, plus maxp.numGlyphs set to 65535 and a distinct hmtx advance per glyph.
  • From content, call ctx.measureText("￿").
  • The returned advance corresponds to a glyph id read from uninitialized heap. The gap's size and position are controlled by the chosen nonDefaultUVSOffset, so repeated measurements with different offsets can in principle be steered across a roughly 64 KB window; this was not exercised as a systematic scan here.
OTS sanitized font buffer, moz_xrealloc, no zero-fill written bytes UNINITIALIZED GAP ~64 KB of process heap format-4 subtable ctx.measureText("U+FFFF") the lookup lands inside the gap advance = 2 leaked heap bytes readable from JS, gap offset attacker-controlled
The lookup for U+FFFF lands inside the uninitialized gap. Its glyph advance, readable from script, leaks two bytes of heap per measurement.
// content-side trigger, advance leaks the uninitialized glyph id
const w = ctx.measureText("￿").width;   // maps into the uninitialized gap
// on a fixed build (154 / ESR 140.14+) the same measurement resolves to glyph id 0
Animated capture of make_font.py building the crafted font, analyze_leak.py reading the crafted cmap back, and od dumping the raw bytes that carry the overrun
Live capture: make_font.py builds the crafted font, analyze_leak.py reads it back, and od shows the raw bytes carrying the attack. 00 00 40 01 is the odd idRangeOffset, 00 00 80 00 is the format-14 nonDefaultUVSOffset that points past the emitted data.
Screen-recorded video, not a mockup: a headless Firefox 140.11.0esr (before the 140.14 fix) loads trigger.html, the Measure button is clicked, and canvas measureText reads the glyph advance back live. U+FFFF resolves to glyph id 1272 and U+FFFD to 2051, each encoding roughly two bytes recovered from uninitialized process heap. A patched build resolves all of these to glyph id 0.

Tools

  • Python 3 with fontTools and Brotli to assemble the OTF/WOFF and splice in the crafted cmap (make_font.py).
  • A small byte-level analyzer (analyze_leak.py) that verifies the crafted subtables directly from the file, independent of any font library.
  • Firefox, reading the leaked glyph advance back through the canvas measureText API.

Impact

A web-controllable, repeatable disclosure of uninitialized process heap that can reveal pointers or secrets. OTS runs in-process ahead of platform-specific rasterization and is not among the sandboxed libraries, so the sanitizer-level gap this bug relies on is not tied to a specific rasterization backend; this was reproduced on the build captured above and was not separately tested across every platform.

Fix

Two independent changes, each targeting one of the defects described above: zero the newly-allocated range in the OTS output stream so no gap is ever uninitialized, and check the format-4 subtable length so the glyph lookup cannot bound against the rest of the cmap. Either change on its own would close the specific chain demonstrated here.

View on Mozilla Bugzilla Proof of concept code