Project

General

Profile

Actions

Bug #1237

closed

DcmRLECodecDecoder::decodeFrame() out of bounds access

Added by Michael Onken about 7 hours ago. Updated about 6 hours ago.

Status:
Closed
Priority:
Normal
Assignee:
Category:
-
Target version:
-
Start date:
2026-07-03
Due date:
% Done:

0%

Estimated time:
Module:
dcmdata
Operating System:
Compiler:

Description

Summary

DcmRLECodecDecoder::decodeFrame reads one byte before a heap allocation when the last RLE stripe decompresses to zero bytes. The read at line 740 (*(outputBuffer - 1)) uses outputBuffer pointing to the base of outputBuffer_, so the access lands one byte before the heap block.

Reachable via local (attacker supplies a crafted DICOM file with an RLE-encoded pixel-data segment whose last stripe compresses to zero decoded bytes -- byteOffset == fragmentLength after the last stripe's data).

Affected version

  • Target: dcmtk 3.7.0+DEV
  • Commit read: 7246c5a9ca64c2d4312774bf40d046e255c00a41
  • Bug is in library code (src/).

Crash

ASan: heap-buffer-overflow, read of size 1.

Crash site: dcmdata/libsrc/dcrleccd.cc:740 in DcmRLECodecDecoder::decodeFrame.

Root cause

After each stripe the loop at lines 734-738 advances outputBuffer by decoderSize bytes. When decoderSize is 0, outputBuffer still equals getOutputBuffer() (the raw base of the allocation). Line 740 then reads *(outputBuffer - 1) unconditionally to seed a fill loop, producing a 1-byte underread of the heap block.

dcmdata/libsrc/dcrleccd.cc:689

bc(c). if (lastStripeOfColor && (rledecoder.size() < bytesPerStripe))

The guard above logs a warning and continues -- it does not prevent reaching line 740.

dcmdata/libsrc/dcrleccd.cc:707

bc(c). outputBuffer = OFstatic_cast(Uint8 *, rledecoder.getOutputBuffer());

dcmdata/libsrc/dcrleccd.cc:733

bc(c). const size_t decoderSize = rledecoder.size();

dcmdata/libsrc/dcrleccd.cc:734

bc(c). for (pixel = 0; pixel < decoderSize; ++pixel)

dcmdata/libsrc/dcrleccd.cc:736

bc(c). *pixelPointer = *outputBuffer++;

dcmdata/libsrc/dcrleccd.cc:740

bc(c). const Uint8 lastPixelValue = *(outputBuffer - 1);

dcmdata/include/dcmtk/dcmdata/dcrledec.h:49

bc(c). outputBuffer_ = new unsigned char[outputBufferSize_];

decompress() returns EC_Normal for zero compressed bytes (dcrledec.h:74), leaving offset_==0 and fail_==0, so size()==0 and getOutputBuffer() returns the base pointer unchanged.

dcmdata/include/dcmtk/dcmdata/dcrledec.h:74

bc(c). if (compressedSize == 0) return EC_Normal;

dcmdata/include/dcmtk/dcmdata/dcrledec.h:179

bc(c). inline void *getOutputBuffer() const

dcmdata/include/dcmtk/dcmdata/dcrledec.h:181

bc(c). return outputBuffer_;

Reproduction

Call path:

bc. DcmFileFormat::loadFile() [dcmdata/libsrc/dcfilefo.cc]
DcmDataset::read() [dcmdata/libsrc/dcdatset.cc]
DcmPixelData::decode() [dcmdata/libsrc/dcpixel.cc]
DcmRLECodecDecoder::decodeFrame() [dcmdata/libsrc/dcrleccd.cc:740]

Reproduction status: yes (full-poc) - ASan heap-buffer-overflow READ at dcrleccd.cc:740; see poc/poc_dcmtk-rle-lastpixel-heap-underread/asan.txt.

Proof of Concept

Self-contained. docker build clones the target at the pinned commit and builds it under AddressSanitizer + UndefinedBehaviorSanitizer; docker run feeds the crafted input and reproduces the fault. Save the files below into a poc/ directory and:

bc. docker build -t poc . && docker run --rm poc

Sanitizer output

.dockerignore (crafted input)

build.log (crafted input)

docker_build.log (crafted input)

rle_frame.bin (64-byte binary input

build.sh

run.sh

Dockerfile

harness.cc

Impact

One byte before a heap allocation is read and used as a pixel fill value. The byte's content is undefined (heap metadata or an adjacent allocation), so information disclosure is heap-layout dependent. No write primitive exists at this site; RCE is not supported by current evidence. Impact is a 1-byte information disclosure plus possible DoS when ASan or hardened allocators trap the underread. Severity: medium.

Suggested fix

Guard the fill-remainder block against a zero-decoded-bytes case:

bc(c). const Uint8 lastPixelValue = (decoderSize > 0) ? *(outputBuffer - 1) : 0;

Not independently verified

The following could not be confirmed against source and are stated as hypotheses only:

  • A live PoC exists; reproduction_tier is full-poc (ASan heap-buffer-overflow READ at dcrleccd.cc:740). The heap-underread reads one byte before a heap allocation whose content is undefined; information disclosure is layout-dependent (the adjacent byte may belong to heap metadata or another allocation). No write primitive exists at this site, so RCE is not supported by current evidence.

<hr />

Generated by icsloop. All quoted code verified present in source at the stated commit (snippet gate: OPEN, 10 checks).

Actions #1

Updated by Michael Onken about 6 hours ago

Fixed in commit 2846f2914a6132d58c8e35d4337fccc0e52e3fe7.

Actions #2

Updated by Michael Onken about 6 hours ago

  • Status changed from New to Closed
Actions

Also available in: Atom PDF