CVE-2026-54058: Pillow: OOB read/DoS via unchecked mmap stride (McIdas)

GHSA-62p4-gmf7-7g93 HIGH CISA: TRACK*
Published July 20, 2026
CISO Take

A memory-safety bug in Pillow's McIdas AREA image plugin lets a crafted image file bypass a bounds check in the mmap-based fast-path loader, so decoding the image reads far past the mapped file region into adjacent process memory. This affects any AI/ML pipeline that accepts user-supplied images, saves them to a path, and opens them with Pillow — a common pattern in vision-model inference services, multimodal RAG ingestion, and dataset preprocessing jobs — turning a routine image upload into either an information-disclosure vector (leaked heap contents served back or stored) or a reliable SIGBUS crash. Risk is tempered by low EPSS (0.00384, roughly bottom-third exploitation likelihood despite the 69th percentile label), absence from CISA KEV, and no public PoC or Nuclei template, but the vulnerability class (CWE-125) is trivial to weaponize once understood and Pillow has a broad AI/ML footprint even though this CVE currently shows 0 tracked downstream dependents in our data. Action: upgrade to Pillow 12.3.0; if you cannot patch immediately, avoid opening untrusted images by filesystem path (load via BytesIO instead, which skips the mmap fast path) and monitor image-processing workers for SIGBUS/segfault crash spikes as a detection signal.

Sources: NVD GitHub Advisory EPSS CISA KEV ATLAS

What is the risk?

High severity (CWE-125, out-of-bounds read) but moderate real-world exploitation risk today: EPSS is very low (0.00384), the CVE is not in CISA KEV, and there is no public exploit code or Nuclei template. The attack requires the victim application to open an attacker-supplied file from a filesystem path (not from an in-memory buffer) using Pillow's default plugin set, and for the McIdas plugin's magic-byte prefix to be accepted — a narrower but still common condition (upload-to-temp-file-then-open is a standard pattern). Exposure in AI/ML systems is elevated because image ingestion (uploads for vision models, dataset loaders, thumbnailing/preview services) frequently follows exactly this save-then-open pattern. No authentication or user interaction beyond submitting a file is required, and the DoS variant (SIGBUS) is reliable, making it attractive for disruption even where information disclosure yield is uncertain.

How does the attack unfold?

Malicious file crafting & delivery
Attacker crafts a McIdas AREA file with forged header words (stride=1, oversized xsize) and submits it to an application feature that accepts image uploads and opens files by filesystem path.
Exploitation via image decode
The application calls Image.open(path), triggering Pillow's mmap-based fast path, which validates only offset+ysize*stride against the file size and misses that stride is smaller than the real row width.
AML.T0049
Impact: disclosure or crash
Subsequent pixel access (tobytes/getpixel/convert/save) reads xsize*pixelsize bytes per row far past the mmap boundary, leaking adjacent process memory into the returned image or crashing the worker with SIGBUS.

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Microsoft APM pip < 12.3.0 12.3.0
3.9K Pushed 3d ago 64% patched ~23d to patch Full package profile →

Do you use Microsoft APM? You're affected.

How severe is it?

CVSS 3.1
N/A
EPSS
0.5%
chance of exploitation in 30 days
Higher than 43% of all CVEs
Exploitation Status
Exploit Available
Exploitation: MEDIUM
Sophistication
Moderate
Exploitation Confidence
medium
CISA SSVC: Public PoC
Composite signal derived from CISA KEV, VulnCheck KEV, CISA SSVC, EPSS, Metasploit, Exploit-DB, trickest/cve, Nuclei templates, and inthewild.io exploitation reports.

What should I do?

1 step
  1. 1) Patch: upgrade Pillow to 12.3.0 or later, where map.c and McIdasImagePlugin.py validate stride >= linesize/xsize*pixelsize. 2) Workaround if patching is delayed: read untrusted image bytes into a BytesIO buffer and call Image.open(BytesIO(data)) instead of opening by filesystem path — this avoids the mmap fast path (use_mmap requires self.filename). 3) Defense-in-depth: if your application does not need the McIdas AREA format, remove/disable the McIdas plugin or restrict accepted image formats via an explicit allowlist (e.g., only JPEG/PNG/WEBP) before calling Image.open. 4) Detection: monitor image-processing workers for SIGBUS/segfault crashes correlated with specific upload sources, and alert on abnormal process crash-loop patterns in inference/preprocessing containers. 5) Audit dependency: run pip list | grep -i pillow (or equivalent SCA scan) across all AI/ML services and CI images to confirm no service pins Pillow < 12.3.0.

What does CISA's SSVC say?

Decision Track*
Exploitation poc
Automatable No
Technical Impact partial

Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.2.6 - AI system security
NIST AI RMF
MEASURE 2.7 - AI system security and resilience are evaluated and documented
OWASP LLM Top 10
LLM03 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-54058?

A memory-safety bug in Pillow's McIdas AREA image plugin lets a crafted image file bypass a bounds check in the mmap-based fast-path loader, so decoding the image reads far past the mapped file region into adjacent process memory. This affects any AI/ML pipeline that accepts user-supplied images, saves them to a path, and opens them with Pillow — a common pattern in vision-model inference services, multimodal RAG ingestion, and dataset preprocessing jobs — turning a routine image upload into either an information-disclosure vector (leaked heap contents served back or stored) or a reliable SIGBUS crash. Risk is tempered by low EPSS (0.00384, roughly bottom-third exploitation likelihood despite the 69th percentile label), absence from CISA KEV, and no public PoC or Nuclei template, but the vulnerability class (CWE-125) is trivial to weaponize once understood and Pillow has a broad AI/ML footprint even though this CVE currently shows 0 tracked downstream dependents in our data. Action: upgrade to Pillow 12.3.0; if you cannot patch immediately, avoid opening untrusted images by filesystem path (load via BytesIO instead, which skips the mmap fast path) and monitor image-processing workers for SIGBUS/segfault crash spikes as a detection signal.

Is CVE-2026-54058 actively exploited?

No confirmed active exploitation of CVE-2026-54058 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-54058?

1) Patch: upgrade Pillow to 12.3.0 or later, where `map.c` and `McIdasImagePlugin.py` validate `stride >= linesize`/`xsize*pixelsize`. 2) Workaround if patching is delayed: read untrusted image bytes into a `BytesIO` buffer and call `Image.open(BytesIO(data))` instead of opening by filesystem path — this avoids the mmap fast path (`use_mmap` requires `self.filename`). 3) Defense-in-depth: if your application does not need the McIdas AREA format, remove/disable the McIdas plugin or restrict accepted image formats via an explicit allowlist (e.g., only JPEG/PNG/WEBP) before calling `Image.open`. 4) Detection: monitor image-processing workers for SIGBUS/segfault crashes correlated with specific upload sources, and alert on abnormal process crash-loop patterns in inference/preprocessing containers. 5) Audit dependency: run `pip list | grep -i pillow` (or equivalent SCA scan) across all AI/ML services and CI images to confirm no service pins Pillow < 12.3.0.

What systems are affected by CVE-2026-54058?

This vulnerability affects the following AI/ML architecture patterns: model serving / inference APIs, multimodal RAG ingestion pipelines, dataset preprocessing pipelines, image upload / thumbnailing services feeding AI pipelines.

What is the CVSS score for CVE-2026-54058?

No CVSS score has been assigned yet.

What is the AI security impact?

Affected AI Architectures

model serving / inference APIsmultimodal RAG ingestion pipelinesdataset preprocessing pipelinesimage upload / thumbnailing services feeding AI pipelines

MITRE ATLAS Techniques

AML.T0010.001 AI Software
AML.T0049 Exploit Public-Facing Application

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.6.2.6
NIST AI RMF: MEASURE 2.7
OWASP LLM Top 10: LLM03

What are the technical details?

Original Advisory

## Summary When Pillow loads an uncompressed image whose tile uses the `raw` codec and a mode in `Image._MAPMODES`, and the image was opened **from a filename**, it memory-maps the file and builds the image's row pointers directly into the mapping via `PyImaging_MapBuffer` (`src/map.c`). The per-row spacing (`stride`) is taken from the tile arguments. `map.c` validates `offset + ysize*stride <= buffer_len` but **never checks that `stride` is at least the natural row width `xsize * pixelsize`**. The **McIdas** AREA plugin (`McIdasImagePlugin.py`) derives `stride`, `offset`, `xsize`, and `ysize` directly from attacker-controlled 32-bit header words with no validation. By supplying a `stride` far smaller than the row width, an attacker makes each row pointer read `xsize*pixelsize` bytes that run past the mapped region. Accessing the pixels (e.g. `Image.tobytes()`, `getpixel`, `convert`, `save`) then reads adjacent process memory (information disclosure) or faults (SIGBUS, denial of service). ## Complete Code Trace **Step 1: `McIdasImageFile._open`** - turns attacker header words into image size, file offset, and row stride with no validation. ```python # src/PIL/McIdasImagePlugin.py:41-70 s = self.fp.read(256) if not _accept(s) or len(s) != 256: # _accept: prefix == b"\x00\x00\x00\x00\x00\x00\x00\x04" raise SyntaxError(...) self.area_descriptor = w = [0, *struct.unpack("!64i", s)] # w[1..64] = signed BE int32, ALL attacker-controlled if w[11] == 1: mode = rawmode = "L" # pixelsize 1, in _MAPMODES elif w[11] == 2: mode = rawmode = "I;16B" # pixelsize 2, in _MAPMODES ... self._mode = mode self._size = w[10], w[9] # (xsize, ysize) <-- attacker offset = w[34] + w[15] # <-- attacker stride = w[15] + w[10] * w[11] * w[14] # <-- attacker (set w[14]=0, w[15]=1 => stride=1) self.tile = [ ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1)) ] ``` **Step 2: `ImageFile.load` (mmap branch)** - selects mmap and delegates to `map_buffer`. ```python # src/PIL/ImageFile.py:322-348 if use_mmap: # use_mmap = self.filename and len(self.tile) == 1 decoder_name, extents, offset, args = self.tile[0] if (decoder_name == "raw" and isinstance(args, tuple) and len(args) >= 3 and args[0] == self.mode and args[0] in Image._MAPMODES): if offset < 0: # only lower-bound guard on offset raise ValueError("Tile offset cannot be negative") with open(self.filename) as fp: self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ) if offset + self.size[1] * args[1] > self.map.size(): # == offset + ysize*stride; NO stride>=linesize check raise OSError("buffer is not large enough") self.im = Image.core.map_buffer( self.map, self.size, decoder_name, offset, args # args = ("L", stride, 1) ) ``` **Step 3: `PyImaging_MapBuffer`** - builds row pointers at `stride` spacing into the mmap; validates everything except `stride >= row width`. ```c /* src/map.c:65-140 */ if (!PyArg_ParseTuple(args, "O(ii)sn(sii)", &target, &xsize, &ysize, &codec, &offset, &mode_name, &stride, &ystep)) return NULL; ... const ModeID mode = findModeID(mode_name); /* "L" */ if (stride <= 0) { /* attacker sets stride=1 (>0) -> NOT recomputed */ if (mode == IMAGING_MODE_L || mode == IMAGING_MODE_P) stride = xsize; else if (isModeI16(mode)) stride = xsize * 2; else stride = xsize * 4; } if (stride > 0 && ysize > PY_SSIZE_T_MAX / stride) {/* overflow guard only */ PyErr_SetString(PyExc_MemoryError, "Integer overflow in ysize"); return NULL; } size = (Py_ssize_t)ysize * stride; /* = 1*1 = 1 */ if (offset > PY_SSIZE_T_MAX - size) { ... } ... if (offset + size > view.len) { /* 1 + 1 = 2 <= 256 -> PASSES */ PyErr_SetString(PyExc_ValueError, "buffer is not large enough"); PyBuffer_Release(&view); return NULL; } im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance)); /* im->linesize = xsize * pixelsize = 200000 (the REAL per-row read width) */ /* setup file pointers -- NO check that stride >= im->linesize */ if (ystep > 0) { for (y = 0; y < ysize; y++) { im->image[y] = (char *)view.buf + offset + y * stride; /* row points into mmap, spacing=1 */ } } else { ... } ``` `im->linesize` (the number of bytes any consumer reads per row) is `xsize * pixelsize = 200000`, but the row pointers are only `stride = 1` byte apart and the buffer is only `offset + ysize*stride = 2` bytes "claimed". Nothing reconciles the two. **Step 4: pixel access (`Image.tobytes()` → raw encoder `copy1`)** - reads `linesize` bytes from `im->image[0]`, i.e. `xsize` bytes starting at `view.buf + offset`, running far past the mmap. ```c /* the raw "L" packer copies linesize (=xsize) bytes per row from im->image[y]; for row 0 that is view.buf+1 .. view.buf+1+200000, vs a 256-byte file. */ ``` ## Chain Summary ``` SOURCE: McIdas AREA header words w[9],w[10],w[11],w[14],w[15],w[34] (Image.open on a path) ↓ McIdasImagePlugin._open: stride = w[15]+w[10]*w[11]*w[14] -> attacker sets stride=1 [McIdasImagePlugin.py:66] ↓ tile = ("raw", (0,0,xsize,1), offset, ("L", 1, 1)) [McIdasImagePlugin.py:68] GADGET: ImageFile.load mmap branch -- only checks offset+ysize*stride<=len <- BUG: no stride>=linesize check [ImageFile.py:343] ↓ core.map_buffer(map, (xsize,1), "raw", offset, ("L",1,1)) [ImageFile.py:346] SINK: PyImaging_MapBuffer: im->image[0] = view.buf + offset + 0*stride; linesize=xsize [map.c:134] ↓ Image.tobytes() raw "L" encoder reads linesize (=xsize) bytes from im->image[0] IMPACT: reads xsize bytes from a tiny mmap -> OOB read of adjacent process memory (leak) or SIGBUS (DoS) ``` ## Proof of Concept See attached [poc.zip](https://github.com/user-attachments/files/28460498/poc.zip) ## Impact on a Parent Application Any application that opens image files supplied by users **from a path on disk** (the common pattern: save upload to a temp file, then `Image.open(path)`), has the default plugin set (McIdas is registered by default), and subsequently reads/returns/re-encodes the decoded pixels (thumbnailing, format conversion, serving a preview), is exposed: - **Information disclosure (High):** the decoded "image" contains bytes of the worker process's adjacent heap/mapped memory, which the app then serves or stores - potentially leaking secrets, credentials, or other users' data. - **Denial of service (High):** a larger `xsize` reliably crashes the worker with SIGBUS. ## Suggested fix Core fix in `src/map.c` (`PyImaging_MapBuffer`): reject `offset < 0` and `stride < im->linesize`. Defense-in-depth in `McIdasImagePlugin._open`: reject `offset < 0` or `stride < xsize*pixelsize` .

Exploitation Scenario

An attacker interacts with a public-facing AI service that accepts image uploads (e.g., a vision-model inference endpoint, a multimodal chat feature, or a document/RAG ingestion pipeline that extracts images). The service saves the upload to a temporary file and calls `Image.open(path)` for validation, thumbnailing, or format normalization before passing it to the model. The attacker crafts a file with the McIdas AREA 8-byte magic prefix and forges header words so that `stride=1` while `xsize` is large (e.g., 200000), producing a mismatch between the tiny mmap region Pillow validates as 'large enough' and the much larger row width the pixel-access code actually reads. When the service subsequently calls `.tobytes()`, `.convert()`, `.save()`, or `getpixel()` on the decoded image (routine operations in any preprocessing pipeline), Pillow reads far past the mapped file boundary — either returning bytes of adjacent process memory that get embedded in a response, thumbnail, or stored artifact (information disclosure), or triggering a SIGBUS crash that takes down the inference/preprocessing worker (denial of service) and can be repeated to degrade the service.

Weaknesses (CWE)

CWE-125 — Out-of-bounds Read: The product reads data past the end, or before the beginning, of the intended buffer.

  • [Implementation] Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylis
  • [Architecture and Design] Use a language that provides appropriate memory abstractions.

Source: MITRE CWE corpus.

Timeline

Published
July 20, 2026
Last Modified
July 20, 2026
First Seen
July 21, 2026

Related Vulnerabilities