CVE-2026-54058: Pillow: OOB read/DoS via unchecked mmap stride (McIdas)
GHSA-62p4-gmf7-7g93 HIGH CISA: TRACK*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.
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?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Microsoft APM | pip | < 12.3.0 | 12.3.0 |
Do you use Microsoft APM? You're affected.
How severe is it?
What should I do?
1 step-
1) Patch: upgrade Pillow to 12.3.0 or later, where
map.candMcIdasImagePlugin.pyvalidatestride >= linesize/xsize*pixelsize. 2) Workaround if patching is delayed: read untrusted image bytes into aBytesIObuffer and callImage.open(BytesIO(data))instead of opening by filesystem path — this avoids the mmap fast path (use_mmaprequiresself.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 callingImage.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: runpip 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?
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:
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
MITRE ATLAS Techniques
AML.T0010.001 AI Software AML.T0049 Exploit Public-Facing Application Compliance Controls Affected
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.
References
- github.com/advisories/GHSA-62p4-gmf7-7g93
- github.com/python-pillow/Pillow/commit/6a8de891fb00968e5ea79bfa84368ed90b3cfc1d
- github.com/python-pillow/Pillow/pull/9719
- github.com/python-pillow/Pillow/releases/tag/12.3.0
- github.com/python-pillow/Pillow/security/advisories/GHSA-62p4-gmf7-7g93
- nvd.nist.gov/vuln/detail/CVE-2026-54058
Timeline
Related Vulnerabilities
CVE-2026-46858 9.1 Oracle APM: unauthenticated write/DoS via JVM Diagnostics
Same package: apm CVE-2026-57947 8.5 Pinpoint APM: SSRF via alarm webhook registration
Same package: apm CVE-2026-45539 7.4 Microsoft APM: symlink attack leaks host files in agent deps
Same package: apm CVE-2026-57948 6.8 Pinpoint: insecure JWT cookie enables session hijacking
Same package: apm CVE-2026-49835 5.9 Sigstore TSA: unbounded metrics label DoS
Same package: apm