Talos Vulnerability Report

TALOS-2026-2330

LibRaw HuffTable::initval heap-based buffer overflow vulnerability

April 7, 2026
CVE Number

CVE-2026-20911

SUMMARY

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.

CONFIRMED VULNERABLE VERSIONS

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

PRODUCT URLS

LibRaw - https://github.com/LibRaw/LibRaw.git

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-131 - Incorrect Calculation of Buffer Size

DETAILS

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.

Crash Information

=================================================================
==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  (<unknown module>)

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<unsigned int, std::__1::allocator<unsigned int>>::__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  (<unknown module>)

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
TIMELINE

2026-02-12 - Initial Vendor Contact
2026-02-12 - Vendor Disclosure
2026-04-06 - Vendor Patch Release
2026-04-07 - Public Release

Credit

Discovered by Francesco Benvenuto of Cisco Talos.