High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionIncorrect boundary conditions in the Graphics: Text component
ComponentCore
Bug ClassLogic Error
Tracker2038537
Fix commite1b2f7ec555c (firefox) +84/-108
CISA KEVNot listed
Creditedtaiho kim
Disclosed2026-06-02

Background

TrueType bytecode interpreter
the stack machine in ttinterp.c that executes a font’s embedded hinting program to move glyph outline points at a given size.
Glyph zone (`TT_GlyphZone` / `TT_GlyphZoneRec`)
a structure holding a glyph’s point arrays (cur, org, tags, n_points) that hinting instructions read from and write into.
Phantom points
the extra trailing points appended to every glyph zone to carry sidebearing/advance metrics, which certain instructions must exclude from their operating range.
Control Value Table (CVT)
the per-font array (exc->cvt) of scaled hinting values that instructions like WCVTP write into.

Root Cause Analysis

The shift-family instructions (Ins_SHP, Ins_SHC, Ins_SHZ) and their helper Compute_Point_Displacement used FT_UShort reference/point indices and copied a whole TT_GlyphZoneRec by value, so the range endpoints (start, limit, refp) and the zone identity used for the move loop were derived with incorrect boundary conditions — most notably Ins_SHZ, which iterated a limit that failed to subtract the phantom points the instruction is not supposed to move. Because the loop bound could extend past the real point count, the interpreter could compute displacements and write into exc->zp2.cur[i] outside the zone’s allocated point array.

The fix narrows and corrects these bounds: Compute_Point_Displacement now takes a live FT_Vector* cur pointer plus an FT_UInt* refp and returns ~0U (a sentinel) when the reference point belongs to a different zone, indices are widened to FT_UInt and validated with BOUNDS against the actual n_points, and Ins_SHZ explicitly subtracts phantom points from its limit. Separately, Ins_WCVTP now calls Modify_CVT_Check( exc ) and bails on exc->error before writing exc->cvt[I], so a CVT-modifying write cannot proceed against an invalid/unresized table. Together these restore the invariant that every point/CVT index and loop limit is bounds-checked against the current, correctly-sized zone before any write.

Key insight
The single core mistake was trusting loop limits and reference indices that were computed with the wrong endpoints (phantom points not excluded, indices truncated to FT_UShort, and the zone copied by value so its identity could desync) rather than validated against the live zone bounds. The fix threads the real zone pointer through Compute_Point_Displacement, widens indices to FT_UInt with proper BOUNDS checks, and subtracts phantom points from Ins_SHZ’s limit so no write can land outside the point array.

Attack Path

  1. Craft a malicious font Build a TrueType/OpenType font whose glyph program uses SHZ/SHP/SHC instructions with reference and contour/zone operands chosen to push the shift range past the real point count.
  2. Deliver it to the target Serve the font as a web font (@font-face) or otherwise get it loaded by the browser so it will be used to render text.
  3. Trigger hinting Cause the glyph to be rasterized at a size that runs the bytecode interpreter, invoking the vulnerable shift instruction path through Compute_Point_Displacement and Move_Zp2_Point.
  4. Drive the out-of-bounds move The mis-computed limit/refp lets the loop apply a displacement to exc->zp2.cur[i] beyond the allocated point array, corrupting adjacent heap memory with attacker-influenced FT_F26Dot6 values.

Impact Assessment

An attacker who gets a crafted font rendered gains an out-of-bounds heap write (of coordinate deltas at attacker-influenced indices) inside the process that rasterizes text via the FreeType TrueType interpreter, which for web content is the content/rendering process. The only precondition is that the victim loads and renders a page (or document) that uses the malicious font; no user interaction beyond viewing is required. The corruption is a memory-safety primitive that, combined with heap grooming, can escalate toward code execution, consistent with the reported high severity.

Files Changed

  • modules/freetype2/src/truetype/ttinterp.c

Audit Directions

  • Phantom-point boundaries
    Audit every interpreter loop whose limit derives from n_points (e.g. IUP, SHZ, DELTA) to confirm phantom points are excluded whenever the instruction is not meant to move them.
  • Truncated hinting indices
    Flag any point/contour/CVT index read from the stack that is narrowed to FT_UShort before its BOUNDS check, since truncation can alias or bypass the intended range; prefer full-width FT_UInt with an explicit bound.
  • Zone-by-value copies
    Look for helpers that copy a TT_GlyphZoneRec by value instead of passing TT_GlyphZone/FT_Vector* pointers, since a stale copy can desync from the zone the caller actually mutates and defeat identity checks like cur == zp->cur.
diff --git a/modules/freetype2/src/truetype/ttinterp.c b/modules/freetype2/src/truetype/ttinterp.c
index 3841118f14a..a975cce7eb3 100644
--- a/modules/freetype2/src/truetype/ttinterp.c
+++ b/modules/freetype2/src/truetype/ttinterp.c
@@ -1104,7 +1104,7 @@
   /* documentation is in freetype.h */
 
   static __inline FT_Int32
-  TT_MulFixi14_i386( FT_Int32  a,
+  TT_MulFix14_i386( FT_Int32  a,
                      FT_Int32  b )
   {
     FT_Int32  result;
@@ -2841,7 +2841,13 @@
         ARRAY_BOUND_ERROR;
     }
     else
+    {
+      Modify_CVT_Check( exc );
+      if ( exc->error )
+        return;
+
       exc->cvt[I] = FT_MulFix( args[1], exc->tt_metrics.scale );
+    }
   }
 
 
@@ -4923,37 +4929,38 @@
   Compute_Point_Displacement( TT_ExecContext  exc,
                               FT_F26Dot6*     x,
                               FT_F26Dot6*     y,
-                              TT_GlyphZone    zone,
-                              FT_UShort*      refp )
+                              FT_Vector*      cur,
+                              FT_UInt*        refp )
   {
-    TT_GlyphZoneRec  zp;
-    FT_UShort        p;
-    FT_F26Dot6       d;
+    TT_GlyphZone  zp;
+    FT_UShort     p;
+    FT_F26Dot6    d;
 
 
     if ( exc->opcode & 1 )
     {
-      zp = exc->zp0;
+      zp = &exc->zp0;
       p  = exc->GS.rp1;
     }
     else
     {
-      zp = exc->zp1;
+      zp = &exc->zp1;
       p  = exc->GS.rp2;
     }
 
-    if ( BOUNDS( p, zp.n_points ) )
+    if ( BOUNDS( p, zp->n_points ) )
     {
       if ( exc->pedantic_hinting )
         exc->error = FT_THROW( Invalid_Reference );
-      *refp = 0;
       return FAILURE;
     }
 
-    *zone = zp;
-    *refp = p;
+    /* return reference if zones match */
+    if ( refp )
+      *refp = cur == zp->cur ? p
+                             : ~0U;  /* nan */
 
-    d = PROJECT( zp.cur + p, zp.org + p );
+    d = PROJECT( zp->cur + p, zp->org + p );
 
     *x = FT_MulFix( d, exc->moveVector.x );
     *y = FT_MulFix( d, exc->moveVector.y );
@@ -4965,10 +4972,9 @@
   /* See `ttinterp.h' for details on backward compatibility mode. */
   static void
   Move_Zp2_Point( TT_ExecContext  exc,
-                  FT_UShort       point,
+                  FT_UInt         point,
                   FT_F26Dot6      dx,
-                  FT_F26Dot6      dy,
-                  FT_Bool         touch )
+                  FT_F26Dot6      dy )
   {
     if ( exc->GS.freeVector.x != 0 )
     {
@@ -4978,8 +4984,7 @@
 #endif
         exc->zp2.cur[point].x = ADD_LONG( exc->zp2.cur[point].x, dx );
 
-      if ( touch )
-        exc->zp2.tags[point] |= FT_CURVE_TAG_TOUCH_X;
+      exc->zp2.tags[point] |= FT_CURVE_TAG_TOUCH_X;
     }
 
     if ( exc->GS.freeVector.y != 0 )
@@ -4990,8 +4995,7 @@
 #endif
         exc->zp2.cur[point].y = ADD_LONG( exc->zp2.cur[point].y, dy );
 
-      if ( touch )
-        exc->zp2.tags[point] |= FT_CURVE_TAG_TOUCH_Y;
+      exc->zp2.tags[point] |= FT_CURVE_TAG_TOUCH_Y;
     }
   }
 
@@ -5007,11 +5011,9 @@
            FT_Long*        args )
   {
     FT_Long          loop = exc->GS.loop;
-    TT_GlyphZoneRec  zp;
-    FT_UShort        refp;
 
     FT_F26Dot6       dx, dy;
-    FT_UShort        point;
+    FT_UInt          point;
 
 
     if ( exc->new_top < loop )
@@ -5023,12 +5025,12 @@
 
     exc->new_top -= loop;
 
-    if ( Compute_Point_Displacement( exc, &dx, &dy, &zp, &refp ) )
+    if ( Compute_Point_Displacement( exc, &dx, &dy, NULL, NULL ) )
       return;
 
     while ( loop-- )
     {
-      point = (FT_UShort)*(--args);
+      point = (FT_UInt)*(--args);
 
       if ( BOUNDS( point, exc->zp2.n_points ) )
       {
@@ -5039,7 +5041,7 @@
         }
       }
       else
-        Move_Zp2_Point( exc, point, dx, dy, TRUE );
+        Move_Zp2_Point( exc, point, dx, dy );
     }
 
   Fail:
@@ -5061,12 +5063,10 @@
   Ins_SHC( TT_ExecContext  exc,
            FT_Long*        args )
   {
-    TT_GlyphZoneRec  zp;
-    FT_UShort        refp;
+    FT_UInt          refp, start, limit, i;
     FT_F26Dot6       dx, dy;
 
     FT_UShort        contour, bounds;
-    FT_UShort        start, limit, i;
 
 
     contour = (FT_UShort)args[0];
@@ -5079,7 +5079,7 @@
       return;
     }
 
-    if ( Compute_Point_Displacement( exc, &dx, &dy, &zp, &refp ) )
+    if ( Compute_Point_Displacement( exc, &dx, &dy, exc->zp2.cur, &refp ) )
       return;
 
     if ( contour == 0 )
@@ -5095,8 +5095,8 @@
 
     for ( i = start; i < limit; i++ )
     {
-      if ( zp.cur != exc->zp2.cur || refp != i )
-        Move_Zp2_Point( exc, i, dx, dy, TRUE );
+      if ( refp != i )
+        Move_Zp2_Point( exc, i, dx, dy );
     }
   }
 
@@ -5111,38 +5111,59 @@
   Ins_SHZ( TT_ExecContext  exc,
            FT_Long*        args )
   {
-    TT_GlyphZoneRec  zp;
-    FT_UShort        refp;
-    FT_F26Dot6       dx,
-                     dy;
-
-    FT_UShort        limit, i;
+    FT_Vector*       cur;
+    FT_UInt          refp, i, limit;
+    FT_F26Dot6       dx, dy;
 
 
-    if ( BOUNDS( args[0], 2 ) )
+    /* XXX: UNDOCUMENTED! SHZ doesn't move the phantom points, */
+    /*      which must be subtracted.                          */
+    switch ( (FT_Int)args[0] )
     {
+    case 0:
+      cur   = exc->twilight.cur;
+      limit = exc->twilight.n_points;
+      break;
+
+    case 1:
+      cur   = exc->pts.cur;
+      limit = exc->pts.n_points > 4U ? exc->pts.n_points - 4U : 0;
+      break;
+
+    default:
       if ( exc->pedantic_hinting )
         exc->error = FT_THROW( Invalid_Reference );
       return;
     }
 
-    if ( Compute_Point_Displacement( exc, &dx, &dy, &zp, &refp ) )
+    if ( Compute_Point_Displacement( exc, &dx, &dy, cur, &refp ) )
       return;
 
-    /* XXX: UNDOCUMENTED! SHZ doesn't move the phantom points.     */
-    /*      Twilight zone has no real contours, so use `n_points'. */
-    /*      Normal zone's `n_points' includes phantoms, so must    */
-    /*      subtract them.                                         */
-    if ( exc->GS.gep2 == 0 )
-      limit = exc->zp2.n_points;
-    else
-      limit = exc->zp2.n_points - 4U;
+    /* XXX: UNDOCUMENTED! SHZ doesn't touch the points. */
+    if ( dx )
+    {
+#ifdef TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL
+      /* See `ttinterp.h' for details on backward compatibility mode. */
+      if ( !exc->backward_compatibility )
+#endif
+        for ( i = 0; i < limit; i++ )
+        {
+          if ( refp != i )
+            cur[i].x = ADD_LONG( cur[i].x, dx );
+        }
+    }
 
-    /* XXX: UNDOCUMENTED! SHZ doesn't touch the points */
-    for ( i = 0; i < limit; i++ )
+    if ( dy )
     {
-      if ( zp.cur != exc->zp2.cur || refp != i )
-        Move_Zp2_Point( exc, i, dx, dy, FALSE );
+#ifdef TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL
+      /* See `ttinterp.h' for details on backward compatibility mode. */
+      if ( exc->backward_compatibility != 0x7 )
+#endif
+        for ( i = 0; i < limit; i++ )
+        {
+          if ( refp != i )
+            cur[i].y = ADD_LONG( cur[i].y, dy );
+        }
     }
   }
 
@@ -5159,7 +5180,7 @@
   {
     FT_Long     loop = exc->GS.loop;
     FT_F26Dot6  dx, dy;
-    FT_UShort   point;
+    FT_UInt     point;
 #ifdef TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL
     FT_Bool     in_twilight = FT_BOOL( exc->GS.gep0 == 0 ||
                                        exc->GS.gep1 == 0 ||
@@ -5181,7 +5202,7 @@
 
     while ( loop-- )
     {
-      point = (FT_UShort)*(--args);
+      point = (FT_UInt)*(--args);
 
       if ( BOUNDS( point, exc->zp2.n_points ) )
       {
@@ -5204,11 +5225,11 @@
              ( exc->backward_compatibility != 0x7                     &&
                ( ( exc->is_composite && exc->GS.freeVector.y != 0 ) ||
                  ( exc->zp2.tags[point] & FT_CURVE_TAG_TOUCH_Y )    ) ) )
-          Move_Zp2_Point( exc, point, 0, dy, TRUE );
+          Move_Zp2_Point( exc, point, 0, dy );
       }
       else
 #endif
-        Move_Zp2_Point( exc, point, dx, dy, TRUE );
+        Move_Zp2_Point( exc, point, dx, dy );
     }
 
   Fail:
@@ -6189,14 +6210,14 @@
     IUP_WorkerRec  V;
     FT_Byte        mask;
 
-    FT_UInt   first_point;   /* first point of contour        */
-    FT_UInt   end_point;     /* end point (last+1) of contour */
+    FT_UInt  first_point;   /* first point of contour        */
+    FT_UInt  end_point;     /* end point (last+1) of contour */
 
-    FT_UInt   first_touched; /* first touched point in contour   */
-    FT_UInt   cur_touched;   /* current touched point in contour */
+    FT_UInt  first_touched; /* first touched point in contour   */
+    FT_UInt  cur_touched;   /* current touched point in contour */
 
-    FT_UInt   point;         /* current point   */
-    FT_Short  contour;       /* current contour */
+    FT_UInt  point;         /* current point   */
+    FT_UInt  contour;       /* current contour */
 
 
 #ifdef TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL
@@ -7337,56 +7358,11 @@
       }
 
       if ( exc->error )
-      {
-        switch ( exc->error )
-        {
-        case FT_ERR( Invalid_Opcode ):
-          {
-            TT_DefRecord*  def   = exc->IDefs;
-            TT_DefRecord*  limit = FT_OFFSET( def, exc->numIDefs );
-
-
-            /* looking for redefined instructions */
-            for ( ; def < limit; def++ )
-            {
-              if ( def->active && exc->opcode == (FT_Byte)def->opc )
-              {
-                TT_CallRec*  callrec;
-
-
-                if ( exc->callTop >= exc->callSize )
-                {
-                  exc->error = FT_THROW( Invalid_Reference );
-                  goto LErrorLabel_;
-                }
-
-                callrec = &exc->callStack[exc->callTop];
-
-                callrec->Caller_Range = exc->curRange;
-                callrec->Caller_IP    = exc->IP + 1;
-                callrec->Cur_Count    = 1;
-                callrec->Def          = def;
-
-                if ( Ins_Goto_CodeRange( exc,
-                                         def->range,
-                                         def->start ) == FAILURE )
-                  goto LErrorLabel_;
-
-                goto LSuiteLabel_;
-              }
-            }
-          }
-          FALL_THROUGH;
-
-        default:
-          goto LErrorLabel_;
-        }
-      }
+        goto LErrorLabel_;
 
       exc->top = exc->new_top;
       exc->IP += exc->length;
 
-    LSuiteLabel_:
       if ( exc->IP >= exc->codeSize )
       {
         if ( exc->callTop > 0 )
Loading diff…