Rafael Witak created FOP-3329:
---------------------------------

             Summary: ArrayIndexOutOfBoundsException/NPE in MultiByteFont for 
out-of-range/sentinel glyph index
                 Key: FOP-3329
                 URL: https://issues.apache.org/jira/browse/FOP-3329
             Project: FOP
          Issue Type: Bug
          Components: font/opentype
    Affects Versions: 2.11
            Reporter: Rafael Witak
         Attachments: reproduce.fo, reproduce.xconf

h3. Overview

When complex-script (GSUB) processing maps a code point to an out-of-range glyph
index, {{MultiByteFont}} uses that index to access the 
{{{}width[]{}}}/{{{}boundingBoxes[]{}}}
arrays directly and throws an {{ArrayIndexOutOfBoundsException}} in
{{{}getWidth{}}}/{{{}getBoundingBox{}}}. The offending index can be:
 * the sentinel index {{0xFFFF}} (65535) emitted by GSUB for unsupported 
sequences,
 * a code point passed through as a glyph index that exceeds the metric array 
length
(some fonts pass the variation selector U+FE0F through as {{0xFE0F}} = 65039),
 * or {{{}-1{}}}, returned by {{CIDSubset}} for an index it has not registered.

When such an index additionally reaches the embedded subset 
({{{}CIDSubset{}}}), it later
causes a {{NullPointerException}} in {{OTFSubSetFile#createCharStringDataCID}} 
during
CFF subsetting.

This is NOT limited to emoji: it also affects ordinary text containing 
typographic
characters (e.g. U+22EF MIDLINE HORIZONTAL ELLIPSIS) that route to a large 
CID-keyed
fallback font.
h3. Steps to Reproduce
 # Configure a font stack whose fallback font is a large CID-keyed OpenType/CFF 
font
with about 65535 glyphs and active GSUB (e.g. Noto Sans CJK).
 # Render the attached minimal XSL-FO ([^reproduce.fo]) to PDF with the attached
configuration ([^reproduce.xconf]). The document contains a code point whose 
GSUB
processing yields an out-of-range glyph index. Verified triggers include:
 ** U+27A1 followed by U+FE0F ("right arrow" + variation selector), and
 ** the plain character U+22EF, i.e. {{"A" + U+22EF + "B"}} already crashes.
 # Observe the rendering.

h3. Actual Results

{{ArrayIndexOutOfBoundsException}} in {{MultiByteFont.getWidth}} (or 
{{{}getBoundingBox{}}}).
The exact message depends only on which invalid index occurs, e.g.
{{"Index 65535 out of bounds for length 65535"}} (fallback font has exactly 
65535
glyphs and GSUB emits the sentinel), or {{"Index -1 out of bounds for length 
<N>"}}
({{{}CIDSubset{}}} returns -1 for an unregistered index).
{noformat}
java.lang.ArrayIndexOutOfBoundsException: Index 65535 out of bounds for length 
65535
    at org.apache.fop.fonts.MultiByteFont.getWidth(MultiByteFont.java:198)
    at org.apache.fop.fonts.LazyFont.getWidth(LazyFont.java:292)
    at org.apache.fop.fonts.Font.getWidth(Font.java:224)
    <... full stack trace from the attached console log ...>
{noformat}
When the invalid index reaches the embedded subset, a {{NullPointerException}} 
follows
in {{OTFSubSetFile#createCharStringDataCID}} during CFF subsetting.
h3. Expected Results

The unmapped/out-of-range glyph index should fall back to {{.notdef}} (glyph 
index 0)
so that metric lookups and subsetting do not crash. The page renders without 
exception
(the offending character simply produces no visible glyph).
h3. Build & Environment

Verified against current {{main}} (HEAD 3791b92, 2026-07-08) and FOP 2.11 
({{{}MultiByteFont{}}}
accesses {{{}width[glyphIndex]{}}}/{{{}boundingBoxes[index]{}}} without a 
bounds check on both).
Java 17 (also reproducible on 8/21). OS-independent (Windows 11 here). The 
defect is
font-independent: with the guard removed, the provided JUnit tests fail on the 
real
{{CIDSubset}} path with {{"Index -1 out of bounds for length 10"}} - i.e. 
neither a
specific font nor the value 65535 is required to trigger it.
h3. Fonts

The trigger fonts are freely available under the SIL Open Font License 1.1:
 * Noto Sans CJK: [https://github.com/notofonts/noto-cjk]
 * Noto Emoji: [https://github.com/google/fonts/tree/main/ofl/notoemoji]

Noto Sans CJK has exactly 65535 glyphs and produces the 65535 sentinel; Noto 
Emoji
passes U+FE0F through as glyph index 65039 (same out-of-range bug class).
h3. Additional Information

{*}Fix{*}: clamp any out-of-range or negative glyph index to 0 
({{{}.notdef{}}}) before it is
used to index {{{}width[]{}}}/{{{}boundingBoxes[]{}}} and before it is added to 
the embedded
subset via {{{}cidSet.mapChar{}}}/{{{}mapCodePoint{}}}. The guard is range-based
({{{}index < 0 || index >= length{}}}), not tied to a single magic value; valid 
indices are
unaffected.

Empirically verified on unpatched current {{{}main{}}}: the attached 
{{MultiByteFontTestCase}}
(13 tests, synthetic width arrays + the real {{CIDSubset}} -1 path) and
{{MultiByteFontRealFontTestCase}} (3 tests, using the bundled 
{{AndroidEmoji.ttf}} and
{{{}DejaVuLGCSerif.ttf{}}}) fail with {{ArrayIndexOutOfBoundsException}} 
without the fix and
all pass with it. Checkstyle is clean. A GitHub pull request with the fix and 
both tests
will be linked in a comment.

 

??with assistance of Claude Opus 4.8??



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to