The size check in _load_pnm compares the declared pixel count against the file size, but the multiplication is done in unsigned int and wraps at
2^32. Oversized images go through, then assign() uses 64-bit arithmetic and tries to allocate the full huge size.
Current code (CImg.h around line 57390 and 57428 in _load_pnm):
unsigned int ppm_type, W, H, D = 1, colormax = 255;
// ... W, H, D parsed from the ASCII header ...
const cimg_int64 siz = cimg::fsize(filename);
if (W*H*D > siz) // unsigned int overflow here
throw CImgIOException(...);
assign(W, H, D, channels); // safe_size is 64-bit, sees the real size
With W = 65536, H = 65537, D = 1:
W*H*D as unsigned int = 65536 * 65537 = 0x100010000 -> wraps to 65536
65536 > siz -> false for any file >= 64KB, so check passes
assign(65536, 65537, 1, 1) allocates 4,295,032,832 bytes -> OOM
CVE-2020-25693 was the integer overflow in _load_pnm fixed in 2.9.3. The fix there added 64-bit checks inside assign/safe_size, but this earlier pre-assign guard was left in 32-bit. So the guard is still bypassable on current master (commit 3de5134, version 3.7.5-pre).
To Reproduce
Attached PoC: poc_pnm_intoverflow.pgm (65,556 bytes).
Header: P5\n65536 65537\n255\n followed by 65537 bytes of pixel data. Load with CImg().load_pnm(path).
ASan output:
ERROR: libFuzzer: out-of-memory (malloc(4295032832))
#8 in cimg_library::CImg<unsigned char>::assign() CImg.h:13579
#9 in cimg_library::CImg<unsigned char>::_load_pnm() (via assign)
Expected behavior
Reject the image, same as the internal safe_size does when it is reached directly.
Fix idea
--- a/CImg.h
+++ b/CImg.h
@@ -57425,7 +57425,7 @@
const cimg_int64 siz = cimg::fsize(filename);
- if (W*H*D > siz)
+ if ((cimg_int64)W*H*D > siz)
throw CImgIOException(_cimg_instance
"load_pnm(): File size (%lld) inconsistent with "
"image dimensions (%u,%u,%u).",
cimg_instance,
(long long)siz,W,H,D);
Same pattern (unsigned int multiplication against a 64-bit size) is worth checking in the other ASCII/binary loaders (pandore, inr, etc.).
poc_pnm_intoverflow.zip
The size check in _load_pnm compares the declared pixel count against the file size, but the multiplication is done in unsigned int and wraps at
2^32. Oversized images go through, then assign() uses 64-bit arithmetic and tries to allocate the full huge size.
Current code (CImg.h around line 57390 and 57428 in _load_pnm):
With W = 65536, H = 65537, D = 1:
CVE-2020-25693 was the integer overflow in _load_pnm fixed in 2.9.3. The fix there added 64-bit checks inside assign/safe_size, but this earlier pre-assign guard was left in 32-bit. So the guard is still bypassable on current master (commit 3de5134, version 3.7.5-pre).
To Reproduce
Attached PoC: poc_pnm_intoverflow.pgm (65,556 bytes).
Header:
P5\n65536 65537\n255\nfollowed by 65537 bytes of pixel data. Load with CImg().load_pnm(path).ASan output:
Expected behavior
Reject the image, same as the internal safe_size does when it is reached directly.
Fix idea
Same pattern (unsigned int multiplication against a 64-bit size) is worth checking in the other ASCII/binary loaders (pandore, inr, etc.).
poc_pnm_intoverflow.zip