Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionMemory safety bug fixed in Firefox 152
ComponentGraphics
Bug ClassLogic Error
Tracker2037290
Fix commitc9f0db7ce573 (firefox) +100/-5
CISA KEVNot listed
CreditedZijie Zhao
Disclosed2026-06-16

Background

CFF (Compact Font Format)
a table-based font representation whose glyph outlines and metadata are described by a nested set of dictionaries and indexes.
Cairo CFF subsetting
the cairo-cff-subset.c code path that parses an embedded CFF font and rebuilds a reduced (“subset”) copy for PDF/PostScript output.
CFF operator offset
an integer operand (e.g. for CHARSTRINGS_OP, FDSELECT_OP, FDARRAY_OP, PRIVATE_OP) that points, as a byte offset from the start of the font data, at another CFF structure.
CID-keyed font
a CFF variant (font->is_cid) that adds an FDSelect/FDArray layer of per-glyph private dictionaries, exercising extra offset-following parse paths.

Root Cause Analysis

When parsing a top dictionary, cairo_cff_font_read_top_dict and its callee cairo_cff_font_read_cid_fontdict read attacker-controlled offset operands via decode_integer and then formed pointers such as font->data + offset without validating that the resulting pointer stayed inside the [font->data, font->data_end] buffer. The invariant that every derived parse pointer must lie within the mapped font data was violated: a negative or oversized offset produced a pointer before the start or far past the end of the allocation, which was then dereferenced by cff_index_read, cairo_cff_font_read_fdselect, cairo_cff_font_read_cid_fontdict, and cairo_cff_font_read_private_dict. The pre-existing charset check was also incomplete because it only tested the upper bound (>= font->data_end) and not the lower bound, so a small/negative offset still slipped through.

The fix adds explicit bounds checks — comparing the computed pointer p against both font->data and font->data_end, and comparing the raw offset against 0 and font->data_length in the CID fontdict path — and returns CAIRO_INT_STATUS_UNSUPPORTED before any out-of-bounds pointer is used. This works because it rejects the malformed font at parse time rather than letting a wild pointer reach downstream readers.

Key insight
The core mistake was trusting untrusted CFF offset operands and turning them directly into dereferenceable pointers without a bounds check against the font buffer; the fix validates each font->data + offset pointer (and the raw offset) against the buffer’s start and end before use.

Attack Path

  1. Craft a malicious font An attacker builds a CFF font whose top-dict offset operands (charstrings, charset, FDSelect, FDArray, or Private) encode negative or out-of-range values.
  2. Deliver it to the victim The font is embedded in a document or served to a page so that Firefox routes it through cairo’s CFF subsetting during PDF/PostScript output.
  3. Reach the unchecked offset cairo_cff_font_read_top_dict decodes the offset and computes font->data + offset (or cairo_cff_font_read_cid_fontdict uses the raw offset) with no bounds validation.
  4. Trigger out-of-bounds access Downstream readers such as cff_index_read or cairo_cff_font_read_private_dict dereference the wild pointer, reading memory outside the font allocation.

Impact Assessment

An attacker gains an out-of-bounds memory read (a memory-safety violation) during CFF font subsetting, in whatever Firefox process performs the font parsing for PDF/PostScript generation. The precondition is that the victim processes an attacker-supplied CFF font through cairo’s subsetting path; Mozilla rates the impact moderate/medium, consistent with an information-disclosure or crash primitive rather than a demonstrated write.

Changed Functions

FunctionChangeNotes
if
gfx/cairo/cairo/src/cairo-cff-subset.c
modified
if
gfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch
modified

Files Changed

  • gfx/cairo/cairo/src/cairo-cff-subset.c
  • gfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch

Audit Directions

  • Offset-to-pointer conversions
    Audit every base + offset computed from parsed font/file data and confirm both a lower (< base) and upper (> end) bound is enforced before dereference.
  • One-sided bounds checks
    Search for range tests that check only >= end (like the old charset check) and verify a matching lower-bound test guards against negative or underflowing offsets.
  • Signed offset operands
    Review decode_integer consumers where a signed offset can be negative or exceed font->data_length, since a cast to unsigned long or pointer arithmetic can otherwise produce a wild address.
diff --git a/gfx/cairo/cairo/src/cairo-cff-subset.c b/gfx/cairo/cairo/src/cairo-cff-subset.c
index 6c01ed4ef87..6e3a4340c4e 100644
--- a/gfx/cairo/cairo/src/cairo-cff-subset.c
+++ b/gfx/cairo/cairo/src/cairo-cff-subset.c
@@ -1086,6 +1086,10 @@ cairo_cff_font_read_cid_fontdict (cairo_cff_font_t *font, unsigned char *ptr)
         }
         operand = decode_integer (operand, &size);
         decode_integer (operand, &offset);
+        if (unlikely (offset < 0 || (unsigned long)offset > font->data_length)) {
+            status = CAIRO_INT_STATUS_UNSUPPORTED;
+            goto fail;
+        }
         status = cff_dict_init (&font->fd_private_dict[i]);
 	if (unlikely (status))
             goto fail;
@@ -1205,6 +1209,8 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
     operand = cff_dict_get_operands (font->top_dict, CHARSTRINGS_OP, &size);
     decode_integer (operand, &offset);
     p = font->data + offset;
+    if (unlikely (p < font->data || p > font->data_end))
+        return CAIRO_INT_STATUS_UNSUPPORTED;
     status = cff_index_read (&font->charstrings_index, &p, font->data_end);
     if (unlikely (status))
         goto fail;
@@ -1217,7 +1223,7 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
 
 	 decode_integer (operand, &offset);
 	 font->charset = font->data + offset;
-	 if (font->charset >= font->data_end)
+	 if (unlikely (font->charset < font->data || font->charset >= font->data_end))
 	      return CAIRO_INT_STATUS_UNSUPPORTED;
     }
 
@@ -1227,27 +1233,36 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
     if (font->is_cid) {
         operand = cff_dict_get_operands (font->top_dict, FDSELECT_OP, &size);
         decode_integer (operand, &offset);
-        status = cairo_cff_font_read_fdselect (font, font->data + offset);
+        p = font->data + offset;
+        if (unlikely (p < font->data || p > font->data_end))
+            return CAIRO_INT_STATUS_UNSUPPORTED;
+        status = cairo_cff_font_read_fdselect (font, p);
 	if (unlikely (status))
 	    goto fail;
 
         operand = cff_dict_get_operands (font->top_dict, FDARRAY_OP, &size);
         decode_integer (operand, &offset);
-        status = cairo_cff_font_read_cid_fontdict (font, font->data + offset);
+        p = font->data + offset;
+        if (unlikely (p < font->data || p > font->data_end))
+            return CAIRO_INT_STATUS_UNSUPPORTED;
+        status = cairo_cff_font_read_cid_fontdict (font, p);
 	if (unlikely (status))
 	    goto fail;
     } else {
         operand = cff_dict_get_operands (font->top_dict, PRIVATE_OP, &size);
         operand = decode_integer (operand, &size);
         decode_integer (operand, &offset);
-	status = cairo_cff_font_read_private_dict (font,
+        p = font->data + offset;
+        if (unlikely (p < font->data || p > font->data_end))
+            return CAIRO_INT_STATUS_UNSUPPORTED;
+        status = cairo_cff_font_read_private_dict (font,
                                                    font->private_dict,
 						   &font->local_sub_index,
 						   &font->local_sub_bias,
 						   &font->local_subs_used,
                                                    &font->default_width,
                                                    &font->nominal_width,
-						   font->data + offset,
+						   p,
 						   size);
 	if (unlikely (status))
 	    goto fail;
diff --git a/gfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch b/gfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch
new file mode 100644
index 00000000000..b8ac128f086
--- /dev/null
+++ b/gfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch
@@ -0,0 +1,80 @@
+commit 10d78e49f5d39fe5ef4925a271e721bb7eef22d3
+Author: Jonathan Kew <jkew@mozilla.com>
+Date:   Wed May 6 13:21:19 2026 +0100
+
+    Bug 2037290 - Check more offsets during cairo CFF subsetting.
+
+diff --git a/gfx/cairo/cairo/src/cairo-cff-subset.c b/gfx/cairo/cairo/src/cairo-cff-subset.c
+index 6c01ed4ef876..6e3a4340c4ec 100644
+--- a/gfx/cairo/cairo/src/cairo-cff-subset.c
++++ b/gfx/cairo/cairo/src/cairo-cff-subset.c
+@@ -1086,6 +1086,10 @@ cairo_cff_font_read_cid_fontdict (cairo_cff_font_t *font, unsigned char *ptr)
+         }
+         operand = decode_integer (operand, &size);
+         decode_integer (operand, &offset);
++        if (unlikely (offset < 0 || (unsigned long)offset > font->data_length)) {
++            status = CAIRO_INT_STATUS_UNSUPPORTED;
++            goto fail;
++        }
+         status = cff_dict_init (&font->fd_private_dict[i]);
+ 	if (unlikely (status))
+             goto fail;
+@@ -1205,6 +1209,8 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
+     operand = cff_dict_get_operands (font->top_dict, CHARSTRINGS_OP, &size);
+     decode_integer (operand, &offset);
+     p = font->data + offset;
++    if (unlikely (p < font->data || p > font->data_end))
++        return CAIRO_INT_STATUS_UNSUPPORTED;
+     status = cff_index_read (&font->charstrings_index, &p, font->data_end);
+     if (unlikely (status))
+         goto fail;
+@@ -1217,7 +1223,7 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
+ 
+ 	 decode_integer (operand, &offset);
+ 	 font->charset = font->data + offset;
+-	 if (font->charset >= font->data_end)
++	 if (unlikely (font->charset < font->data || font->charset >= font->data_end))
+ 	      return CAIRO_INT_STATUS_UNSUPPORTED;
+     }
+ 
+@@ -1227,27 +1233,36 @@ cairo_cff_font_read_top_dict (cairo_cff_font_t *font)
+     if (font->is_cid) {
+         operand = cff_dict_get_operands (font->top_dict, FDSELECT_OP, &size);
+         decode_integer (operand, &offset);
+-        status = cairo_cff_font_read_fdselect (font, font->data + offset);
++        p = font->data + offset;
++        if (unlikely (p < font->data || p > font->data_end))
++            return CAIRO_INT_STATUS_UNSUPPORTED;
++        status = cairo_cff_font_read_fdselect (font, p);
+ 	if (unlikely (status))
+ 	    goto fail;
+ 
+         operand = cff_dict_get_operands (font->top_dict, FDARRAY_OP, &size);
+         decode_integer (operand, &offset);
+-        status = cairo_cff_font_read_cid_fontdict (font, font->data + offset);
++        p = font->data + offset;
++        if (unlikely (p < font->data || p > font->data_end))
++            return CAIRO_INT_STATUS_UNSUPPORTED;
++        status = cairo_cff_font_read_cid_fontdict (font, p);
+ 	if (unlikely (status))
+ 	    goto fail;
+     } else {
+         operand = cff_dict_get_operands (font->top_dict, PRIVATE_OP, &size);
+         operand = decode_integer (operand, &size);
+         decode_integer (operand, &offset);
+-	status = cairo_cff_font_read_private_dict (font,
++        p = font->data + offset;
++        if (unlikely (p < font->data || p > font->data_end))
++            return CAIRO_INT_STATUS_UNSUPPORTED;
++        status = cairo_cff_font_read_private_dict (font,
+                                                    font->private_dict,
+ 						   &font->local_sub_index,
+ 						   &font->local_sub_bias,
+ 						   &font->local_subs_used,
+                                                    &font->default_width,
+                                                    &font->nominal_width,
+-						   font->data + offset,
++						   p,
+ 						   size);
+ 	if (unlikely (status))
+ 	    goto fail;
Loading diff…