CVE-2026-20911
A heap-based buffer overflow vulnerability exists in the HuffTable::initval functionality of LibRaw Commit 0b56545 and 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 0b56545
LibRaw Commit d20315b
LibRaw – https://github.com/LibRaw/LibRaw.git
9.8 – CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-131 – Incorrect Calculation of Buffer Size
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 a lossless JPEG decoder used for processing compressed RAW data from various camera formats. The Huffman table initialization routine in this decoder is vulnerable to a heap buffer overflow because the `bits` array, which describes the Huffman table structure, is not validated before being used to calculate the table size.
The function responsible for initializing Huffman decoding tables is `HuffTable::initval` in `src/decompressors/losslessjpeg.cpp`:
“`
void HuffTable::initval(uint32_t _bits[17], uint32_t _huffval[256], bool _dng_bug) { memmove(bits, _bits, sizeof(bits)); memmove(huffval, _huffval, sizeof(huffval)); dng_bug = _dng_bug; [1] nbits = 16; for(int i = 0; i < 16; i++) { if(bits[16 – i] != 0) break; nbits–; } [2] hufftable.resize( size_t(1ULL << nbits)); for (unsigned i = 0; i < hufftable.size(); i++) hufftable[i] = 0; int h = 0; int pos = 0; for (uint8_t len = 0; len < nbits; len++) { [3] for (uint32_t i = 0; i < bits[len + 1]; i++) { for (int j = 0; j < (1 << (nbits – len – 1)); j++) { [4] hufftable[h] = ((len+1) << 16) | (uint8_t(huffval[pos] & 0xff) << 8) | uint8_t(shiftval[pos] & 0xff); h++; } pos++; } } […] }
“`
The function’s `_bits` argument is taken directly from the image provided to the library, then copied into the object’s `bits` field. The `bits` array specifies how many Huffman codes exist for each bit length (1-16 bits). Note that `bits[0]` is unused; only `bits[1]` through `bits[16]` are meaningful. For example:
– `bits[1] = 0` means no 1-bit codes
– `bits[2] = 2` means two 2-bit codes
– `bits[5] = 10` means ten 5-bit codes
At `[1]`, the code determines `nbits` by finding the maximum bit length with at least one code. At `[2]`, the Huffman lookup table is allocated with size `2^nbits`. The nested loops at `[3]` iterate through each bit length, and for each code of length `len+1`, write `2^(nbits-len-1)` entries to the table at `[4]`.
The total number of entries written is:
`
∑ bits[len+1] × 2^(nbits – len – 1) for len = 0 to nbits-1
`
It seems that the developer assumed that the `bits` array would always describe a valid Huffman table. However, because the `bits` array is not validated, an attacker can provide values that require more table entries than the allocation at `[2]` provides. For example, consider:
– `bits[5] = 100` (100 codes of length 5)
– `bits[6] = 1` (1 code of length 6)
– All other `bits[i] = 0`
This gives `nbits = 6`, so the table is allocated with `2^6 = 64` entries. But the total entries written is:
`
100 × 2^(6-5) + 1 × 2^(6-6) = 100 × 2 + 1 × 1 = 201 entries
`
The allocation at `[2]` creates a buffer for **64 entries**, but the loop at `[4]` will attempt to write **201 entries**, causing a heap-based buffer overflow.
The sizing logic at `[2]` is likely connected to the **Kraft inequality**—a mathematical property required for valid prefix-free codes.
The Kraft inequality states that for a valid prefix-free code with codeword lengths L₁, L₂, …, Lₙ:
`
∑ 2^(-Lᵢ) ≤ 1
`
In this code, `bits[i]` specifies _how many_ codes have length i. If there are `bits[i]` codewords of length i, each contributes `2^(-i)` to the sum, so grouping by length:
`
∑ bits[i] × 2^(-i) ≤ 1 for i = 1 to 16
`
In our case, we have code lengths from 1 to 16.
Recall that `nbits` is the maximum code length used (highest index where `bits[i] != 0`). For all `i > nbits`, `bits[i] = 0`, so those terms vanish and we can truncate to `i = 1 to nbits`:
`
∑ bits[i] × 2^(-i) ≤ 1 for i = 1 to nbits
`
We “shift” the indexing {1,…,nbits} to {0,…,nbits-1} by assigning `len = i – 1`:
`
∑ bits[len + 1] × 2^(-len – 1) ≤ 1 for len = 0 to nbits-1
`
Multiplying both sides by `2^nbits`, the inequality becomes:
`
∑ bits[len + 1] × 2^(nbits – len – 1) ≤ 2^nbits for len = 0 to nbits-1
`
This is exactly the condition required for the total entries written to not exceed the allocated `hufftable` size. However, **this assumption is never validated against the untrusted input**, and a malicious file can trivially violate it.
“`
================================================================= ==91815==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x606000000360 at pc 0x00010099c5d8 bp 0x00016f479590 sp 0x00016f479588 WRITE of size 4 at 0x606000000360 thread T0 #0 0x00010099c5d4 in HuffTable::initval(unsigned int*, unsigned int*, bool) losslessjpeg.cpp:374 #1 0x000100999a94 in LibRaw_LjpegDecompressor::initialize(bool, bool) losslessjpeg.cpp:125 #2 0x000100999f50 in LibRaw_LjpegDecompressor::LibRaw_LjpegDecompressor(unsigned char*, unsigned int) losslessjpeg.cpp:48 #3 0x0001009f17a4 in LibRaw::sony_ycbcr_load_raw() sonycc.cpp:276 #4 0x000100a06b64 in LibRaw::unpack() unpack.cpp:447 #5 0x0001008c8e4c in main poc_huffman.cpp:32 #6 0x00018f671d50 () 0x606000000360 is located 0 bytes after 64-byte region [0x606000000320,0x606000000360) allocated by thread T0 here: #0 0x000101223428 in _Znwm+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x4b428) #1 0x00010099ffcc in std::__1::vector>::__append(unsigned long) vector.h:943 #2 0x00010099c064 in HuffTable::initval(unsigned int*, unsigned int*, bool) losslessjpeg.cpp:363 #3 0x000100999a94 in LibRaw_LjpegDecompressor::initialize(bool, bool) losslessjpeg.cpp:125 #4 0x000100999f50 in LibRaw_LjpegDecompressor::LibRaw_LjpegDecompressor(unsigned char*, unsigned int) losslessjpeg.cpp:48 #5 0x0001009f17a4 in LibRaw::sony_ycbcr_load_raw() sonycc.cpp:276 #6 0x000100a06b64 in LibRaw::unpack() unpack.cpp:447 #7 0x0001008c8e4c in main poc_huffman.cpp:32 #8 0x00018f671d50 () SUMMARY: AddressSanitizer: heap-buffer-overflow losslessjpeg.cpp:374 in HuffTable::initval(unsigned int*, unsigned int*, bool) Shadow bytes around the buggy address: 0x606000000080: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00 0x606000000100: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 02 fa 0x606000000180: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa 0x606000000200: 00 00 00 00 00 00 04 fa fa fa fa fa 00 00 00 00 0x606000000280: 00 00 06 fa fa fa fa fa fd fd fd fd fd fd fd fd =>0x606000000300: fa fa fa fa 00 00 00 00 00 00 00 00[fa]fa fa fa 0x606000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x606000000400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x606000000480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x606000000500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa 0x606000000580: 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 ==91815==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.
