CVE-2026-24660
A heap-based buffer overflow vulnerability exists in the x3f_load_huffman functionality of LibRaw Commit d20315b. 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 d20315b
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 Sigma/Foveon X3F files, which use Huffman compression for RAW sensor data. The Huffman decompression 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.
Note: This vulnerability is not exploitable with the default max_raw_memory_mb. See the end of this advisory for details on the memory limit requirements.
The function x3f_load_huffman() in src/x3f/x3f_utils_patched.cpp allocates a buffer for decompressed RAW data:
static void x3f_load_huffman(x3f_info_t *I, x3f_directory_entry_t *DE, ...)
{
x3f_directory_entry_header_t *DEH = &DE->header;
x3f_image_data_t *ID = &DEH->data_subsection.image_data;
x3f_huffman_t *HUF = new_huffman(&ID->huffman);
uint32_t size;
[...]
switch (ID->type_format)
{
case X3F_IMAGE_RAW_HUFFMAN_X530:
case X3F_IMAGE_RAW_HUFFMAN_10BIT:
[1] size = ID->columns * ID->rows * 3;
[...]
[2] HUF->x3rgb16.buf = malloc(sizeof(uint16_t) * size);
HUF->x3rgb16.data = (uint16_t *)HUF->x3rgb16.buf;
break;
[...]
}
[...]
}
The ID->columns and ID->rows values are read directly from the X3F image section header in the file and are attacker-controlled. At [1], the code calculates the buffer size as columns * rows * 3. Since both ID->columns and ID->rows are uint32_t, the multiplication is performed in 32-bit arithmetic and overflows when the product exceeds UINT32_MAX. At [2], this value is used to allocate a buffer, which can be much smaller than intended if the overflow occurred.
The Huffman decoder then writes data based on original dimensions:
static void huffman_decode_row(x3f_info_t * /*I*/, x3f_directory_entry_t *DE,
int /*bits*/, int row, int offset, int *minimum) {
[...]
int16_t c[3] = {(int16_t)offset, (int16_t)offset, (int16_t)offset};
[...]
for (col = 0; col < (int)ID->columns; col++)
{
int color;
for (color = 0; color < 3; color++)
{
uint16_t c_fix;
[3] c[color] += get_huffman_diff(&BS, &HUF->tree);
if (c[color] < 0)
{
c_fix = 0;
[...]
}
else
{
c_fix = c[color];
}
switch (ID->type_format)
{
case X3F_IMAGE_RAW_HUFFMAN_X530:
case X3F_IMAGE_RAW_HUFFMAN_10BIT:
[4] HUF->x3rgb16.data[3 * (row * ID->columns + col) + color] =
(uint16_t)c_fix;
break;
[...]
}
}
}
}
At [3], the code adds Huffman-decoded difference values to an int16_t accumulator c. The function huffman_decode() calls huffman_decode_row() once per row (rows iterations). Within each call, the outer loop iterates over columns and the inner loop iterates 3 times for each color channel, so [4] is executed rows * columns * 3 times using the original dimension values. The index at [4], 3 * (row * ID->columns + col) + color, quickly exceeds the under-allocated buffer size, causing a heap buffer overflow.
The value written to the buffer (c_fix) is derived from Huffman-decoded data read from the X3F file, giving the attacker control over the written values. The int16_t accumulator allows controlled overflow wrapping (e.g., to write 257 (0x101) after 514 (0x202), provide difference 65279). Written values are limited to the range 0-32767; values with the MSB set (32768-65535) cannot be written because negative accumulator values are set to 0.
When columns * rows * 3 exceeds UINT32_MAX, the result wraps to a much smaller value, causing a buffer allocation of hundreds of megabytes instead of the intended gigabytes. The loop then writes far beyond the allocated buffer. While the 32-bit index also eventually wraps, billions of out-of-bounds writes occur before that happens.
LibRaw’s memory limit check at src/decoders/unpack.cpp:
if (INT64(MAX(S.width, S.raw_width)) *
INT64(MAX(S.height, S.raw_height) + 8) *
INT64(sizeof(*imgdata.image)) // 8, accounts for max 4 channels × 2 bytes
+ INT64(libraw_internal_data.unpacker_data.meta_length)
> INT64(imgdata.rawparams.max_raw_memory_mb) * INT64(1024 * 1024))
throw LIBRAW_EXCEPTION_TOOBIG;
The integer overflow at [1] requires columns * rows > 1,431,655,765 (UINT32_MAX / 3, since [1] multiplies by 3). Since the memory check uses 8 bytes per pixel, this requires max_raw_memory_mb around 11 GB (1,431,655,765 × 8 around 11 GB). On 32-bit systems, [2] introduces an additional overflow: sizeof(uint16_t) * size is computed in 32-bit arithmetic, so the vulnerability triggers with smaller dimensions (columns * rows > 715,827,882, i.e. UINT32_MAX / 6). On 64-bit systems, sizeof(uint16_t) returns size_t (64-bit), promoting the multiplication to 64-bit, so no additional overflow occurs at [2].
Any application that calls unpack() on untrusted X3F files with max_raw_memory_mb greater than around 11GB is vulnerable to this heap buffer overflow, which can result in heap corruption and potential code execution. Note that X3F support requires LibRaw to be compiled with -DUSE_X3FTOOLS, which is not enabled by default.
=================================================================
==43504==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x00011fbe6200 at pc 0x0001005236d8 bp 0x00016f9766b0 sp 0x00016f9766a8
WRITE of size 2 at 0x00011fbe6200 thread T0
#0 0x0001005236d4 in huffman_decode_row(x3f_info_s*, x3f_directory_entry_s*, int, int, int, int*) x3f_utils_patched.cpp:1083
#1 0x000100521efc in x3f_load_huffman(x3f_info_s*, x3f_directory_entry_s*, int, int, int) x3f_utils_patched.cpp:1483
#2 0x00010051f43c in x3f_load_data(x3f_s*, x3f_directory_entry_s*) x3f_utils_patched.cpp:2076
#3 0x0001005159a4 in LibRaw::x3f_load_raw() x3f_parse_process.cpp:579
#4 0x00010050b34c in LibRaw::unpack() unpack.cpp:447
#5 0x0001003cd100 in main poc_x3f_overflow.cpp:43
#6 0x00018f671d50 (<unknown module>)
0x00011fbe6200 is located 0 bytes after 410065408-byte region [0x0001074d4800,0x00011fbe6200)
allocated by thread T0 here:
#0 0x000100ce1330 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d330)
#1 0x00010052146c in x3f_load_huffman(x3f_info_s*, x3f_directory_entry_s*, int, int, int) x3f_utils_patched.cpp
#2 0x00010051f43c in x3f_load_data(x3f_s*, x3f_directory_entry_s*) x3f_utils_patched.cpp:2076
#3 0x0001005159a4 in LibRaw::x3f_load_raw() x3f_parse_process.cpp:579
#4 0x00010050b34c in LibRaw::unpack() unpack.cpp:447
#5 0x0001003cd100 in main poc_x3f_overflow.cpp:43
#6 0x00018f671d50 (<unknown module>)
SUMMARY: AddressSanitizer: heap-buffer-overflow x3f_utils_patched.cpp:1083 in huffman_decode_row(x3f_info_s*, x3f_directory_entry_s*, int, int, int, int*)
Shadow bytes around the buggy address:
0x00011fbe5f80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00011fbe6000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00011fbe6080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00011fbe6100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x00011fbe6180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x00011fbe6200:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x00011fbe6280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x00011fbe6300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x00011fbe6380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x00011fbe6400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x00011fbe6480: 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
==43504==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.