_load_bmp reads nb_colors from bytes 0x2E-0x31 of the BITMAPINFOHEADER as a signed int and passes it straight to colormap.assign(nb_colors). The only check is for nb_colors == 0, any non-zero value goes through. With nb_colors = 0x3FFFFFFF and bpp = 8 the call becomes colormap.assign(0x3FFFFFFF) and tries to allocate ~4GB. It stays under the internal cimg_max_buf_size (16GB on 64-bit) so CImg does not reject it,
and the process runs out of memory.
Current code (CImg.h around line 56645):
int nb_colors = header[0x2E] + (header[0x2F]<<8) + (header[0x30]<<16) + (header[0x31]<<24);
// ...
if (bpp<16) { if (!nb_colors) nb_colors = 1<<bpp; } else nb_colors = 0;
if (nb_colors) { colormap.assign(nb_colors); cimg::fread(colormap._data,nb_colors,nfile); }
The check on nb_colors only handles the zero case. Negative values from the signed int cast, or values larger than 1<<bpp, are never clamped.
CVE-2022-1325 fixed the dx/dy allocation in 3.1.0 by adding cimg_max_buf_size, but that limit is 16GB so it does not catch a 4GB colormap. The nb_colors path was not covered by that fix.
To Reproduce
Attached PoC: poc_nbcolors_oom.bmp (55 bytes).
Reproduces on current master (commit 3de5134, version 3.7.5-pre) with a minimal harness calling CImg().load_bmp(path).
ASan output:
ERROR: libFuzzer: out-of-memory (malloc(4294967292))
#8 in cimg_library::CImg<int>::assign() CImg.h:13579
#9 in cimg_library::CImg<unsigned char>::_load_bmp() CImg.h:56649
Expected behavior
Reject the file or clamp nb_colors to the valid range for the bpp.
Fix idea
--- a/CImg.h
+++ b/CImg.h
@@ -56645,7 +56645,11 @@
int nb_colors = header[0x2E] + (header[0x2F]<<8) + (header[0x30]<<16) + (header[0x31]<<24);
// ...
- if (bpp<16) { if (!nb_colors) nb_colors = 1<<bpp; } else nb_colors = 0;
+ if (bpp<16) {
+ const int max_colors = 1 << bpp;
+ if (nb_colors <= 0 || nb_colors > max_colors) nb_colors = max_colors;
+ } else nb_colors = 0;
if (nb_colors) { colormap.assign(nb_colors); cimg::fread(colormap._data,nb_colors,nfile); }
Same pattern (signed int read from file, passed to assign without bounds)
poc_nbcolors_oom.bmp
probably exists in other loaders too, worth a grep.
_load_bmp reads nb_colors from bytes 0x2E-0x31 of the BITMAPINFOHEADER as a signed int and passes it straight to colormap.assign(nb_colors). The only check is for nb_colors == 0, any non-zero value goes through. With nb_colors = 0x3FFFFFFF and bpp = 8 the call becomes colormap.assign(0x3FFFFFFF) and tries to allocate ~4GB. It stays under the internal cimg_max_buf_size (16GB on 64-bit) so CImg does not reject it,
and the process runs out of memory.
Current code (CImg.h around line 56645):
The check on nb_colors only handles the zero case. Negative values from the signed int cast, or values larger than 1<<bpp, are never clamped.
CVE-2022-1325 fixed the dx/dy allocation in 3.1.0 by adding cimg_max_buf_size, but that limit is 16GB so it does not catch a 4GB colormap. The nb_colors path was not covered by that fix.
To Reproduce
Attached PoC: poc_nbcolors_oom.bmp (55 bytes).
Reproduces on current master (commit 3de5134, version 3.7.5-pre) with a minimal harness calling CImg().load_bmp(path).
ASan output:
Expected behavior
Reject the file or clamp nb_colors to the valid range for the bpp.
Fix idea
Same pattern (signed int read from file, passed to assign without bounds)
poc_nbcolors_oom.bmp
probably exists in other loaders too, worth a grep.