Skip to content
Fltk Bmp Image Stack Heap Overflow

Fltk Bmp Image Stack Heap Overflow

hackmd.io • October 27, 2026

# CVE-2026-116723, CVE-2026-116724: FLTK BMP Image Loader: Stack and Heap Buffer Overflow via Crafted BMP File

![Vulnerability Type](

![CWE-121](

![CWE-190](

![CWE-122](

![Component](

![Severity](

![Status](

**Reported by:** HaiND (Nguyen Dinh Hai), Post and Telecommunication Institute of Technology

**Project:** [fltk/fltk]( Fast Light Tool Kit, BMP image loader (`src/Fl_BMP_Image.cxx`)

**Affected:** release-1.4.5 (commit `a9b1113516ffd15fc7602a6d425a317df30f4720`) and all earlier releases, confirmed on master HEAD `7715487f347f69b523f7fcb9509f5a6289e7c2f1`

**Class:** CWE-121 (Stack-based Buffer Overflow), CWE-190 (Integer Overflow or Wraparound), CWE-122 (Heap-based Buffer Overflow)

The FLTK BMP image loader in `Fl_BMP_Image::load_bmp_()` (`src/Fl_BMP_Image.cxx`) has two memory-safety vulnerabilities reachable from the public constructor under default configuration. **Finding 1:** the `colors_used` field from the BMP header is read as a 32-bit integer and used directly as the loop bound for writing into a fixed-size stack buffer `colormap[256][3]` (768 bytes) with no upper-bound check. An attacker sets `colors_used` to any value above 256 and supplies matching colormap data, overflowing the stack buffer with fully controlled bytes past the saved frame pointer and return address. **Finding 2:** the pixel buffer allocation `new uchar[width * height * bDepth]` performs the multiplication in `int` arithmetic while the preceding size-limit check uses `size_t`. The default `max_size()` value is `SIZE_MAX` (no real limit). Crafted dimensions (for example width=32768, height=32768, depth=32) cause the `int` product to overflow to 0, resulting in a zero-byte allocation followed by a ~4 GB write. Both findings require only opening a crafted `.bmp` file through any FLTK application.

### FINDING 1: Stack buffer overflow via `colors_used`

The function `load_bmp_()` declares a fixed-size stack array for the BMP color table:

// src/Fl_BMP_Image.cxx:154 (release-1.4.5)

This buffer holds 256 entries of 3 bytes each (768 bytes total), matching the BMP specification's maximum of 256 palette entries for 8-bit images.

The `colors_used` value is read from byte offset 46 in the BMP info header as an unsigned 32-bit integer:

colors_used = rdr.read_dword(); // 46: uint32 number of colors used, 0 = all colors

The only modification to `colors_used` is a default-value assignment that fires only when the field is exactly zero:

Any nonzero `colors_used` from the file passes through without a bounds check against 256.

The colormap-reading loop uses `colors_used` as its bound:

When `colors_used` exceeds 256, the loop writes past the end of the 768-byte stack buffer. Each iteration writes 3 attacker-controlled bytes (the RGB values of the fake palette entry) to `colormap[i][0..2]`. The overflow corrupts adjacent stack variables (`havemask`, `use_5_6_5`, `bit`, `byte`, `ptr`, etc.), the saved frame pointer, and the return address.

#### Sibling contrast: GIF colormap is bounded by construction

The GIF loader computes its color table size from a 3-bit field in the packed byte:

int ColorMapSize = HasColormap ? 2 max_size() ) {

Fl::warning("BMP file \"%s\" is too large!\n", rdr.name());

The multiplication casts `width` to `size_t`, so the arithmetic is done in 64-bit on 64-bit platforms. This correctly computes the actual size. However, `max_size()` is initialized to `~((size_t)0)` (SIZE_MAX) in `src/Fl_Image.cxx:335`:

size_t Fl_RGB_Image::max_size_ = ~((size_t)0);

The check never rejects any allocation under the default configuration.

#### The allocation uses `int` arithmetic

array = new uchar[width * height * bDepth];

`width`, `height`, and `bDepth` are all `int`. The expression `width * height * bDepth` is evaluated in `int` arithmetic. When the product exceeds `INT_MAX` (2,147,483,647), the result wraps (undefined behavior in the standard, wrapping in practice on all major compilers). If the wrapped value is a small positive number, the allocation succeeds with a tiny buffer.

Example values: width=32768, height=32768, depth=32 (bDepth=4).

- `32768 * 32768` = 1,073,741,824 (fits in `int`)

- `1,073,741,824 * 4` = 4,294,967,296 = 2^32, wraps to 0 in 32-bit `int`

- The row loop then writes 32768 * 32768 * 4 = 4,294,967,296 bytes

On master HEAD (7715487), the allocation is at line 321 as `new uchar[width * height * dstBDepth]` with the same pattern.

participant BMP as Fl_BMP_Image::load_bmp_

participant Stack as Stack Buffer colormap 256x3

A->>App: Deliver crafted BMP file (colors_used=512)

BMP->>BMP: Read BMP header, colors_used=512

BMP->>BMP: Check colors_used==0 fails, no clamp to 256

BMP->>Stack: colormap[i][0..2] = attacker RGB bytes

Note over Stack: Overflow at i=256, 768 bytes past buffer end

Note over Stack: Overwrites saved frame pointer and return address

BMP-->>App: Returns with corrupted stack frame

A minimal crafted BMP triggers Finding 1 (the stack overflow). The file needs only:

2. Bytes 2-5: file size (any value, not validated)

4. Bytes 10-13: pixel data offset (set to point past the colormap)

5. Bytes 14-17: info header size = 40 (standard BITMAPINFOHEADER)

15. Bytes 54 onward: 512 palette entries, each 4 bytes (BGR + pad), totaling 2048 bytes of attacker-controlled data

The parser reads the header, sees `colors_used = 512`, skips the default-assignment (because 512 != 0), and enters the colormap loop for 512 iterations. Starting at `colormap[0]`, the loop writes 3 bytes per entry. At `i = 256` (byte offset 768 into the 768-byte buffer), the writes begin overflowing the stack. The attacker's RGB bytes overwrite adjacent stack variables and, at the correct offset, the saved frame pointer and return address.

For Finding 2, the crafted file uses width=32768, height=32768, depth=32. The parser's size check passes (default max_size is SIZE_MAX). The allocation `new uchar[32768 * 32768 * 4]` overflows to `new uchar[0]`. The 32-bit pixel-reading loop then writes ~4 GB into the zero-byte heap allocation.

This analysis is based on static source review of the released source at commit `a9b1113516ffd15fc7602a6d425a317df30f4720` (release-1.4.5) with the same code confirmed on master HEAD `7715487f347f69b523f7fcb9509f5a6289e7c2f1`. Runtime verification (ASan build and PoC execution) was not performed in this phase.

- The `colormap[256][3]` buffer is 768 bytes on the stack (line 154).

- `colors_used` is read as a raw 32-bit value from the file (line 222) and used as the loop bound (line 257) with no clamp to 256.

- The default-assignment guard (line 254) fires only when `colors_used == 0`.

- The pixel allocation (line 285) uses `int` arithmetic while the size check (line 280) uses `size_t` and an unlimited default cap.

- `max_size_` is initialized to SIZE_MAX (Fl_Image.cxx:335).

- Instruction-pointer control via the stack overflow.

- Write-what-where precision via the heap overflow.

- Specific consumer applications that load untrusted BMP files through FLTK.

**Finding 1 (stack overflow):** An attacker who supplies a crafted BMP file to any FLTK application triggers a stack buffer overflow with fully attacker-controlled data (the fake palette RGB values). The overflow reaches the saved frame pointer and return address. In the absence of stack canaries, this is directly exploitable for code execution. With canaries enabled, it results in a controlled crash (stack smashing detected). The overflow distance is `(colors_used - 256) * 3` bytes, controllable from 3 bytes to over 2 GB.

**Finding 2 (heap overflow):** The integer overflow in the allocation produces a zero-byte or small heap buffer, followed by a massive write of pixel data from the file. This corrupts the heap metadata and adjacent heap objects. Exploitation depends on heap layout and allocator behavior.

**What is NOT established:** No instruction-pointer control was demonstrated. No code execution was demonstrated. The practical exploitability depends on the compiler's stack protection features (canaries, ASLR) for Finding 1 and on heap allocator hardening for Finding 2.

**Attack surface:** Any application using FLTK that loads BMP files from untrusted sources. This includes image viewers, GUI editors, and any tool that uses `Fl_BMP_Image` or `Fl_Shared_Image` with BMP input. The `Fl_ICO_Image` class also reaches `load_bmp_()` for ICO files containing BMP resources.

**Finding 1 - Where exactly:** `src/Fl_BMP_Image.cxx`, between line 255 and line 257 (after the default-assignment guard, before the colormap loop).

**Finding 2 - Where exactly:** `src/Fl_BMP_Image.cxx`, line 285 (the allocation).

- array = new uchar[width * height * bDepth];

+ array = new uchar[(size_t)width * height * bDepth];

And change `max_size_` initialization to a sane default (for example 256 MB or 1 GB instead of SIZE_MAX):

- size_t Fl_RGB_Image::max_size_ = ~((size_t)0);

+ size_t Fl_RGB_Image::max_size_ = (size_t)256 * 1024 * 1024;

**Short-term:** Clamp `colors_used` to 256 and cast the allocation multiplication to `size_t`.

**Medium-term:** Set `max_size_` to a reasonable default. Add explicit dimension validation (reject negative width, excessive dimensions).

**Defense-in-depth:** Compile with stack canaries (`-fstack-protector-strong`) and AddressSanitizer during testing. Consider replacing the stack-allocated colormap with a heap allocation sized to `colors_used` (after validation).

- **Upstream repository:** [fltk/fltk](

- **Pinned release:** [release-1.4.5]( commit [`a9b1113`](

- **Vulnerable source (pinned):** [`src/Fl_BMP_Image.cxx` at a9b1113](

- **max_size_ initialization:** [`src/Fl_Image.cxx:335` at a9b1113](

- **No prior CVE** for the FLTK C++ library's BMP image loader. The Rust `fltk` crate CVEs (CVE-2021-28306/7/8) are unrelated.

- **No FLTK project in OSS-Fuzz.** No public fuzzing harness targets these image decoders.

- **PR #228** fixed crash-on-invalid-BMP (signature check) but did not add a `colors_used` bound.

- **PR #592** added `Fl_Image_Reader` length checks for memory reads but did not address the colormap loop or allocation overflow.

Reported by HaiND (Nguyen Dinh Hai), Post and Telecommunication Institute of Technology, [github.com/Haind03](