3
25
Fork
You've already forked fpdf
10

Panic: index out of range when text contains non-BMP Unicode characters (code points >= U+10000) #112

Open
opened 2026年03月23日 18:21:29 +01:00 by mdz · 1 comment

Summary

Using a UTF-8 font and writing text that contains characters outside the Basic Multilingual Plane (code points >= U+10000, such as emoji) causes a panic during Output(). The panic occurs in generateCIDFontMap (fpdf.go:4600), which iterates up to LastRune+1 and indexes into font.Cw without bounds checking. font.Cw is backed by CharWidths, a fixed 65536-element slice allocated in parseHMTXTable.

A second crash site exists in parseHMTXTable itself (utf8fontfile.go:859-860), which can panic during font loading if the font's cmap table contains non-BMP character mappings.

Reproduction

Self-contained, no external font files needed:

packagemainimport("io""codeberg.org/go-pdf/fpdf""golang.org/x/image/font/gofont/goregular")funcmain(){pdf:=fpdf.New("P","mm","A4","")pdf.AddUTF8FontFromBytes("go","",goregular.TTF)pdf.SetFont("go","",12)pdf.AddPage()pdf.Cell(40,10,"\U0001F7E5")// U+1F7E5 LARGE RED SQUAREpdf.Output(io.Discard)}

Panics with:

panic: runtime error: index out of range [65536] with length 65536
goroutine 1 [running]:
codeberg.org/go-pdf/fpdf.(*Fpdf).generateCIDFontMap(0x..., 0x..., 0x1f7e5)
 fpdf.go:4600 +0xfbc
codeberg.org/go-pdf/fpdf.(*Fpdf).putfonts(0x...)
 fpdf.go:4519 +0xafc
codeberg.org/go-pdf/fpdf.(*Fpdf).putresources(0x...)
 fpdf.go:4985 +0x48
codeberg.org/go-pdf/fpdf.(*Fpdf).enddoc(0x...)
 fpdf.go:5192 +0x13c
codeberg.org/go-pdf/fpdf.(*Fpdf).Close(0x...)
 fpdf.go:767 +0x1d4
codeberg.org/go-pdf/fpdf.(*Fpdf).Output(0x..., {0x..., 0x...})
 fpdf.go:3837 +0x48
main.main()
 main.go:18 +0x190

generateCIDFontMap is called with LastRune = 0x1f7e5 (128997) and iterates sequentially from cid = 1; the panic fires at the first out-of-bounds index (65536).

Root Cause

There are two related bugs sharing the same root cause: CharWidths is allocated with only 65536 elements but code allows character values well beyond that range.

Bug 1: generateCIDFontMap (fpdf.go:4588)

When text is rendered via Cell/Write/etc., each character's code point is recorded in font.usedRunes:

// fpdf.go:2555-2556for_,uni:=rangetxtStr{f.currentFont.usedRunes[int(uni)]=int(uni)}

Later, during makeSubset (called from Output), LastRune is set to the maximum used rune:

// utf8fontfile.go:544utf.LastRune=max(utf.LastRune,char)

Then generateCIDFontMap iterates up to LastRune+1, indexing into font.Cw (which is CharWidths, length 65536) without bounds checking:

// fpdf.go:4596-4600cwLen:=LastRune+1forcid:=startCid;cid<cwLen;cid++{iffont.Cw[cid]==0x00{// PANIC when cid >= 65536

Bug 2: parseHMTXTable (utf8fontfile.go:834)

CharWidths is allocated as make([]int, 256*256) (65536 elements), but the bounds guard allows values up to 196608:

// utf8fontfile.go:839utf.CharWidths=make([]int,256*256)// length 65536// ...// utf8fontfile.go:859-860ifchar<196608{utf.CharWidths[char]=widths// PANIC when 65536 <= char < 196608}

This panics during font loading if the font's cmap contains mappings for non-BMP characters.

Suggested Fix

The most straightforward fix is to make CharWidths large enough for the range it guards (196608), or use a map instead of a slice. If the intent is to only support BMP characters, the guard should be char < 65536 (or char < len(utf.CharWidths)).

Additionally, generateCIDFontMap should cap its iteration to len(font.Cw):

cwLen:=LastRune+1ifcwLen>len(font.Cw){cwLen=len(font.Cw)}

Environment

  • fpdf version: v0.11.1
  • Go version: go1.24
  • OS: darwin/arm64
## Summary Using a UTF-8 font and writing text that contains characters outside the Basic Multilingual Plane (code points >= U+10000, such as emoji) causes a panic during `Output()`. The panic occurs in `generateCIDFontMap` (`fpdf.go:4600`), which iterates up to `LastRune+1` and indexes into `font.Cw` without bounds checking. `font.Cw` is backed by `CharWidths`, a fixed 65536-element slice allocated in `parseHMTXTable`. A second crash site exists in `parseHMTXTable` itself (`utf8fontfile.go:859-860`), which can panic during font loading if the font's cmap table contains non-BMP character mappings. ## Reproduction Self-contained, no external font files needed: ```go package main import ( "io" "codeberg.org/go-pdf/fpdf" "golang.org/x/image/font/gofont/goregular" ) func main() { pdf := fpdf.New("P", "mm", "A4", "") pdf.AddUTF8FontFromBytes("go", "", goregular.TTF) pdf.SetFont("go", "", 12) pdf.AddPage() pdf.Cell(40, 10, "\U0001F7E5") // U+1F7E5 LARGE RED SQUARE pdf.Output(io.Discard) } ``` Panics with: ``` panic: runtime error: index out of range [65536] with length 65536 goroutine 1 [running]: codeberg.org/go-pdf/fpdf.(*Fpdf).generateCIDFontMap(0x..., 0x..., 0x1f7e5) fpdf.go:4600 +0xfbc codeberg.org/go-pdf/fpdf.(*Fpdf).putfonts(0x...) fpdf.go:4519 +0xafc codeberg.org/go-pdf/fpdf.(*Fpdf).putresources(0x...) fpdf.go:4985 +0x48 codeberg.org/go-pdf/fpdf.(*Fpdf).enddoc(0x...) fpdf.go:5192 +0x13c codeberg.org/go-pdf/fpdf.(*Fpdf).Close(0x...) fpdf.go:767 +0x1d4 codeberg.org/go-pdf/fpdf.(*Fpdf).Output(0x..., {0x..., 0x...}) fpdf.go:3837 +0x48 main.main() main.go:18 +0x190 ``` `generateCIDFontMap` is called with `LastRune = 0x1f7e5` (128997) and iterates sequentially from `cid = 1`; the panic fires at the first out-of-bounds index (65536). ## Root Cause There are two related bugs sharing the same root cause: `CharWidths` is allocated with only 65536 elements but code allows character values well beyond that range. ### Bug 1: `generateCIDFontMap` (fpdf.go:4588) When text is rendered via `Cell`/`Write`/etc., each character's code point is recorded in `font.usedRunes`: ```go // fpdf.go:2555-2556 for _, uni := range txtStr { f.currentFont.usedRunes[int(uni)] = int(uni) } ``` Later, during `makeSubset` (called from `Output`), `LastRune` is set to the maximum used rune: ```go // utf8fontfile.go:544 utf.LastRune = max(utf.LastRune, char) ``` Then `generateCIDFontMap` iterates up to `LastRune+1`, indexing into `font.Cw` (which is `CharWidths`, length 65536) without bounds checking: ```go // fpdf.go:4596-4600 cwLen := LastRune + 1 for cid := startCid; cid < cwLen; cid++ { if font.Cw[cid] == 0x00 { // PANIC when cid >= 65536 ``` ### Bug 2: `parseHMTXTable` (utf8fontfile.go:834) `CharWidths` is allocated as `make([]int, 256*256)` (65536 elements), but the bounds guard allows values up to 196608: ```go // utf8fontfile.go:839 utf.CharWidths = make([]int, 256*256) // length 65536 // ... // utf8fontfile.go:859-860 if char < 196608 { utf.CharWidths[char] = widths // PANIC when 65536 <= char < 196608 } ``` This panics during font loading if the font's cmap contains mappings for non-BMP characters. ## Suggested Fix The most straightforward fix is to make `CharWidths` large enough for the range it guards (`196608`), or use a map instead of a slice. If the intent is to only support BMP characters, the guard should be `char < 65536` (or `char < len(utf.CharWidths)`). Additionally, `generateCIDFontMap` should cap its iteration to `len(font.Cw)`: ```go cwLen := LastRune + 1 if cwLen > len(font.Cw) { cwLen = len(font.Cw) } ``` ## Environment - fpdf version: v0.11.1 - Go version: go1.24 - OS: darwin/arm64

thanks for the detailed report.

would you like to try and send a PR to fix that ?

thanks for the detailed report. would you like to try and send a PR to fix that ?
Sign in to join this conversation.
No Branch/Tag specified
main
v0.12.0
v0.11.1
v0.10.0
v0.11.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v1.4.3
v0.4.1
v0.4.0
v0.3.1
v0.3.0
v0.2.0
v0.1.0
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
go-pdf/fpdf#112
Reference in a new issue
go-pdf/fpdf
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?