Talos Vulnerability Report

TALOS-2026-2331

LibRaw lossless_jpeg_load_raw heap-based buffer overflow vulnerability

April 7, 2026
CVE Number

CVE-2026-21413

SUMMARY

A heap-based buffer overflow vulnerability exists in the lossless_jpeg_load_raw 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-129 - Improper Validation of Array Index

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 decoder supports a “slicing” scheme to organize image data, controlled by metadata in the CR2Slice tag (0xC640), that is mainly used by Canon CR2 file. The function responsible for decoding this data is vulnerable to a heap buffer overflow because the col (column) index, calculated from user-controlled CR2Slice values, is never validated against the allocated buffer width before being used as an array index.

The vulnerable function LibRaw::lossless_jpeg_load_raw() in src/decoders/decoders_dcraw.cpp is used as a decoder for JPEG-compressed RAW files. It handles compression types 6, 7, and 99 for various manufacturers:

void LibRaw::lossless_jpeg_load_raw()
{
    int jwide, jhigh, jrow, jcol, val, jidx, i, j, row = 0, col = 0;
    struct jhead jh;
    ushort *rp;

    if (!ljpeg_start(&jh, 0))
        return;

    [...]

[1] jwide = jh.wide * jh.clrs;

    [...]

    try
    {
        for (jrow = 0; jrow < jh.high; jrow++)
        {
            [...]
            rp = ljpeg_row(jrow, &jh);
            [...]
[2]         for (jcol = 0; jcol < jwide; jcol++)
            {
                val = curve[*rp++];
[3]             if (cr2_slice[0])
                {
                    jidx = jrow * jwide + jcol;
[4]                 i = jidx / (cr2_slice[1] * raw_height);
                    if ((j = i >= cr2_slice[0]))
                        i = cr2_slice[0];
                    if(!cr2_slice[1+j])
                        throw LIBRAW_EXCEPTION_IO_CORRUPT;

                    jidx -= i * (cr2_slice[1] * raw_height);
                    row = jidx / cr2_slice[1 + j];
[5]                 col = jidx % cr2_slice[1 + j] + i * cr2_slice[1];
                }
                [...]
[6]             if (row > raw_height)
                    throw LIBRAW_EXCEPTION_IO_CORRUPT;
[7]             if ((unsigned)row < raw_height)
                    RAW(row, col) = val;
                [...]
            }
        }
    }
    [...]
}

The cr2_slice array is read directly from the CR2Slice TIFF tag (0xC640) in the image file: - cr2_slice[0]: Number of “full-width” slices - cr2_slice[1]: Width of each full slice - cr2_slice[2]: Width of the final partial slice

At [1], jwide is calculated as the JPEG width multiplied by the number of color components. The nested loops at [2] iterate through each decoded pixel, with jcol ranging from 0 to jwide-1. At [3], when cr2_slice[0] is non-zero, the code enters the CR2Slice calculation. The variable jidx (calculated as jrow * jwide + jcol) represents the linear position in the JPEG data stream, which is then used to compute the actual row and col positions in the output buffer.

The critical calculation occurs at [4] and [5]: - At [4], i represents the current “slice group” index, calculated as jidx / (cr2_slice[1] * raw_height) - At [5], col is calculated as: jidx % cr2_slice[1 + j] + i * cr2_slice[1]

The RAW(row, col) macro at [7] expands to:

imgdata.rawdata.raw_image[row * raw_width + col]

Note that while row is validated at [6], there is no corresponding validation for col. The code assumes that col will always be less than raw_width, but this assumption is never enforced.

The buffer raw_image is allocated in src/decoders/unpack.cpp (lines 391-397):

imgdata.rawdata.raw_alloc =
    calloc(size_t(rwidth) * (size_t(rheight) + 8), sizeof(imgdata.rawdata.raw_image[0]));

This allocates raw_width × (raw_height + 8) elements. For a valid access, the index row × raw_width + col must be less than this allocation size. This requires:

row × raw_width + col < raw_width × (raw_height + 8)

Since row < raw_height is enforced, the remaining constraint is approximately col < raw_width for typical images.

However, examining the calculation at [5]:

col = jidx % cr2_slice[1 + j] + i × cr2_slice[1]

When i ≥ 1, the term i × cr2_slice[1] is added to col. If an attacker provides a large cr2_slice[1] value, col can grow, within the bound of the size of the variable, arbitrarily large.

For example with cr2_slice[1] = 512 and i = 3: col = X + 3 × 512 = X + 1536; if raw_width = 32, the buffer expects col < 32, but col ≥ 1536.

The value written (val) at [7] is derived from a lookup table:

rp = ljpeg_row(jrow, &jh);
[...]
val = curve[*rp++];

The rp pointer comes from ljpeg_row(), which decodes one row of lossless JPEG data from the embedded bitstream. The decoded pixel values (*rp) are used as indices into the curve array. The curve array is a 65536-entry tone curve table that can be loaded directly from the file via the TIFF LinearizationTable tag (0xc618). The linear_table() function in src/utils/curves.cpp reads this table using read_shorts(curve, len).

Since both the curve table (via LinearizationTable) and the JPEG bitstream (which produces *rp indices via ljpeg_row()) are attacker-controlled, the attacker has full control over the 16-bit value written to out-of-bounds locations.

Any application that calls unpack() on untrusted CR2 files is vulnerable to this heap buffer overflow at [7], which can result in heap corruption and potential code execution.

Crash Information

=================================================================
==56971==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x620000000e80 at pc 0x000102b69620 bp 0x00016d23aa10 sp 0x00016d23aa08
WRITE of size 2 at 0x620000000e80 thread T0
    #0 0x000102b6961c in LibRaw::lossless_jpeg_load_raw() decoders_dcraw.cpp:592
    #1 0x000102c46b64 in LibRaw::unpack() unpack.cpp:447
    #2 0x000102b08e4c in main poc_cr2_oob.cpp:38
    #3 0x00018f671d50  (<unknown module>)

0x620000000e80 is located 0 bytes after 3584-byte region [0x620000000080,0x620000000e80)
allocated by thread T0 here:
    #0 0x00010353d330 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d330)
    #1 0x000102c4b4ac in LibRaw::malloc(unsigned long) utils_libraw.cpp:260
    #2 0x000102c46834 in LibRaw::unpack() unpack.cpp:395
    #3 0x000102b08e4c in main poc_cr2_oob.cpp:38
    #4 0x00018f671d50  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow decoders_dcraw.cpp:592 in LibRaw::lossless_jpeg_load_raw()
Shadow bytes around the buggy address:
  0x620000000c00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x620000000c80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x620000000d00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x620000000d80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x620000000e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x620000000e80:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x620000000f00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x620000000f80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x620000001000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x620000001080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x620000001100: 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
==56971==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.