CVE-2025-64893: DNG SDK OOB Read Exposes Process Memory
Adobe DNG SDK ≤1.7.0 contains an out-of-bounds read in IFD/tile parsing that leaks heap memory and can crash the host application when processing a malformed DNG file.
A security flaw has been discovered in Adobe's DNG SDK, a tool that handles DNG image files—a professional photo format used by photographers and creative software.
Here's what's happening: When a DNG image file is opened, the software reads data from the file into its memory. This vulnerability is like a guard checking IDs at a nightclub but occasionally looking at the wrong line—the software accidentally peeks at memory locations it shouldn't, either accidentally exposing data or crashing the program entirely.
The catch is that nothing happens automatically. Someone has to actively open a malicious image file for the problem to occur. Think of it like spam mail—dangerous only if you actually open and interact with it.
Who should worry? Photographers and designers who work with professional camera files, people using Adobe Lightroom or other photo editing software that relies on this code, and anyone who receives DNG files from untrusted sources. If you casually download images from the internet, this is probably not your main concern.
The real risk has two flavors: an attacker could theoretically steal sensitive information from your computer's memory—like passwords or personal data that happened to be there—or they could simply crash your application to disrupt your work. Neither is catastrophic, but both are annoying and potentially serious depending on what data was accessible.
Here's what you should do: Update your photo software when Adobe releases a patch—this is one of those vulnerabilities that disappears once software is updated. Be cautious about opening DNG files from strangers or untrusted websites, just like you would with any file download. If you're a professional photographer, keep an eye on Adobe's security advisories and prioritize updates for tools you use daily.
Want the full technical analysis? Click "Technical" above.
CVE-2025-64893 is an out-of-bounds read in Adobe DNG SDK versions 1.7.0 and earlier. The SDK ships as an embeddable C++ library consumed by Lightroom, Camera Raw, and dozens of third-party RAW processors. A crafted .dng file can force the parser to read past the end of a heap-allocated tile or strip buffer, leaking adjacent heap contents to any downstream operation that serialises the decoded pixel data — and crashing the process when the read crosses an unmapped page boundary.
CVSS 7.1 (HIGH) reflects the combination of attacker-controlled file content, reliable memory disclosure, and the requirement for a single user action: opening the malicious file.
Affected Component
The DNG SDK exposes a hierarchical IFD parser built around dng_ifd and a tile/strip reader in dng_image_writer / dng_image_reader. The relevant path for this vulnerability is the tile data reader inside dng_host-driven decode pipelines:
dng_stream::Get() — low-level byte reader with a caller-supplied length
dng_ifd::TileByteCount() — returns the on-disk byte count for a tile
The mismatch between the on-disk byte count (attacker-controlled via the TileByteCounts IFD tag) and the allocated buffer size (derived from uncompressed pixel dimensions) is where the read escapes its allocation.
Root Cause Analysis
Root cause:dng_ifd::ReadStage1Image() allocates a decode buffer sized by uncompressed tile dimensions but passes the raw on-disk TileByteCount — an attacker-controlled IFD field — as the read length to dng_stream::Get(), allowing reads beyond the allocation boundary.
The IFD tag TileByteCounts (tag 0x0145) stores the compressed size of each tile as written to disk. For uncompressed or losslessly-compressed DNG, the SDK compares this against the expected uncompressed size as a sanity check. The bug manifests when this check is either absent or bypassable via a specially-crafted compression type field that causes the SDK to trust the on-disk count verbatim:
// dng_ifd.cpp — ReadStage1Image (reconstructed pseudocode, SDK ≤1.7.0)
void dng_ifd::ReadStage1Image(dng_host &host,
dng_stream &stream,
dng_image &image,
uint32 tileIndex)
{
// Uncompressed buffer size derived from pixel dimensions — correct
uint32 tileWidth = TileWidth();
uint32 tileHeight = TileLength();
uint32 pixelSize = (fBitsPerSample[0] + 7) >> 3;
uint32 bufferSize = tileWidth * tileHeight * fSamplesPerPixel * pixelSize;
AutoPtr block(host.Allocate(bufferSize));
uint8 *dst = block->Buffer_uint8();
// On-disk byte count for this tile — comes directly from TileByteCounts IFD tag
uint64 byteCount = TileByteCount(tileIndex); // attacker-controlled
stream.SetReadPosition(TileOffset(tileIndex));
// BUG: byteCount is NOT clamped to bufferSize before the read.
// If byteCount > bufferSize, Get() reads past the end of 'block'.
stream.Get(dst, (uint32)byteCount); // <-- OOB READ HERE
}
The dng_memory_block allocator wraps operator new[]; the block sits on the process heap. When byteCount exceeds bufferSize, dng_stream::Get() memcpy-reads past the allocation into adjacent heap metadata or other live objects.
The codec path matters: when fCompression == ccUncompressed (tag value 1) or certain lossless JPEG variants, no decompressor interposes between the stream read and the destination buffer. This makes the raw-length path reachable without triggering the JPEG decompressor's own length checks.
// dng_ifd.cpp — TileByteCount() — pure IFD field accessor, no validation
uint64 dng_ifd::TileByteCount(uint32 index) const
{
if (fTileByteCountsType == ttLong)
return fTileByteCountsLong[index]; // raw IFD LONG array value
else
return fTileByteCountsShort[index]; // raw IFD SHORT array value
// BUG: no upper-bound check against expected uncompressed size
}
Memory Layout
The following diagram reflects a typical 64-bit host process heap state during a ReadStage1Image() call for a 64×64 px, 16-bit, 3-sample uncompressed tile. bufferSize = 64 × 64 × 3 × 2 = 24576 (0x6000) bytes. The malicious DNG sets TileByteCount[0] = 0x7800, an overread of 0x1800 bytes.
HEAP STATE — BEFORE dng_stream::Get() CALL:
┌──────────────────────────────────────────────────────────────┐
│ dng_memory_block header (operator new[]) │
│ fAllocatedSize = 0x6000 │
│ fBuffer = → [tile pixel data region] │
├──────────────────────────────────────────────────────────────┤
│ [+0x0000] tile pixel buffer (0x6000 bytes allocated) │
│ ← dst pointer starts here │
│ ← valid read region ends at +0x5FFF │
├──────────────────────────────────────────────────────────────┤
│ [+0x6000] NEXT HEAP OBJECT: dng_memory_block (metadata buf) │
│ fAllocatedSize = 0x200 │
│ fBuffer = → [EXIF / XMP staging buffer] │
│ ← byteCount=0x7800 read crosses into here │
└──────────────────────────────────────────────────────────────┘
HEAP STATE — AFTER dng_stream::Get() (byteCount = 0x7800):
┌──────────────────────────────────────────────────────────────┐
│ tile pixel buffer [0x0000 – 0x5FFF] ← legitimately written │
├──────────────────────────────────────────────────────────────┤
│ [+0x6000 – +0x77FF] OOB READ: 0x1800 bytes of adjacent │
│ heap contents copied into dst[0x6000..0x77FF] │
│ MAY CONTAIN: │
│ - heap allocator metadata (size, fwd/bck pointers) │
│ - dng_memory_block fields from EXIF/XMP buffer │
│ - stack-spilled pointers from prior decode calls │
│ - ASLR-defeating image base / heap base addresses │
└──────────────────────────────────────────────────────────────┘
CRASH SCENARIO (byteCount pushed to page boundary + 1):
stream.Get() → memcpy(dst, src, 0xF001)
src + 0xF000 = last mapped byte of heap arena
src + 0xF001 = unmapped → SIGSEGV / ACCESS_VIOLATION
rip/pc = inside memcpy (libc / ntdll)
Exploitation Mechanics
For an attacker targeting information disclosure rather than crash, the goal is to size the overread precisely so it lands in a live allocation containing useful pointers, then recover that data through any output channel the calling application exposes (thumbnail render, metadata display, error message, log file).
EXPLOIT CHAIN — memory disclosure via crafted DNG:
1. Craft a DNG with a single 64×64 uncompressed tile.
IFD fields to set:
TileWidth = 64 (0x0142 = 0x0040)
TileLength = 64 (0x0143 = 0x0040)
BitsPerSample = 16 → bufferSize = 0x6000
Compression = 1 (ccUncompressed)
TileByteCounts[0] = 0x7800 → overread of 0x1800 bytes
2. Pad the on-disk tile data to exactly 0x7800 bytes so
stream.Get() reads the full requested length without
hitting a stream EOF before the heap overread occurs.
3. Target application (e.g. Lightroom, a custom RAW tool)
calls ReadStage1Image() → dng_stream::Get() reads 0x7800
bytes into the 0x6000-byte block, pulling 0x1800 bytes
of adjacent heap into dst[].
4. The decoded "image" pixel data at rows 96–127 (the
overread region mapped onto pixel rows) now contains
raw heap bytes. Any path that serialises pixel data
back to the attacker — e.g. a JPEG thumbnail export,
a preview render returned via API, or an error-path
log — leaks those bytes.
5. From 0x1800 bytes of heap, recover:
- Heap base address (from allocator metadata)
- Image base / DSO base (from vtable pointers
in adjacent dng_memory_block or dng_image objects)
→ Sufficient to defeat ASLR for a follow-on write primitive.
6. For DoS: set TileByteCounts[0] such that
TileOffset[0] + byteCount crosses a page boundary.
stream.Get() → memcpy faults → process terminates.
Patch Analysis
The correct fix clamps byteCount to bufferSize before the Get() call, and separately validates that the IFD-declared byte count is not unreasonably larger than the expected uncompressed size. A secondary fix adds the missing validation inside TileByteCount().
// BEFORE (vulnerable — SDK ≤1.7.0):
uint64 byteCount = TileByteCount(tileIndex);
stream.SetReadPosition(TileOffset(tileIndex));
stream.Get(dst, (uint32)byteCount); // no bounds check on byteCount
// AFTER (patched):
uint64 byteCount = TileByteCount(tileIndex);
// Primary fix: reject tiles where on-disk size exceeds a
// reasonable multiple of the uncompressed buffer size.
// For uncompressed data they must be equal; for lossless
// compressed data a 2× slack is generous.
uint64 maxAllowed = (uint64)bufferSize * 2;
if (byteCount > maxAllowed) {
ThrowBadFormat(); // dng_exception — surfaces as decode error
}
stream.SetReadPosition(TileOffset(tileIndex));
// Secondary fix: read only up to bufferSize bytes regardless.
// Decompressors that need the full on-disk byte count receive
// byteCount separately; the destination buffer is never exceeded.
uint32 readLen = (uint32)min(byteCount, (uint64)bufferSize);
stream.Get(dst, readLen);
Static file indicators: Parse the DNG IFD chain and flag any tile where TileByteCounts[i] > TileWidth × TileLength × SamplesPerPixel × ceil(BitsPerSample/8) by more than the compression ratio could plausibly explain. For Compression == 1 (uncompressed), any deviation is malicious.
import struct, sys
def check_dng_tile_bytecounts(path):
"""Flag DNG files where TileByteCounts exceed uncompressed tile size."""
with open(path, "rb") as f:
data = f.read()
# Minimal IFD walk — assumes little-endian, no chain following
endian = "<" if data[:2] == b"II" else ">"
offset = struct.unpack_from(endian + "I", data, 4)[0]
nentries = struct.unpack_from(endian + "H", data, offset)[0]
ifd = {}
for i in range(nentries):
base = offset + 2 + i * 12
tag, typ, cnt, val = struct.unpack_from(endian + "HHII", data, base)
ifd[tag] = (typ, cnt, val)
tile_w = ifd.get(0x0142, (None, None, 0))[2]
tile_h = ifd.get(0x0143, (None, None, 0))[2]
bps = ifd.get(0x0102, (None, None, 8))[2]
spp = ifd.get(0x0115, (None, None, 1))[2]
compress = ifd.get(0x0103, (None, None, 1))[2]
uncompressed = tile_w * tile_h * spp * ((bps + 7) // 8)
if 0x0145 in ifd:
typ, cnt, val_off = ifd[0x0145]
fmt = endian + ("I" if typ == 4 else "H")
sz = struct.calcsize(fmt)
for i in range(cnt):
bc = struct.unpack_from(fmt, data, val_off + i * sz)[0]
ratio = bc / max(uncompressed, 1)
if compress == 1 and bc != uncompressed:
print(f"[!] SUSPICIOUS tile {i}: TileByteCount={bc:#x} "
f"!= uncompressed={uncompressed:#x} (Compression=1)")
elif ratio > 2.0:
print(f"[!] SUSPICIOUS tile {i}: TileByteCount={bc:#x} "
f"is {ratio:.1f}x uncompressed size")
if __name__ == "__main__":
check_dng_tile_bytecounts(sys.argv[1])
Runtime indicators: ASan will report a heap-buffer-overflow read with READ of size N inside dng_stream::Get → memcpy. On non-instrumented builds, expect SIGSEGV inside memcpy or libc's inlined variant with rsi/x1 pointing just past a heap arena boundary. Crash dumps will show the faulting PC inside a memory-copy routine called from dng_ifd::ReadStage1Image.
Remediation
Update immediately: Upgrade to DNG SDK 1.7.1 or later (or the patched version distributed with the relevant Adobe product update).
Integrators: If you vendor the DNG SDK source, apply the byteCount clamp and ThrowBadFormat() guard in dng_ifd::ReadStage1Image() and dng_ifd::TileByteCount() as described above.
Mitigations (if patching is delayed): Run DNG decode in a sandboxed process with no network/IPC egress to prevent leak exfiltration. On Linux, a seccomp-bpf filter that kills the process on write() after a failed decode is effective. On Windows, an ACG + CFG policy on the decode worker achieves similar containment.
Fuzzing: The IFD integer fields (TileByteCounts, TileOffsets, TileWidth, TileLength) are high-value mutation targets. libFuzzer with ASan on dng_validate (the SDK's bundled test tool) will reproduce this class of bug within minutes of campaign start.