[jira] [Commented] (FOP-3329) ArrayIndexOutOfBoundsException/NPE in MultiByteFont for out-of-range/sentinel glyph index

"Rafael Witak (Jira)" <[email protected]> Mon, 27 Jul 2026 15:01:00 +0000 (UTC)
Newsgroups gmane.text.xml.fop.devel
Message-ID <[email protected]>
    [ https://issues.apache.org/jira/browse/FOP-3329?page=3Dcom.atlassian.j=
ira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=3D180994=
87#comment-18099487 ]=20

Rafael Witak commented on FOP-3329:
-----------------------------------

Attaching a git-format-patch [^multibytefont-sentinel-glyph.patch] (applies=
 cleanly on current main via `git am`) with the
fix plus two JUnit test cases. Happy to open a GitHub PR instead if preferr=
ed.

> ArrayIndexOutOfBoundsException/NPE in MultiByteFont for out-of-range/sent=
inel 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
>            Priority: Major
>         Attachments: multibytefont-sentinel-glyph.patch, reproduce.fo, re=
produce.xconf
>
>
> h3. Overview
> When complex-script (GSUB) processing maps a code point to an out-of-rang=
e 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 a=
rray length
> (some fonts pass the variation selector U+FE0F through as {{0xFE0F}} =3D =
65039),
>  * or {{{}-1{}}}, returned by {{CIDSubset}} for an index it has not regis=
tered.
> When such an index additionally reaches the embedded subset ({{{}CIDSubse=
t{}}}), it later
> causes a {{NullPointerException}} in {{OTFSubSetFile#createCharStringData=
CID}} during
> CFF subsetting.
> This is NOT limited to emoji: it also affects ordinary text containing ty=
pographic
> characters (e.g. U+22EF MIDLINE HORIZONTAL ELLIPSIS) that route to a larg=
e CID-keyed
> fallback font.
> h3. Steps to Reproduce
>  # Configure a font stack whose fallback font is a large CID-keyed OpenTy=
pe/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 a=
ttached
> configuration ([^reproduce.xconf]). The document contains a code point wh=
ose 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 crash=
es.
>  # 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 exact=
ly 65535
> glyphs and GSUB emits the sentinel), or {{"Index -1 out of bounds for len=
gth <N>"}}
> ({{{}CIDSubset{}}} returns -1 for an unregistered index).
> {noformat}
> java.lang.ArrayIndexOutOfBoundsException: Index 65535 out of bounds for l=
ength 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 {{NullPointerExcept=
ion}} follows
> in {{OTFSubSetFile#createCharStringDataCID}} during CFF subsetting.
> h3. Expected Results
> The unmapped/out-of-range glyph index should fall back to {{.notdef}} (gl=
yph index 0)
> so that metric lookups and subsetting do not crash. The page renders with=
out 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). Th=
e defect is
> font-independent: with the guard removed, the provided JUnit tests fail o=
n 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; N=
oto 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 ({{{}.notd=
ef{}}}) before it is
> used to index {{{}width[]{}}}/{{{}boundingBoxes[]{}}} and before it is ad=
ded to the embedded
> subset via {{{}cidSet.mapChar{}}}/{{{}mapCodePoint{}}}. The guard is rang=
e-based
> ({{{}index < 0 || index >=3D length{}}}), not tied to a single magic valu=
e; valid indices are
> unaffected.
> Empirically verified on unpatched current {{{}main{}}}: the attached {{Mu=
ltiByteFontTestCase}}
> (13 tests, synthetic width arrays + the real {{CIDSubset}} -1 path) and
> {{MultiByteFontRealFontTestCase}} (3 tests, using the bundled {{AndroidEm=
oji.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.
> =C2=A0
> ??with assistance of Claude Opus 4.8??



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