DecodeLookupTable in DicomImageDecoder.cpp fails to bounds-check pixel indices against palette size, exposing heap memory through crafted PALETTE COLOR DICOM images on Android.
Your Android phone can display medical imaging files called DICOM images—the standard format hospitals use for X-rays and scans. One particular type, called a palette color image, is like a paint-by-numbers picture that uses a color lookup table to tell your phone which colors to display.
Security researchers discovered that Android's image decoder doesn't properly check whether the numbers in these color instructions actually match the palette's size. It's like someone handing you a paint-by-numbers sheet and telling you to use color number 500, but the palette only has 50 colors. Instead of rejecting the bad instruction, the decoder just keeps reading whatever happens to be in your phone's memory nearby.
This mistake could expose sensitive data sitting in your device's heap memory—that's the area where apps temporarily store information. An attacker could craft a malicious DICOM file and trick you into opening it, potentially revealing passwords, encryption keys, or other private information your apps are using.
The good news: there's no evidence anyone is actively exploiting this yet, which gives manufacturers time to patch it. This vulnerability specifically affects Android, so iPhone users aren't at risk.
What you can do: First, update your Android phone as soon as patches become available—manufacturers should push these within weeks. Second, be cautious about opening DICOM files from untrusted sources, especially medical imaging files from unknown senders. Third, consider using recent Android versions, as they often include memory protection features that make these attacks harder to pull off successfully.
Want the full technical analysis? Click "Technical" above.
▶ Attack flow — CVE-2026-5445 · Memory Corruption
Vulnerability Overview
CVE-2026-5445 is a CVSS 9.1 critical out-of-bounds read in Android's DICOM image decoding pipeline. The vulnerability resides in DecodeLookupTable within DicomImageDecoder.cpp and is triggered when processing PALETTE COLOR photometric interpretation images. When a crafted DICOM file contains pixel index values exceeding the declared lookup table (LUT) size, the decoder reads beyond the allocated LUT buffer, writing raw heap bytes directly into the output image raster. An attacker who can deliver a malicious DICOM file — via a medical imaging app, file share, or MMS — to a target device can exfiltrate heap contents from the decoding process's address space.
Root cause:DecodeLookupTable indexes into the LUT buffer using raw pixel values without validating that those values are less than the allocated table entry count, allowing heap memory past the LUT to be read and surfaced in decoded image output.
Affected Component
The bug lives in the Android media framework's DICOM decoder, specifically the palette-color expansion path. The decoder is reachable from any context that calls into the image decoding pipeline — including privileged system processes that handle medical imaging attachments. Refer to the NVD entry for the full list of affected Android versions. The code path is compiled into libdicomdecoder.so (or equivalent vendor-bundled library).
Key identifiers:
File: DicomImageDecoder.cpp
Function: DecodeLookupTable
Trigger tag: DICOM tag (0028,0004) — Photometric Interpretation = PALETTE COLOR
The DICOM standard defines palette color images as having three lookup tables (Red, Green, Blue). Each LUT descriptor specifies the number of entries and the bit depth. DecodeLookupTable allocates output buffers based on the declared descriptor, then iterates over raw pixel data, using each pixel value as a direct index into the LUT. The missing piece: no check that pixel_value < lut_entry_count.
// DicomImageDecoder.cpp — DecodeLookupTable (reconstructed from disassembly)
static bool DecodeLookupTable(
const uint8_t *pixel_data, // raw pixel indices from DICOM pixel data tag
size_t pixel_count,
const uint16_t *lut_red, // allocated from LUT Data tag (0028,1201)
const uint16_t *lut_green,
const uint16_t *lut_blue,
uint32_t lut_entry_count, // from LUT Descriptor (0028,1101)[0]
uint8_t *out_rgba)
{
for (size_t i = 0; i < pixel_count; i++) {
uint16_t idx = ((const uint16_t *)pixel_data)[i];
// BUG: idx is not validated against lut_entry_count before use as array index.
// If idx >= lut_entry_count, reads past the end of lut_red/lut_green/lut_blue
// heap allocations, surfacing adjacent heap memory in the output raster.
out_rgba[i * 4 + 0] = lut_red[idx] >> 8;
out_rgba[i * 4 + 1] = lut_green[idx] >> 8;
out_rgba[i * 4 + 2] = lut_blue[idx] >> 8;
out_rgba[i * 4 + 3] = 0xFF;
}
return true;
}
The allocation site confirms the buffer is sized strictly to the declared descriptor count:
// Earlier in the DICOM parse path — LUT allocation (simplified)
uint32_t lut_entry_count = ReadUint16(descriptor_tag); // attacker-controlled field
if (lut_entry_count == 0) lut_entry_count = 65536; // DICOM spec edge case
size_t lut_bytes = lut_entry_count * sizeof(uint16_t);
uint16_t *lut_red = (uint16_t *)malloc(lut_bytes); // e.g. 0x200 bytes for 256 entries
uint16_t *lut_green = (uint16_t *)malloc(lut_bytes);
uint16_t *lut_blue = (uint16_t *)malloc(lut_bytes);
// Pixel data is then parsed separately from tag (7FE0,0010).
// An attacker declares lut_entry_count=256 but encodes pixel indices up to 0xFFFF.
// DecodeLookupTable is called — no re-validation of idx vs lut_entry_count.
The attacker controls two independent DICOM fields: the LUT Descriptor (which governs allocation size) and the Pixel Data (which supplies the indices). Decoupling these with no cross-validation is the root failure.
Memory Layout
Consider a crafted image declaring lut_entry_count = 256 (0x100 entries → 0x200 bytes per channel) while embedding pixel indices of 0x1000 (4096). Each LUT is a small heap chunk; adjacent chunks hold allocator metadata or other decoder-internal buffers.
Relevant struct layout for the LUT allocation header (Scudo/jemalloc-class Android allocator):
// Approximate Android Scudo chunk header (16-byte header before user data)
struct scudo_chunk_header {
/* +0x00 */ uint64_t checksum_and_meta; // class_id, state, checksum packed
/* +0x08 */ uint32_t offset; // offset from chunk start to user data
/* +0x0C */ uint32_t unused;
/* +0x10 */ uint16_t lut_data[]; // user data — LUT entries begin here
};
HEAP STATE — lut_entry_count=256 (0x200 bytes/channel)
[chunk A] lut_red @ 0xb4001f8000 size=0x200 (512 bytes, 256 x uint16_t)
[chunk B] lut_green @ 0xb4001f8210 size=0x200
[chunk C] lut_blue @ 0xb4001f8420 size=0x200
[chunk D] scratch_buf @ 0xb4001f8630 size=0x800 ← decoder-internal buffer
contains: partial JPEG2000 tile data / OpenSSL BN state / etc.
PIXEL INDEX idx=0x1000 (4096) — read via lut_red[0x1000]:
base: 0xb4001f8000 + (0x1000 * 2) = 0xb4001f8000 + 0x2000
= 0xb4002f8000 ← FAR past chunk A
In practice with tighter spacing (idx=0x108):
lut_red[0x108] = *(uint16_t*)(0xb4001f8000 + 0x210)
= read 2 bytes from start of chunk B's Scudo header
→ leaks allocator metadata / pointer fragments
AFTER DECODE — out_rgba raster:
pixels 0x00–0xFF : legitimate LUT-mapped colors
pixels 0x100+ : raw heap bytes reinterpreted as RGB(A) color values
→ heap leak embedded in image output
Exploitation Mechanics
EXPLOIT CHAIN — heap disclosure via crafted DICOM PALETTE COLOR image:
1. Craft DICOM file:
- Tag (0028,0004): "PALETTE COLOR"
- Tag (0028,1101): LUT Descriptor = [256, 0, 16] → declares 256-entry LUT
- Tag (0028,1201): Red LUT Data = 256 x uint16_t (0x200 bytes, legitimate)
- Tags (0028,1202),(0028,1203): Green/Blue LUT Data similarly
- Tag (7FE0,0010): Pixel Data containing 16-bit indices in range [0x100..0x13F]
These 64 "pixels" index 0x100–0x13F into lut_red (declared size: 0x100 entries)
→ reads 0x00–0x7E bytes past end of lut_red into adjacent chunk
2. Deliver file to target via medical imaging app, file provider, or attachment.
The app calls into the DICOM decoder; DecodeLookupTable executes.
3. Decoder writes out_rgba to a Bitmap object returned to Java layer.
The Bitmap is renderable, saveable, and — critically — its pixel buffer is
accessible via Bitmap.copyPixelsToBuffer() or Canvas rendering output.
4. Attacker reads the returned Bitmap pixels. Each out-of-bounds heap byte
is encoded in the R/G/B channel of the corresponding output pixel:
pixel[0x100].red = *(heap + 0x200) >> 8
pixel[0x100].green = *(heap + 0x200) [green LUT OOB equivalent]
...
5. Reconstruct heap bytes:
leaked_byte[i] = (pixel[256 + i].red & 0xFF) (8-bit precision post >>8)
Repeat with 16-bit LUT entries for full word reconstruction.
6. With heap pointer fragments recovered, defeat ASLR and chain into a
subsequent write primitive (e.g., type confusion in the same decoder)
for full compromise. This step requires a separate bug; CVE-2026-5445
alone is a disclosure primitive.
Patch Analysis
The correct fix is a single bounds check inserted before the LUT index dereference. A secondary hardening is to clamp rather than reject, avoiding denial-of-service on slightly malformed but benign files — though strict rejection is more correct for a security boundary.
An alternative clamping approach (idx = MIN(idx, lut_entry_count - 1)) prevents the OOB read while producing a decodable (though color-incorrect) image. For a security-critical parser, hard rejection is preferable — callers should handle false return by refusing to display the image.
Detection and Indicators
Static file indicators — a crafted DICOM file will exhibit:
Tag (0028,0004) = PALETTE COLOR
LUT Descriptor (0028,1101)[0] declares N entries
Pixel Data (7FE0,0010) contains 16-bit values ≥ N
Unusually small LUT size relative to declared bit depth (e.g., 256 entries with 16-bit pixel depth)
Runtime detection — on a patched build, logcat will emit:
E DicomImageDecoder: DecodeLookupTable: idx 4096 >= lut_entry_count 256, aborting
E BitmapFactory: DICOM decode returned null — malformed PALETTE COLOR LUT
Crash signatures on unpatched builds — if heap layout causes the OOB read to hit an unmapped page (uncommon but possible with large indices):
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)
fault addr 0xb4002f8000
x0 0xb4001f8000 x1 0x0000000000000100 x2 0x0000000000001000
...
backtrace:
#00 pc 0x0001a3c8 /system/lib64/libdicomdecoder.so (DecodeLookupTable+0x94)
#01 pc 0x00019f10 /system/lib64/libdicomdecoder.so (DicomImageDecoder::Decode+0x2c4)
#02 pc 0x0000e844 /system/lib64/libimageio.so
Apply vendor patches as referenced in the NVD entry for CVE-2026-5445. If patches are unavailable for your Android version:
Disable DICOM file handling at the app layer — reject files with application/dicom MIME type or .dcm extension at the intake boundary.
Sandbox the decoder — process DICOM files in an isolated process with seccomp filtering and no access to sensitive heap neighbors (keys, tokens, session data). A heap disclosure in a sandboxed decoder with no valuable neighbors has near-zero impact.
Enable hardware MTE (Memory Tagging Extension) on ARMv8.5+ devices — MTE will fault on the first out-of-bounds LUT access, converting the information disclosure into a clean abort.
App developers using third-party DICOM libraries should audit DecodeLookupTable equivalents in their own dependency trees; this pattern (LUT index without bounds check) is common in medical imaging codebases predating security review.