CVE-2026-12305
Overview
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.ccode 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 anFDSelect/FDArraylayer 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.
font->data + offset pointer (and the raw offset) against the buffer’s start and end before use.Attack Path
- 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.
- 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.
- Reach the unchecked offset
cairo_cff_font_read_top_dictdecodes the offset and computesfont->data + offset(orcairo_cff_font_read_cid_fontdictuses the raw offset) with no bounds validation. - Trigger out-of-bounds access
Downstream readers such as
cff_index_readorcairo_cff_font_read_private_dictdereference the wild pointer, reading memory outside the font allocation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgfx/cairo/cairo/src/cairo-cff-subset.c |
modified | |
ifgfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch |
modified |
Files Changed
gfx/cairo/cairo/src/cairo-cff-subset.cgfx/cairo/patches/0033-Bug-2037290-cff-subset-offset-checks.patch
Audit Directions
- Offset-to-pointer conversionsAudit every
base + offsetcomputed from parsed font/file data and confirm both a lower (< base) and upper (> end) bound is enforced before dereference. - One-sided bounds checksSearch for range tests that check only
>= end(like the oldcharsetcheck) and verify a matching lower-bound test guards against negative or underflowing offsets. - Signed offset operandsReview
decode_integerconsumers where a signedoffsetcan be negative or exceedfont->data_length, since a cast tounsigned longor pointer arithmetic can otherwise produce a wild address.
Patch
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;