From 0a263e6264aa5399988d9acd3bbfbca2ca3ec77d Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:41:22 +1000
Subject: [PATCH] Add decompression bomb checks to FontFile classes (#9711)

---
 Tests/test_font_bdf.py | 10 +++++++++-
 Tests/test_font_pcf.py | 21 ++++++++++++++++++++-
 Tests/test_fontfile.py |  7 +++++++
 src/PIL/BdfFontFile.py |  1 +
 src/PIL/FontFile.py    |  3 ++-
 src/PIL/PcfFontFile.py |  1 +
 6 files changed, 40 insertions(+), 3 deletions(-)

Index: pillow-11.3.0/Tests/test_font_bdf.py
===================================================================
--- pillow-11.3.0.orig/Tests/test_font_bdf.py
+++ pillow-11.3.0/Tests/test_font_bdf.py
@@ -4,7 +4,7 @@ import io
 
 import pytest
 
-from PIL import BdfFontFile, FontFile
+from PIL import BdfFontFile, FontFile, Image
 
 filename = "Tests/images/courB08.bdf"
 
@@ -24,6 +24,14 @@ def test_zero_width_chars() -> None:
     BdfFontFile.BdfFontFile(io.BytesIO(data))
 
 
+def test_decompression_bomb() -> None:
+    with open(filename, "rb") as fp:
+        data = fp.read()
+    b = io.BytesIO(data.replace(b"BBX 1 1", b"BBX 13378 13378"))
+    with pytest.raises(Image.DecompressionBombError):
+        BdfFontFile.BdfFontFile(b)
+
+
 def test_invalid_file() -> None:
     with open("Tests/images/flower.jpg", "rb") as fp:
         with pytest.raises(SyntaxError):
Index: pillow-11.3.0/Tests/test_font_pcf.py
===================================================================
--- pillow-11.3.0.orig/Tests/test_font_pcf.py
+++ pillow-11.3.0/Tests/test_font_pcf.py
@@ -1,12 +1,13 @@
 from __future__ import annotations
 
 import os
+from io import BytesIO
 from pathlib import Path
 from typing import AnyStr
 
 import pytest
 
-from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile
+from PIL import FontFile, Image, ImageDraw, ImageFont, PcfFontFile, _binary
 
 from .helper import (
     assert_image_equal_tofile,
@@ -108,3 +109,21 @@ def test_high_characters(request: pytest
     _test_high_characters(request, tmp_path, message)
     # accept bytes instances.
     _test_high_characters(request, tmp_path, message.encode("latin1"))
+
+
+def test_decompression_bomb() -> None:
+    with open(fontname, "rb") as fp:
+        data = fp.read()
+    b = BytesIO(
+        data[:900]
+        + _binary.o32le(0)  # jumbo format
+        + _binary.o32le(1)  # number of metrics
+        + _binary.o16le(0)  # left
+        + _binary.o16le(65535)  # right
+        + _binary.o16le(0)  # width
+        + _binary.o16le(0)  # ascent
+        + _binary.o16le(65535)  # descent
+        + _binary.o16le(0)  # attributes
+    )
+    with pytest.raises(Image.DecompressionBombError):
+        PcfFontFile.PcfFontFile(b)
Index: pillow-11.3.0/Tests/test_fontfile.py
===================================================================
--- pillow-11.3.0.orig/Tests/test_fontfile.py
+++ pillow-11.3.0/Tests/test_fontfile.py
@@ -20,6 +20,13 @@ def test_compile() -> None:
     assert font.ysize == 2
 
 
+def test_decompression_bomb() -> None:
+    font = FontFile.FontFile()
+    font.glyph[0] = ((0, 0), (0, 0, 0, 0), (0, 0, 10000, 10000), Image.new("L", (0, 0)))
+    with pytest.raises(Image.DecompressionBombError):
+        font.compile()
+
+
 def test_save(tmp_path: Path) -> None:
     tempname = str(tmp_path / "temp.pil")
 
Index: pillow-11.3.0/src/PIL/BdfFontFile.py
===================================================================
--- pillow-11.3.0.orig/src/PIL/BdfFontFile.py
+++ pillow-11.3.0/src/PIL/BdfFontFile.py
@@ -69,6 +69,7 @@ def bdf_char(
     # and x and y displacement (BBxoff0, BByoff0)
     # of the lower left corner from the origin of the character.
     width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
+    Image._decompression_bomb_check((width, height))
 
     # The word DWIDTH
     # followed by the width in x and y of the character in device pixels.
Index: pillow-11.3.0/src/PIL/FontFile.py
===================================================================
--- pillow-11.3.0.orig/src/PIL/FontFile.py
+++ pillow-11.3.0/src/PIL/FontFile.py
@@ -74,7 +74,7 @@ class FontFile:
             if glyph:
                 d, dst, src, im = glyph
                 h = max(h, src[3] - src[1])
-                w = w + (src[2] - src[0])
+                w += src[2] - src[0]
                 if w > WIDTH:
                     lines += 1
                     w = src[2] - src[0]
@@ -89,6 +89,7 @@ class FontFile:
         self.ysize = h
 
         # paste glyphs into bitmap
+        Image._decompression_bomb_check((xsize, ysize))
         self.bitmap = Image.new("1", (xsize, ysize))
         self.metrics: list[
             tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]]
Index: pillow-11.3.0/src/PIL/PcfFontFile.py
===================================================================
--- pillow-11.3.0.orig/src/PIL/PcfFontFile.py
+++ pillow-11.3.0/src/PIL/PcfFontFile.py
@@ -154,32 +154,30 @@ class PcfFontFile(FontFile.FontFile):
 
         fp, format, i16, i32 = self._getformat(PCF_METRICS)
 
-        append = metrics.append
+        def append(
+            left: int,
+            right: int,
+            width: int,
+            ascent: int,
+            descent: int,
+            attributes: int = 0,
+        ) -> None:
+            xsize = right - left
+            ysize = ascent + descent
+            Image._decompression_bomb_check((xsize, ysize))
+            metrics.append(
+                (xsize, ysize, left, right, width, ascent, descent, attributes)
+            )
 
         if (format & 0xFF00) == 0x100:
             # "compressed" metrics
             for i in range(i16(fp.read(2))):
-                left = i8(fp.read(1)) - 128
-                right = i8(fp.read(1)) - 128
-                width = i8(fp.read(1)) - 128
-                ascent = i8(fp.read(1)) - 128
-                descent = i8(fp.read(1)) - 128
-                xsize = right - left
-                ysize = ascent + descent
-                append((xsize, ysize, left, right, width, ascent, descent, 0))
+                append(*(i8(fp.read(1)) - 128 for _ in range(5)))
 
         else:
             # "jumbo" metrics
             for i in range(i32(fp.read(4))):
-                left = i16(fp.read(2))
-                right = i16(fp.read(2))
-                width = i16(fp.read(2))
-                ascent = i16(fp.read(2))
-                descent = i16(fp.read(2))
-                attributes = i16(fp.read(2))
-                xsize = right - left
-                ysize = ascent + descent
-                append((xsize, ysize, left, right, width, ascent, descent, attributes))
+                append(*(i16(fp.read(2)) for _ in range(6)))
 
         return metrics
 
