CVE-2026-10701
Overview
Background
- TrueType bytecode interpreter
- the stack machine in
ttinterp.cthat 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 likeWCVTPwrite 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.
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
- Craft a malicious font
Build a TrueType/OpenType font whose glyph program uses
SHZ/SHP/SHCinstructions with reference and contour/zone operands chosen to push the shift range past the real point count. - 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. - 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_DisplacementandMove_Zp2_Point. - Drive the out-of-bounds move
The mis-computed
limit/refplets the loop apply a displacement toexc->zp2.cur[i]beyond the allocated point array, corrupting adjacent heap memory with attacker-influencedFT_F26Dot6values.
Impact Assessment
Files Changed
modules/freetype2/src/truetype/ttinterp.c
Audit Directions
- Phantom-point boundariesAudit every interpreter loop whose
limitderives fromn_points(e.g.IUP,SHZ,DELTA) to confirm phantom points are excluded whenever the instruction is not meant to move them. - Truncated hinting indicesFlag any point/contour/CVT index read from the stack that is narrowed to
FT_UShortbefore itsBOUNDScheck, since truncation can alias or bypass the intended range; prefer full-widthFT_UIntwith an explicit bound. - Zone-by-value copiesLook for helpers that copy a
TT_GlyphZoneRecby value instead of passingTT_GlyphZone/FT_Vector*pointers, since a stale copy can desync from the zone the caller actually mutates and defeat identity checks likecur == zp->cur.
Patch
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 )