From 3414c2f4d372b1f8ae17d8e08daef281ea1c23aa Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 22:48:43 +0530 Subject: [PATCH 1/3] Fix crash on stretchy glyphs with empty variant lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MTFontMathTable -getVariantsForGlyph:inDictionary: returned the font's variant list verbatim. For assembly-only glyphs — whose OpenType MathGlyphConstruction has a GlyphAssembly but zero MathGlyphVariantRecords (e.g. XITS's stretchy arrows U+2190/2192/2194, stored as an empty h_variants array) — this returned an empty array. Both -findStretchyVariantGlyph: and -findVariantGlyph:withMaxWidth: then tripped the "numVariants > 0" assertion, crashing on e.g. \overrightarrow{x} in XITS. Treat an empty variant list the same as an absent one: fall back to the glyph itself. This restores the "a glyph is always its own variant" invariant the callers rely on, so the stretchy path falls through to the horizontal glyph assembly as intended. Also fix a missing break in the iOS example's font picker: selecting "TeX Gyre Termes" fell through into the XITS case, so the app actually switched to XITS (the font that triggers the crash) — which is why the crash was reported as happening "when switching to TeX Gyre Termes". Add a regression test rendering the stretchy arrows in the XITS font. Co-Authored-By: Claude Opus 4.8 --- iosMath/render/internal/MTFontMathTable.m | 8 +++++-- iosMathExample/example/ViewController.m | 2 ++ iosMathTests/MTTypesetterTest.m | 28 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/iosMath/render/internal/MTFontMathTable.m b/iosMath/render/internal/MTFontMathTable.m index 3b66bf4..3574fe1 100644 --- a/iosMath/render/internal/MTFontMathTable.m +++ b/iosMath/render/internal/MTFontMathTable.m @@ -427,8 +427,12 @@ - (CGFloat) stretchStackTopShiftUp { NSString* glyphName = [self.font getGlyphName:glyph]; NSArray* variantGlyphs = (NSArray*) variants[glyphName]; NSMutableArray* glyphArray = [NSMutableArray arrayWithCapacity:variantGlyphs.count]; - if (!variantGlyphs) { - // There are no extra variants, so just add the current glyph to it. + if (variantGlyphs.count == 0) { + // No sized variants for this glyph. This covers two cases: the glyph has no + // MathGlyphConstruction entry at all (variantGlyphs == nil), and assembly-only + // glyphs whose construction has a GlyphAssembly but zero variant records (an + // empty array, e.g. XITS's stretchy arrows). In both cases the glyph itself is + // its only variant, so callers can rely on a non-empty result. CGGlyph glyph = [self.font getGlyphWithName:glyphName]; [glyphArray addObject:@(glyph)]; return glyphArray; diff --git a/iosMathExample/example/ViewController.m b/iosMathExample/example/ViewController.m index b3c622b..e3a9ef4 100644 --- a/iosMathExample/example/ViewController.m +++ b/iosMathExample/example/ViewController.m @@ -350,9 +350,11 @@ - (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComp case 1: [self.controller termesButtonPressed:nil]; + break; case 2: [self.controller xitsButtonPressed:nil]; + break; default: break; diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index 2f66a52..911f22f 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -2001,6 +2001,34 @@ - (void)testOverrightarrowNarrow XCTAssertGreaterThanOrEqual(display.width + 0.01, stack.over.width); } +// Regression: XITS encodes the stretchy arrows (U+2190/2192/2194) as assembly-only +// glyphs — their OpenType MathGlyphConstruction has a GlyphAssembly but zero variant +// records, so h_variants is an empty list. Typesetting an \overrightarrow with such a +// font must not trip the "numVariants > 0" assertion; it should fall through to the +// horizontal glyph assembly. +- (void)testStretchyArrowAssemblyOnlyFont +{ + MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20]; + XCTAssertNotNil(xits); + + for (NSString* latex in @[@"\\overrightarrow{x}", @"\\overrightarrow{ABCD}", + @"\\overleftarrow{y}", @"\\overleftrightarrow{ABC}"]) { + MTMathList* list = [MTMathListBuilder buildFromString:latex]; + XCTAssertNotNil(list, @"%@", latex); + MTMathListDisplay* display = [MTTypesetter createLineForMathList:list font:xits style:kMTLineStyleDisplay]; + XCTAssertNotNil(display, @"%@", latex); + XCTAssertEqual(display.subDisplays.count, 1u, @"%@", latex); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTStackDisplay class]], @"%@", latex); + MTStackDisplay* stack = (MTStackDisplay*)sub0; + XCTAssertNotNil(stack.over, @"%@", latex); + XCTAssertNil(stack.under, @"%@", latex); + // The over-row must cover the base width. + XCTAssertGreaterThanOrEqual(stack.over.width + 0.01, stack.base.width, @"%@", latex); + } +} + - (void)testOverrightarrowWide { MTMathListDisplay* display = [self displayForLaTeX:@"\\overrightarrow{ABCD}"]; From b04e5b1cc806325bd567843514427ad156487745 Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 23:18:25 +0530 Subject: [PATCH 2/3] Add regression test for vertical assembly-only stretchy glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The empty-variant fix in getVariantsForGlyph:inDictionary: also repairs the vertical large-delimiter path. XITS encodes the stretchy vertical arrows (U+2191/2193/2195, used as \uparrow/\downarrow/\updownarrow delimiters) as assembly-only glyphs: empty v_variants but a populated v_assembly. Unlike the horizontal path, -findGlyph:withHeight: has no numVariants > 0 assertion — with an empty list it read glyphs[numVariants - 1] = glyphs[-1], an out-of-bounds stack read (confirmed by AddressSanitizer as a dynamic-stack-buffer-overflow at MTTypesetter.m:1516). It did not crash without sanitizers because the bogus glyph value is discarded (glyphAscent/Descent stay 0, forcing the assembly path), so the defect was latent. testStretchyVerticalArrowAssemblyOnlyFont renders \left\uparrow \frac{..}{..} \right\downarrow (and relatives) in XITS, asserting the boundary falls through to a vertical glyph assembly that covers the content height within the allowed delimiter shortfall. Co-Authored-By: Claude Opus 4.8 --- iosMathTests/MTTypesetterTest.m | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/iosMathTests/MTTypesetterTest.m b/iosMathTests/MTTypesetterTest.m index 911f22f..7ff5a38 100644 --- a/iosMathTests/MTTypesetterTest.m +++ b/iosMathTests/MTTypesetterTest.m @@ -2029,6 +2029,46 @@ - (void)testStretchyArrowAssemblyOnlyFont } } +// Vertical twin of the regression above. XITS encodes the stretchy vertical arrows +// (U+2191/2193/2195) as assembly-only glyphs — empty v_variants but a populated +// v_assembly. These are reachable as \left/\right delimiters (\uparrow, \downarrow, +// \updownarrow). Unlike the horizontal path, -findGlyph:withHeight: has no assertion +// guarding numVariants > 0: with an empty list it read glyphs[-1] (out-of-bounds). +// Treating the empty variant list as absent makes the boundary fall through to the +// vertical glyph assembly instead. +- (void)testStretchyVerticalArrowAssemblyOnlyFont +{ + MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20]; + XCTAssertNotNil(xits); + + // Tall content (a fraction) forces the boundary delimiter to stretch, exercising + // the variant lookup and then the glyph assembly. + for (NSString* latex in @[@"\\left\\uparrow \\frac{1}{2} \\right\\downarrow", + @"\\left\\updownarrow \\frac{a}{b} \\right\\updownarrow", + @"\\left\\downarrow \\frac{x}{y} \\right\\uparrow"]) { + MTMathList* list = [MTMathListBuilder buildFromString:latex]; + XCTAssertNotNil(list, @"%@", latex); + MTMathListDisplay* display = [MTTypesetter createLineForMathList:list font:xits style:kMTLineStyleDisplay]; + XCTAssertNotNil(display, @"%@", latex); + XCTAssertEqual(display.subDisplays.count, 1u, @"%@", latex); + + MTDisplay* sub0 = display.subDisplays[0]; + XCTAssertTrue([sub0 isKindOfClass:[MTInnerDisplay class]], @"%@", latex); + MTInnerDisplay* inner = (MTInnerDisplay*)sub0; + // No pre-built variant fits the tall content, so the empty variant list must + // fall through to the vertical glyph assembly rather than crash. + XCTAssertTrue([inner.leftDelimiter isKindOfClass:[MTGlyphConstructionDisplay class]], @"%@", latex); + XCTAssertTrue([inner.rightDelimiter isKindOfClass:[MTGlyphConstructionDisplay class]], @"%@", latex); + // The stretched delimiters cover the inner content's height, up to the + // allowed 5pt delimiter shortfall (kDelimiterShortfallPoints). + CGFloat innerHeight = inner.inner.ascent + inner.inner.descent; + XCTAssertGreaterThanOrEqual(inner.leftDelimiter.ascent + inner.leftDelimiter.descent + 5.01, + innerHeight, @"%@", latex); + XCTAssertGreaterThanOrEqual(inner.rightDelimiter.ascent + inner.rightDelimiter.descent + 5.01, + innerHeight, @"%@", latex); + } +} + - (void)testOverrightarrowWide { MTMathListDisplay* display = [self displayForLaTeX:@"\\overrightarrow{ABCD}"]; From 120a6e33fb44f74c4505b93d7a1f62a411e7018a Mon Sep 17 00:00:00 2001 From: Kostub D Date: Fri, 29 May 2026 23:23:07 +0530 Subject: [PATCH 3/3] Use glyph parameter directly in empty-variant fallback Drop the redundant getGlyphWithName: round-trip in getVariantsForGlyph:inDictionary:. The fallback already receives the glyph as a parameter; re-deriving it from its name added an unnecessary lookup, shadowed the parameter, and would return glyph 0 for nameless glyphs. Co-Authored-By: Claude Opus 4.8 --- iosMath/render/internal/MTFontMathTable.m | 1 - 1 file changed, 1 deletion(-) diff --git a/iosMath/render/internal/MTFontMathTable.m b/iosMath/render/internal/MTFontMathTable.m index 3574fe1..bb1e5c5 100644 --- a/iosMath/render/internal/MTFontMathTable.m +++ b/iosMath/render/internal/MTFontMathTable.m @@ -433,7 +433,6 @@ - (CGFloat) stretchStackTopShiftUp { // glyphs whose construction has a GlyphAssembly but zero variant records (an // empty array, e.g. XITS's stretchy arrows). In both cases the glyph itself is // its only variant, so callers can rely on a non-empty result. - CGGlyph glyph = [self.font getGlyphWithName:glyphName]; [glyphArray addObject:@(glyph)]; return glyphArray; }