CVE-2026-24450
An integer overflow vulnerability exists in the uncompressed_fp_dng_load_raw functionality of LibRaw Commit 8dc68e2. A specially crafted malicious file can lead to a heap buffer overflow. An attacker can provide a malicious file to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
LibRaw Commit 8dc68e2
LibRaw - https://github.com/LibRaw/LibRaw.git
8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-190 - Integer Overflow or Wraparound
LibRaw is an open-source C/C++ library for reading, decoding, and processing RAW image files from multiple camera manufacturers.
LibRaw is a widely-used library for reading RAW image files from digital cameras. The library includes support for uncompressed floating-point DNG files. The decoder routine is vulnerable to a heap buffer overflow because the size calculation for the pixel buffer uses 32-bit arithmetic that can overflow when processing attacker-controlled dimension values, despite a correct 64-bit memory limit check.
Note: This vulnerability is not exploitable with the default max_raw_memory_mb. See memory limit requirements below.
The vulnerable function uncompressed_fp_dng_load_raw() in src/decoders/fp_dng.cpp:
void LibRaw::uncompressed_fp_dng_load_raw()
{
[...]
tile_stripe_data_t tiles;
tiles.init(ifd, imgdata.sizes, libraw_internal_data.unpacker_data,
libraw_internal_data.unpacker_data.order,
libraw_internal_data.internal_data.input);
[1] INT64 allocsz = INT64(tiles.tileCnt) * INT64(tiles.tileWidth) *
INT64(tiles.tileHeight) * INT64(ifd->samples) *
INT64(sizeof(float));
[2] if (allocsz > INT64(imgdata.rawparams.max_raw_memory_mb) * INT64(1024 * 1024))
throw LIBRAW_EXCEPTION_TOOBIG;
if (ifd->sample_format == 3)
[3] float_raw_image = (float *)calloc(tiles.tileCnt * tiles.tileWidth *
tiles.tileHeight * ifd->samples,
sizeof(float));
[...]
for (size_t y = 0, t = 0; y < imgdata.sizes.raw_height; y += tiles.tileHeight)
{
for (unsigned x = 0; x < imgdata.sizes.raw_width && t < (unsigned)tiles.tileCnt;
x += tiles.tileWidth, ++t)
{
libraw_internal_data.internal_data.input->seek(tiles.tOffsets[t], SEEK_SET);
size_t rowsInTile = y + tiles.tileHeight > imgdata.sizes.raw_height ? imgdata.sizes.raw_height - y : tiles.tileHeight;
size_t colsInTile = x + tiles.tileWidth > imgdata.sizes.raw_width ? imgdata.sizes.raw_width - x : tiles.tileWidth;
size_t inrowbytes = colsInTile * bytesps * ifd->samples;
int fullrowbytes = tiles.tileWidth *bytesps * ifd->samples;
size_t outrowbytes = colsInTile * sizeof(float) * ifd->samples;
for (size_t row = 0; row < rowsInTile; ++row)
{
[4] unsigned char *dst = fullrowbytes > int(inrowbytes) ? rowbuf.data():
(unsigned char *)&float_raw_image
[((y + row) * imgdata.sizes.raw_width + x) * ifd->samples];
[5] libraw_internal_data.internal_data.input->read(dst, 1, fullrowbytes);
[...]
}
}
}
}
At [4], fullrowbytes is the tile row size in bytes (tileWidth * bytesps * samples, where bytesps = bps / 8), and inrowbytes is the actual row size for the current tile position. When processing full tiles, the condition is false and dst points directly into float_raw_image.
The tiles.tileWidth and tiles.tileHeight values come from the TileWidth and TileLength TIFF tags (or ImageWidth/RowsPerStrip for strip layouts). The tiles.tileCnt is calculated as ceil(ImageWidth/TileWidth) * ceil(ImageLength/TileHeight). The ifd->samples comes from SamplesPerPixel. All of these are attacker-controlled through the DNG file.
At [1], the code correctly calculates the required buffer size using 64-bit arithmetic with explicit INT64() casts. At [2], this value is compared against the memory limit.
However, at [3], the actual allocation uses 32-bit arithmetic without the INT64 casts:
calloc(tiles.tileCnt * tiles.tileWidth * tiles.tileHeight * ifd->samples, sizeof(float))
The types involved in the first argument (count) are all 32-bit:
- tiles.tileCnt - int
- tiles.tileWidth - unsigned
- tiles.tileHeight - unsigned
- ifd->samples - int
This multiplication is performed in 32-bit arithmetic and overflows when the product exceeds UINT32_MAX. The second argument (sizeof(float)) is 64-bit size_t, but it’s evaluated separately and doesn’t promote the first argument. The overflowed result is used to allocate the buffer.
The memory check at [1]-[2] uses 64-bit arithmetic and correctly prevents exploitation with default settings:
[3] requires: tileCnt * tileWidth * tileHeight * samples > UINT32_MAXUINT32_MAX + 1 = 4'294'967'296[1] includes sizeof(float): 4'294'967'296 * 4 = 17'179'869'184 bytesmax_raw_memory_mb >= 17'179'869'184 / (1'024 * 1'024) = 16'384 MB (~16 GB)The default max_raw_memory_mb is 2048 MB (2 GB), which rejects overflow-triggering dimensions before reaching the vulnerable allocation. Applications that increase this limit become vulnerable.
When processing full tiles or strips (fullrowbytes <= inrowbytes), the condition at [4] evaluates to false, and dst points directly into float_raw_image:
dst = &float_raw_image[((y + row) * imgdata.sizes.raw_width + x) * ifd->samples];
At [5], data is read from the file directly into this undersized buffer. The index calculation uses the original dimensions (raw_width, raw_height), causing writes far beyond the allocated region.
Example: With strip layout using width = 64000, height = 22370, samples = 3:
- Intended size: 64000 * 22370 * 3 = 4'295'040'000 elements
- Overflowed uint32_t: 72'704 elements
- Actual allocation: 72'704 * 4 = 290'816 bytes (~284 KB)
- First row attempts to write: 64000 * 3 * 4 = 768'000 bytes -> heap buffer overflow
Any application that calls unpack() on untrusted DNG files with max_raw_memory_mb greater than around 16GB is vulnerable to this heap buffer overflow, which can result in heap corruption and potential code execution.
=================================================================
==50166==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x000104a53c00 at pc 0x000103121740 bp 0x00016d4d2530 sp 0x00016d4d1ce0
WRITE of size 768000 at 0x000104a53c00 thread T0
#0 0x00010312173c in __asan_memmove+0x234 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x8573c)
#1 0x000102937ccc in LibRaw_buffer_datastream::read(void*, unsigned long, unsigned long) libraw_datastream.cpp:339
#2 0x0001028fc79c in LibRaw::uncompressed_fp_dng_load_raw() fp_dng.cpp:652
#3 0x0001029af79c in LibRaw::unpack() unpack.cpp:447
#4 0x0001028710c8 in main poc_fp_dng_overflow.cpp:54
#5 0x00018f671d50 (<unknown module>)
0x000104a53c00 is located 0 bytes after 291840-byte region [0x000104a0c800,0x000104a53c00)
allocated by thread T0 here:
#0 0x0001030d9620 in calloc+0x80 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d620)
#1 0x0001029b4e04 in LibRaw::calloc(unsigned long, unsigned long) utils_libraw.cpp:275
#2 0x0001028fc4ac in LibRaw::uncompressed_fp_dng_load_raw() fp_dng.cpp:626
#3 0x0001029af79c in LibRaw::unpack() unpack.cpp:447
#4 0x0001028710c8 in main poc_fp_dng_overflow.cpp:54
#5 0x00018f671d50 (<unknown module>)
SUMMARY: AddressSanitizer: heap-buffer-overflow libraw_datastream.cpp:339 in LibRaw_buffer_datastream::read(void*, unsigned long, unsigned long)
Shadow bytes around the buggy address:
0x000104a53980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x000104a53a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x000104a53a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x000104a53b00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x000104a53b80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x000104a53c00:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x000104a53c80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x000104a53d00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x000104a53d80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x000104a53e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x000104a53e80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==50166==ABORTING
2026-02-12 - Initial Vendor Contact
2026-02-12 - Vendor Disclosure
2026-04-06 - Vendor Patch Release
2026-04-07 - Public Release
Discovered by Francesco Benvenuto of Cisco Talos.