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_MAX` `UINT32_MAX + 1 = 4’294’967’296` `[1]includes sizeof(float):` `4’294’967’296 * 4 = 17’179’869’184 bytes` `max_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 () 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 () 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.