Bug #1191 » IC-DCMTK-0003_REPORT.md
IC-DCMTK-0003: Stack Overflow via Deeply Nested DICOM Sequences
Version: DCMTK master 418274445 (DCMTK-3.7.0+64)
CWE: CWE-674 (Uncontrolled Recursion)
Description
The DCMTK binary DICOM parser processes nested sequences through unbounded mutual recursion between DcmSequenceOfItems::read() and DcmItem::read(). A crafted DICOM file containing deeply nested sequences causes stack exhaustion, crashing any DCMTK-based application that parses it. The PoC is only 39,879 bytes (~20 bytes per nesting level) and reliably triggers a stack overflow under AddressSanitizer at approximately 1,060 nesting levels.
The recursion cycle spans four functions with no depth limit anywhere in the chain:
DcmSequenceOfItems::read() dcsequen.cc:758
-> readSubItem() dcsequen.cc:654
-> DcmItem::read() dcitem.cc:1385
-> readSubElement() dcitem.cc:1306/1483
-> subElem->read() [if VR is SQ, dispatches back to DcmSequenceOfItems::read()]
Each recursion level consumes approximately 7-8 KB of stack with ASan instrumentation (1.5-2 KB without). Since readSubElement() creates a DcmSequenceOfItems for any SQ-typed element and immediately calls its read() method, the recursion depth is determined entirely by the nesting depth of sequences in the input file. Real-world DICOM datasets rarely exceed 5-10 levels of nesting.
Reproduction
The PoC contains 1,100 levels of nested (0040,A730) Content Sequence. Each recursion level consumes ~7-8 KB of stack under ASan, so the crash threshold depends on the stack size limit. With the default 8 MB stack (ulimit -s 8192), the overflow triggers at approximately 1,060 levels. Larger stack limits require proportionally deeper nesting.
ulimit -s 8192 # default 8 MB stack; ensures crash with 1,100-level PoC
./bin/dcmdump poc.dcm
Actual output:
W: DcmItem: Length of element (0002,0012) is odd
AddressSanitizer:DEADLYSIGNAL
=================================================================
==2190515==ERROR: AddressSanitizer: stack-overflow on address 0x7fffffefe868 (pc 0x5555556c5536 bp 0x7fffffeff0b0 sp 0x7fffffefe870 T0)
#0 0x5555556c5536 in __asan_memcpy (build-asan/bin/dcmdump+0x171536)
#1 0x555555715be5 in OFCondition::OFCondition(OFConditionConst const&) ofstd/include/dcmtk/ofstd/ofcond.h:208:5
#2 0x55555574ccd9 in DcmItem::readTagAndLength(DcmInputStream&, E_TransferSyntax, DcmTag&, unsigned int&, unsigned int&) dcmdata/libsrc/dcitem.cc:1020:27
#3 0x5555557599d6 in DcmItem::readUntilTag(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int, DcmTagKey const&) dcmdata/libsrc/dcitem.cc:1429:29
#4 0x555555758cc7 in DcmItem::read(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcitem.cc:1385:21
#5 0x5555557c23b0 in DcmSequenceOfItems::readSubItem(DcmInputStream&, DcmTag const&, unsigned int, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcsequen.cc:654:30
#6 0x5555557c4d04 in DcmSequenceOfItems::read(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcsequen.cc:758:33
#7 0x555555751a1d in DcmItem::readSubElement(DcmInputStream&, DcmTag&, unsigned int, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcitem.cc:1306:28
#8 0x55555575aa42 in DcmItem::readUntilTag(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int, DcmTagKey const&) dcmdata/libsrc/dcitem.cc:1483:37
#9 0x555555758cc7 in DcmItem::read(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcitem.cc:1385:21
#10 0x5555557c23b0 in DcmSequenceOfItems::readSubItem(DcmInputStream&, DcmTag const&, unsigned int, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcsequen.cc:654:30
#11 0x5555557c4d04 in DcmSequenceOfItems::read(DcmInputStream&, E_TransferSyntax, E_GrpLenEncoding, unsigned int) dcmdata/libsrc/dcsequen.cc:758:33
... (alternating DcmItem::read/DcmSequenceOfItems::read pattern continues for 246 frames) ...
SUMMARY: AddressSanitizer: stack-overflow (build-asan/bin/dcmdump+0x171536) in __asan_memcpy
==2190515==ABORTING
Without ASan, the process receives SIGSEGV (threshold is higher: ~3,000-5,000 levels).
Fix
Add a recursion depth counter through the DcmSequenceOfItems::read() / DcmItem::read() chain, rejecting input that exceeds a reasonable nesting limit:
// In DcmSequenceOfItems::read() -- add a nestingDepth parameter through the chain
OFCondition DcmSequenceOfItems::read(DcmInputStream &inStream,
const E_TransferSyntax xfer, const E_GrpLenEncoding glenc,
const Uint32 maxReadLength, const Uint32 nestingDepth /* = 0 */)
{
static const Uint32 MAX_NESTING_DEPTH = 64;
if (nestingDepth > MAX_NESTING_DEPTH) {
DCMDATA_ERROR("Maximum sequence nesting depth exceeded ("
<< nestingDepth << " > " << MAX_NESTING_DEPTH << ")");
return EC_InvalidStream;
}
// ... existing code, passing nestingDepth + 1 to readSubItem() ...
}
- « Previous
- 1
- 2
- 3
- Next »