Add sequence nesting depth limit for parsing
authorMichael Onken <onken@open-connections.de>
Wed, 3 Jun 2026 19:54:21 +0000 (21:54 +0200)
committerÉtienne Mollier <emollier@debian.org>
Wed, 3 Jun 2026 19:54:21 +0000 (21:54 +0200)
Applied-Upstream: 885ff0f10372bd589b5f44cea974f28a3964cb0f
Last-Update: 2026-04-11
Bug: https://support.dcmtk.org/redmine/issues/1191
Bug-Debian: https://bugs.debian.org/1138713
Reviewed-By: Étienne Mollier <emollier@debian.org>
Track sequence nesting depth on DcmInputStream during parsing. When the
depth exceeds a configurable limit, parsing is aborted with
EC_NestingDepthLimitExceeded. This prevents stack overflow from
excessively nested DICOM sequences.

The default limit is controlled by the compile-time macro
DCMTK_MAX_SEQUENCE_NESTING (default: 64). It can be overridden per parse
operation via the runtime API:

- DcmInputStream::setMaxNestingDepth() for direct stream access
- DcmItem::setMaxNestingDepth() (inherited by DcmDataset) for loadFile()
  and read()
- DcmFileFormat::setMaxNestingDepth() for loadFile() and read(),
  forwarded to the contained dataset
- DcmSCP::setMaxNestingDepth() and DcmSCU::setMaxNestingDepth() for
  datasets received over the network via receiveDIMSEDataset()

The runtime setter uses Sint32 semantics:
  0 = compile-time default, -1 = unlimited, >0 = limit

Note: the specific EC_NestingDepthLimitExceeded error code is not yet
surfaced through the DIMSE layer; DIMSE_receiveDataSetInMemory() maps
all parse errors to DIMSE_RECEIVEFAILED.

Thanks to the IN-CYPHER OSS Security Team for the report, detailed
analysis and proof of concept.

This closes DCMTK Bug #1191.

Gbp-Pq: Name CVE-2026-10528-partial.patch

24 files changed:
config/docs/macros.txt
dcmdata/include/dcmtk/dcmdata/dcerror.h
dcmdata/include/dcmtk/dcmdata/dcfilefo.h
dcmdata/include/dcmtk/dcmdata/dcistrma.h
dcmdata/include/dcmtk/dcmdata/dcitem.h
dcmdata/include/dcmtk/dcmdata/dcobject.h
dcmdata/libsrc/dcdatset.cc
dcmdata/libsrc/dcerror.cc
dcmdata/libsrc/dcfilefo.cc
dcmdata/libsrc/dcistrma.cc
dcmdata/libsrc/dcitem.cc
dcmdata/libsrc/dcsequen.cc
dcmdata/tests/CMakeLists.txt
dcmdata/tests/Makefile.dep
dcmdata/tests/Makefile.in
dcmdata/tests/tests.cc
dcmdata/tests/tnesting.cc [new file with mode: 0644]
dcmnet/include/dcmtk/dcmnet/scp.h
dcmnet/include/dcmtk/dcmnet/scu.h
dcmnet/libsrc/dimse.cc
dcmnet/libsrc/scp.cc
dcmnet/libsrc/scu.cc
dcmnet/tests/tests.cc
dcmnet/tests/tscuscp.cc

index cd9ff2f037e4621b5851201b20f03b1c01ed8505..b44cd16607f91713361492104a4496cb03be348d 100644 (file)
@@ -109,6 +109,21 @@ DCMTK_LOG4CPLUS_AVOID_WIN32_FLS
     dcmtk::log4cplus::threadCleanup() should be called by the user code in
     order to clean-up oflog's thread local storage.
 
+DCMTK_MAX_SEQUENCE_NESTING
+  Affected: dcmdata
+  Type of modification: Compile-time tunable
+  Explanation: Defines the default maximum permitted sequence nesting depth
+    during DICOM parsing.  Deeply nested sequences can cause a stack overflow
+    by exhausting the call stack through unbounded recursion.  When this macro
+    is not defined, the default limit of 64 nested sequence levels is used.
+    Real-world DICOM data rarely exceeds 5-10 nesting levels.  The limit can
+    be changed at runtime per parse operation via
+    DcmInputStream::setMaxNestingDepth(), DcmItem::setMaxNestingDepth(), or
+    DcmFileFormat::setMaxNestingDepth().  Setting the runtime value to -1
+    disables the check entirely.
+    Minimum value: 1.  Values less than 1 cause a compile-time error.
+    Maximum value: 2147483647 (aligned to 32-bit signed integer runtime API).
+
 DCMTK_MERGE_STDERR_TO_STDOUT
   Affected: dcmdata
   Type of modification: Activates feature
index c8ba944bfedc46b23258a256f7594b550b3716e1..96533acba0b84d9b85b1795cfe5c6ff923fd3fc7 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2025, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -200,6 +200,8 @@ extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_BulkDataURINotSupported;
 extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_UnsupportedURIType;
 /// Execution of command line failed
 extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_CommandLineFailed;
+/// Maximum sequence nesting depth exceeded (stack overflow protection)
+extern DCMTK_DCMDATA_EXPORT const OFConditionConst EC_NestingDepthLimitExceeded;
 
 ///@}
 
index 452aa1dd0fdfdb197cba7e2d92956e1ee09898b6..18f5aec107c7e8d9da878d6ad469cab5e98d5801 100644 (file)
@@ -514,6 +514,35 @@ class DCMTK_DCMDATA_EXPORT DcmFileFormat
 
     /// implementation version name to write in the meta-header
     OFString ImplementationVersionName;
+
+    /// maximum sequence nesting depth for parsing
+    /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited)
+    Sint32 MaxNestingDepth;
+
+  public:
+
+    /** set the maximum permitted sequence nesting depth for parsing.
+     *  Applied to the input stream in loadFile() and read(), and also
+     *  forwarded to the contained dataset.
+     *  - Value 0 (default): apply the compile-time default
+     *    (DCMTK_MAX_SEQUENCE_NESTING, default is 64)
+     *  - Value -1: disable the check (allow unlimited nesting)
+     *  - Value > 0: use this value as the maximum permitted nesting depth
+     *  @param maxDepth maximum nesting depth setting
+     */
+    void setMaxNestingDepth(Sint32 maxDepth)
+    {
+        MaxNestingDepth = maxDepth;
+        DcmDataset* dset = getDataset();
+        if (dset)
+            dset->setMaxNestingDepth(maxDepth);
+    }
+
+    /** return the maximum permitted sequence nesting depth for parsing.
+     *  @return maximum nesting depth setting
+     *    (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited)
+     */
+    Sint32 getMaxNestingDepth() const { return MaxNestingDepth; }
 };
 
 
index f9ce25b99ae10f0a98a0bcc94c0a3028348811e9..0d190238a87f7f66e1cf056c23e1c7de7a78bee3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2018, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -224,6 +224,40 @@ public:
    */
   virtual DcmInputStreamFactory *newFactory() const = 0;
 
+  /** returns the current sequence nesting depth.
+   *  This counter is used to prevent stack overflow from deeply nested
+   *  DICOM sequences in malicious input files.
+   *  @return current nesting depth
+   */
+  Uint32 nestingDepth() const;
+
+  /** increments the sequence nesting depth counter.
+   *  @return the new nesting depth after incrementing
+   */
+  Uint32 incrementNestingDepth();
+
+  /** decrements the sequence nesting depth counter.
+   *  Does nothing if the counter is already zero.
+   */
+  void decrementNestingDepth();
+
+  /** returns the maximum permitted sequence nesting depth for this stream.
+   *  A value of 0 means the compile-time default (DCMTK_MAX_SEQUENCE_NESTING) applies.
+   *  A value of -1 means the check is disabled (unlimited nesting).
+   *  @return maximum nesting depth setting
+   */
+  Sint32 maxNestingDepth() const;
+
+  /** sets the maximum permitted sequence nesting depth for this stream.
+   *  Must be called before parsing begins.
+   *  - Value 0 (default): apply the compile-time default
+   *    (DCMTK_MAX_SEQUENCE_NESTING, default is 64)
+   *  - Value -1: disable the check (allow unlimited nesting)
+   *  - Value > 0: use this value as the maximum permitted nesting depth
+   *  @param maxDepth maximum nesting depth setting
+   */
+  void setMaxNestingDepth(Sint32 maxDepth);
+
   /** marks the current stream position for a later putback operation,
    *  overwriting a possibly existing prior putback mark.
    *  The DcmObject read methods rely on the possibility to putback
@@ -272,6 +306,12 @@ private:
 
   /// putback marker
   offile_off_t mark_;
+
+  /// current sequence nesting depth (for stack overflow protection)
+  Uint32 nestingDepth_;
+
+  /// maximum permitted sequence nesting depth (0 = default 64, -1 = unlimited)
+  Sint32 maxNestingDepth_;
 };
 
 
index 84d2b6f3bf88779f95c88cdcf7fd7b402415eea5..5b4a3ff865a5ed93f0820e97f1ec00376eb0b8d9 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2025, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -1573,6 +1573,29 @@ class DCMTK_DCMDATA_EXPORT DcmItem
 
     /// cache for private creator tags and identifiers
     DcmPrivateTagCache privateCreatorCache;
+
+    /// maximum sequence nesting depth for parsing
+    /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited)
+    Sint32 maxNestingDepth;
+
+  public:
+
+    /** set the maximum permitted sequence nesting depth for parsing.
+     *  This limit is applied to the input stream before parsing in
+     *  loadFile() and read().
+     *  - Value 0 (default): apply the compile-time default
+     *    (DCMTK_MAX_SEQUENCE_NESTING, default is 64)
+     *  - Value -1: disable the check (allow unlimited nesting)
+     *  - Value > 0: use this value as the maximum permitted nesting depth
+     *  @param maxDepth maximum nesting depth setting
+     */
+    void setMaxNestingDepth(Sint32 maxDepth) { maxNestingDepth = maxDepth; }
+
+    /** return the maximum permitted sequence nesting depth for parsing.
+     *  @return maximum nesting depth setting
+     *    (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING, -1 = unlimited)
+     */
+    Sint32 getMaxNestingDepth() const { return maxNestingDepth; }
 };
 
 /** Checks whether left hand side item is smaller than right hand side
index 714520bcf5732c377c5c73dabfa47899484d626a..9a1499cc56d96b7042f4d9a3f3b4fd5b9a3ff665 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2024, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -50,6 +50,18 @@ class DcmSpecificCharacterSet;
 
 // Undefined Length Identifier now defined in dctypes.h
 
+// Default maximum sequence nesting depth (can be overridden at compile time).
+// Must be in the range [1, 2147483647].
+#ifndef DCMTK_MAX_SEQUENCE_NESTING
+#define DCMTK_MAX_SEQUENCE_NESTING 64
+#endif
+#if DCMTK_MAX_SEQUENCE_NESTING < 1
+#error "DCMTK_MAX_SEQUENCE_NESTING must be >= 1"
+#endif
+#if DCMTK_MAX_SEQUENCE_NESTING > 2147483647
+#error "DCMTK_MAX_SEQUENCE_NESTING must be <= 2147483647"
+#endif
+
 // Maximum number of read bytes for a Value Element
 const Uint32 DCM_MaxReadLength = 4096;
 
index 9292f139c6157abfbac0d8c9b6a138256b86c93d..375d384e314c613c5d6240f5fb3d96a8dfd74c1a 100644 (file)
@@ -647,6 +647,9 @@ OFCondition DcmDataset::loadFileUntilTag(const OFFilename &fileName,
         {
             /* use stdin stream */
             DcmStdinStream inStream;
+            /* apply configured nesting depth limit */
+            if (getMaxNestingDepth() > 0)
+                inStream.setMaxNestingDepth(getMaxNestingDepth());
 
             /* clear this object */
             l_error = clear();
@@ -670,6 +673,9 @@ OFCondition DcmDataset::loadFileUntilTag(const OFFilename &fileName,
         } else {
             /* open file for input */
             DcmInputFileStream fileStream(fileName);
+            /* apply configured nesting depth limit */
+            if (getMaxNestingDepth() > 0)
+                fileStream.setMaxNestingDepth(getMaxNestingDepth());
 
             /* check stream status */
             l_error = fileStream.status();
index a27c93b5387dc8d682135446e6f4c190b051b5e7..330303674d40d40abf346ec90bb9569001b4b1c4 100644 (file)
@@ -91,6 +91,7 @@ makeOFConditionConst(EC_InvalidJSONContent,              OFM_dcmdata, 65, OF_err
 makeOFConditionConst(EC_BulkDataURINotSupported,         OFM_dcmdata, 66, OF_error, "BulkDataURI not yet supported"              );
 makeOFConditionConst(EC_UnsupportedURIType,              OFM_dcmdata, 67, OF_error, "Unsupported URI type"                       );
 makeOFConditionConst(EC_CommandLineFailed,               OFM_dcmdata, 68, OF_error, "Execution of command line failed"           );
+makeOFConditionConst(EC_NestingDepthLimitExceeded,       OFM_dcmdata, 69, OF_error, "Maximum sequence nesting depth exceeded"    );
 
 const unsigned short EC_CODE_CannotSelectCharacterSet     = 35;
 const unsigned short EC_CODE_CannotConvertCharacterSet    = 36;
index b1a8c16c034eeef3a8b1e473f268444d91ceeeb9..d4a91e6783a434f85863a95202840d30bcd8ba2b 100644 (file)
@@ -54,7 +54,8 @@ DcmFileFormat::DcmFileFormat()
   : DcmSequenceOfItems(DCM_InternalUseTag),
     FileReadMode(ERM_autoDetect),
     ImplementationClassUID(OFFIS_IMPLEMENTATION_CLASS_UID),
-    ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME)
+    ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME),
+    MaxNestingDepth(0)
 {
     DcmMetaInfo *MetaInfo = new DcmMetaInfo();
     DcmSequenceOfItems::itemList->insert(MetaInfo);
@@ -71,7 +72,8 @@ DcmFileFormat::DcmFileFormat(DcmDataset *dataset,
   : DcmSequenceOfItems(DCM_InternalUseTag),
     FileReadMode(ERM_autoDetect),
     ImplementationClassUID(OFFIS_IMPLEMENTATION_CLASS_UID),
-    ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME)
+    ImplementationVersionName(OFFIS_DTK_IMPLEMENTATION_VERSION_NAME),
+    MaxNestingDepth(0)
 {
     DcmMetaInfo *MetaInfo = new DcmMetaInfo();
     DcmSequenceOfItems::itemList->insert(MetaInfo);
@@ -99,7 +101,8 @@ DcmFileFormat::DcmFileFormat(const DcmFileFormat &old)
   : DcmSequenceOfItems(old),
     FileReadMode(old.FileReadMode),
     ImplementationClassUID(old.ImplementationClassUID),
-    ImplementationVersionName(old.ImplementationVersionName)
+    ImplementationVersionName(old.ImplementationVersionName),
+    MaxNestingDepth(old.MaxNestingDepth)
 {
 }
 
@@ -128,6 +131,7 @@ DcmFileFormat &DcmFileFormat::operator=(const DcmFileFormat &obj)
     FileReadMode = obj.FileReadMode;
     ImplementationClassUID = obj.ImplementationClassUID;
     ImplementationVersionName = obj.ImplementationVersionName;
+    MaxNestingDepth = obj.MaxNestingDepth;
   }
 
   return *this;
@@ -942,6 +946,9 @@ OFCondition DcmFileFormat::loadFileUntilTag(
         {
             /* use stdin stream */
             DcmStdinStream inStream;
+            /* apply configured nesting depth limit */
+            if (MaxNestingDepth != 0)
+                inStream.setMaxNestingDepth(MaxNestingDepth);
 
             /* clear this object */
             l_error = clear();
@@ -972,6 +979,9 @@ OFCondition DcmFileFormat::loadFileUntilTag(
         } else {
             /* open file for output */
             DcmInputFileStream fileStream(fileName);
+            /* apply configured nesting depth limit */
+            if (MaxNestingDepth > 0)
+                fileStream.setMaxNestingDepth(MaxNestingDepth);
 
             /* check stream status */
             l_error = fileStream.status();
index b61a79b7e32976f51d6efcc2669af123518f4137..efc446a627772c461d21630034101fcb9c8569f1 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2010, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -29,6 +29,8 @@ DcmInputStream::DcmInputStream(DcmProducer *initial)
 , compressionFilter_(NULL)
 , tell_(0)
 , mark_(0)
+, nestingDepth_(0)
+, maxNestingDepth_(0)
 {
 }
 
@@ -94,6 +96,32 @@ const DcmProducer *DcmInputStream::currentProducer() const
   return current_;
 }
 
+Uint32 DcmInputStream::nestingDepth() const
+{
+  return nestingDepth_;
+}
+
+Uint32 DcmInputStream::incrementNestingDepth()
+{
+  return ++nestingDepth_;
+}
+
+void DcmInputStream::decrementNestingDepth()
+{
+  if (nestingDepth_ > 0)
+    --nestingDepth_;
+}
+
+Sint32 DcmInputStream::maxNestingDepth() const
+{
+  return maxNestingDepth_;
+}
+
+void DcmInputStream::setMaxNestingDepth(Sint32 maxDepth)
+{
+  maxNestingDepth_ = maxDepth;
+}
+
 OFCondition DcmInputStream::installCompressionFilter(E_StreamCompression filterType)
 {
   OFCondition result = EC_Normal;
index 62bf16f2437feb64254a63d43e1dd578bb1c0348..274ad14ecb28fa63711f35c28fc043f335ec2c54 100644 (file)
@@ -83,7 +83,8 @@ DcmItem::DcmItem()
     elementList(NULL),
     lastElementComplete(OFTrue),
     fStartPosition(0),
-    privateCreatorCache()
+    privateCreatorCache(),
+    maxNestingDepth(0)
 {
     elementList = new DcmList;
 }
@@ -95,7 +96,8 @@ DcmItem::DcmItem(const DcmTag &tag,
     elementList(NULL),
     lastElementComplete(OFTrue),
     fStartPosition(0),
-    privateCreatorCache()
+    privateCreatorCache(),
+    maxNestingDepth(0)
 {
     elementList = new DcmList;
 }
@@ -106,7 +108,8 @@ DcmItem::DcmItem(const DcmItem &old)
     elementList(new DcmList),
     lastElementComplete(old.lastElementComplete),
     fStartPosition(old.fStartPosition),
-    privateCreatorCache()
+    privateCreatorCache(),
+    maxNestingDepth(old.maxNestingDepth)
 {
     if (!old.elementList->empty())
     {
@@ -136,6 +139,7 @@ DcmItem& DcmItem::operator=(const DcmItem& obj)
         // copy DcmItem's member variables
         lastElementComplete = obj.lastElementComplete;
         fStartPosition = obj.fStartPosition;
+        maxNestingDepth = obj.maxNestingDepth;
         if (!obj.elementList->empty())
         {
             elementList->seek(ELP_first);
@@ -1394,6 +1398,10 @@ OFCondition DcmItem::readUntilTag(DcmInputStream & inStream,
         return errorFlag;
     }
 
+    /* apply configured nesting depth limit to the input stream (0 = use default, skip override) */
+    if (getMaxNestingDepth() != 0)
+        inStream.setMaxNestingDepth(getMaxNestingDepth());
+
     /* figure out if the stream reported an error */
     errorFlag = inStream.status();
     /* if the stream reported an error or if it is the end of the */
index adeb64539f583b7f23c3e6e291666df89bf9586d..e10c375ac0cfade6096e301253745d534bebc791 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2023, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -704,6 +704,23 @@ OFCondition DcmSequenceOfItems::read(DcmInputStream &inStream,
         errorFlag = EC_IllegalCall;
     else
     {
+        const Uint32 depth = inStream.incrementNestingDepth();
+        const Sint32 maxDepthSetting = inStream.maxNestingDepth();
+        /* -1 = unlimited; 0 = use built-in default (DCMTK_MAX_SEQUENCE_NESTING); > 0 = custom limit */
+        const Uint32 effectiveMax = (maxDepthSetting == 0) ? DCMTK_MAX_SEQUENCE_NESTING
+                                  : (maxDepthSetting < 0)  ? 0   /* unlimited */
+                                  : OFstatic_cast(Uint32, maxDepthSetting);
+        if (effectiveMax > 0 && depth > effectiveMax)
+        {
+            DCMDATA_ERROR("DcmSequenceOfItems: Maximum nesting depth (" << effectiveMax
+                << ") exceeded while parsing sequence " << getTagName() << " " << getTag());
+            inStream.decrementNestingDepth();
+            errorFlag = EC_NestingDepthLimitExceeded;
+            // dump information if required
+            DCMDATA_TRACE("DcmSequenceOfItems::read() returns error = " << errorFlag.text());
+            return errorFlag;
+        }
+
         errorFlag = inStream.status();
 
         if (errorFlag.good() && inStream.eos())
@@ -773,6 +790,7 @@ OFCondition DcmSequenceOfItems::read(DcmInputStream &inStream,
             errorFlag = EC_Normal;
         if (errorFlag.good())
             setTransferState(ERW_ready);      // sequence is complete
+        inStream.decrementNestingDepth();
     }
     // dump information if required
     DCMDATA_TRACE("DcmSequenceOfItems::read() returns error = " << errorFlag.text());
index 98ea562ea7a71de310c6f1a79fdfa441e991cb20..607e5ad5dc4f0557505cd2e144584766d53288b5 100644 (file)
@@ -11,6 +11,7 @@ DCMTK_ADD_TEST_EXECUTABLE(dcmdata_tests
   ti2dbmp.cc
   titem.cc
   tmatch.cc
+  tnesting.cc
   tnewdcme.cc
   tparent.cc
   tparser.cc
index 800b37b2e646b5a90695b1edbbd8db9634935c88..fdfc723dd6b976abe6b4414a00e778f9823edc80 100644 (file)
@@ -712,6 +712,73 @@ tmatch.o: tmatch.cc ../../config/include/dcmtk/config/osconfig.h \
  ../include/dcmtk/dcmdata/dcvr.h \
  ../../ofstd/include/dcmtk/ofstd/ofglobal.h \
  ../include/dcmtk/dcmdata/dcmatch.h
+tnesting.o: tnesting.cc ../../config/include/dcmtk/config/osconfig.h \
+ ../../ofstd/include/dcmtk/ofstd/oftest.h \
+ ../../ofstd/include/dcmtk/ofstd/ofconapp.h \
+ ../../ofstd/include/dcmtk/ofstd/oftypes.h \
+ ../../ofstd/include/dcmtk/ofstd/ofdefine.h \
+ ../../ofstd/include/dcmtk/ofstd/ofcast.h \
+ ../../ofstd/include/dcmtk/ofstd/ofexport.h \
+ ../../ofstd/include/dcmtk/ofstd/ofstdinc.h \
+ ../../ofstd/include/dcmtk/ofstd/ofcmdln.h \
+ ../../ofstd/include/dcmtk/ofstd/ofexbl.h \
+ ../../ofstd/include/dcmtk/ofstd/oftraits.h \
+ ../../ofstd/include/dcmtk/ofstd/oflist.h \
+ ../../ofstd/include/dcmtk/ofstd/ofstring.h \
+ ../../ofstd/include/dcmtk/ofstd/ofstream.h \
+ ../../ofstd/include/dcmtk/ofstd/ofconsol.h \
+ ../../ofstd/include/dcmtk/ofstd/ofthread.h \
+ ../../ofstd/include/dcmtk/ofstd/offile.h \
+ ../../ofstd/include/dcmtk/ofstd/ofstd.h \
+ ../../ofstd/include/dcmtk/ofstd/ofcond.h \
+ ../../ofstd/include/dcmtk/ofstd/ofdiag.h \
+ ../../ofstd/include/dcmtk/ofstd/diag/push.def \
+ ../../ofstd/include/dcmtk/ofstd/diag/useafree.def \
+ ../../ofstd/include/dcmtk/ofstd/diag/pop.def \
+ ../../ofstd/include/dcmtk/ofstd/oflimits.h \
+ ../../ofstd/include/dcmtk/ofstd/oferror.h \
+ ../../ofstd/include/dcmtk/ofstd/ofexit.h \
+ ../include/dcmtk/dcmdata/dcuid.h ../include/dcmtk/dcmdata/dcdefine.h \
+ ../../oflog/include/dcmtk/oflog/oflog.h \
+ ../../oflog/include/dcmtk/oflog/logger.h \
+ ../../oflog/include/dcmtk/oflog/config.h \
+ ../../oflog/include/dcmtk/oflog/config/defines.h \
+ ../../oflog/include/dcmtk/oflog/helpers/threadcf.h \
+ ../../oflog/include/dcmtk/oflog/loglevel.h \
+ ../../ofstd/include/dcmtk/ofstd/ofvector.h \
+ ../../oflog/include/dcmtk/oflog/tstring.h \
+ ../../oflog/include/dcmtk/oflog/tchar.h \
+ ../../oflog/include/dcmtk/oflog/spi/apndatch.h \
+ ../../oflog/include/dcmtk/oflog/appender.h \
+ ../../ofstd/include/dcmtk/ofstd/ofmem.h \
+ ../../ofstd/include/dcmtk/ofstd/ofutil.h \
+ ../../ofstd/include/dcmtk/ofstd/variadic/tuplefwd.h \
+ ../../oflog/include/dcmtk/oflog/layout.h \
+ ../../oflog/include/dcmtk/oflog/streams.h \
+ ../../oflog/include/dcmtk/oflog/helpers/pointer.h \
+ ../../oflog/include/dcmtk/oflog/thread/syncprim.h \
+ ../../oflog/include/dcmtk/oflog/spi/filter.h \
+ ../../oflog/include/dcmtk/oflog/helpers/lockfile.h \
+ ../../oflog/include/dcmtk/oflog/spi/logfact.h \
+ ../../oflog/include/dcmtk/oflog/logmacro.h \
+ ../../oflog/include/dcmtk/oflog/helpers/snprintf.h \
+ ../../oflog/include/dcmtk/oflog/tracelog.h \
+ ../../ofstd/include/dcmtk/ofstd/oftempf.h \
+ ../include/dcmtk/dcmdata/dcdatset.h ../include/dcmtk/dcmdata/dcitem.h \
+ ../include/dcmtk/dcmdata/dctypes.h ../include/dcmtk/dcmdata/dcobject.h \
+ ../../ofstd/include/dcmtk/ofstd/ofglobal.h \
+ ../include/dcmtk/dcmdata/dcerror.h ../include/dcmtk/dcmdata/dcxfer.h \
+ ../include/dcmtk/dcmdata/dcvr.h \
+ ../../ofstd/include/dcmtk/ofstd/ofdeprec.h \
+ ../include/dcmtk/dcmdata/dctag.h ../include/dcmtk/dcmdata/dctagkey.h \
+ ../../ofstd/include/dcmtk/ofstd/diag/ignrattr.def \
+ ../include/dcmtk/dcmdata/dcstack.h ../include/dcmtk/dcmdata/dclist.h \
+ ../include/dcmtk/dcmdata/dcpcache.h ../include/dcmtk/dcmdata/dcfilefo.h \
+ ../include/dcmtk/dcmdata/dcsequen.h ../include/dcmtk/dcmdata/dcelem.h \
+ ../include/dcmtk/dcmdata/dcistrmb.h ../include/dcmtk/dcmdata/dcistrma.h \
+ ../include/dcmtk/dcmdata/dcostrmb.h ../include/dcmtk/dcmdata/dcostrma.h \
+ ../include/dcmtk/dcmdata/dcdeftag.h ../include/dcmtk/dcmdata/dcwcache.h \
+ ../include/dcmtk/dcmdata/dcfcache.h
 tnewdcme.o: tnewdcme.cc ../../config/include/dcmtk/config/osconfig.h \
  ../../ofstd/include/dcmtk/ofstd/oftest.h \
  ../../ofstd/include/dcmtk/ofstd/ofconapp.h \
index 03312127821a67eadbcbcdbc46712d115dbd823a..cfdd0ba85bae824b5375f4cb2a994021a73cb95b 100644 (file)
@@ -25,7 +25,7 @@ LIBDCMXML = -ldcmxml
 
 objs = tests.o tpread.o ti2dbmp.o tchval.o tpath.o tvrdatim.o telemlen.o tparser.o \
        tdict.o tvrds.o tvrfd.o tvrui.o tvrol.o tvrov.o tvrsv.o tvruv.o tstrval.o \
-       tspchrs.o tvrpn.o tparent.o tfilter.o tvrcomp.o tmatch.o tnewdcme.o \
+       tspchrs.o tvrpn.o tparent.o tfilter.o tvrcomp.o tmatch.o tnesting.o tnewdcme.o \
        tgenuid.o tsequen.o titem.o ttag.o txfer.o tbytestr.o tfrmsiz.o
 
 progs = tests
index c8b40d42150e760a28b67177f71968eddb0bb9be..6efd48834f9da0db77164e7314e56b55c10dfe43 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 2011-2025 OFFIS e.V.
+ *  Copyright (C) 2011-2026 OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were developed by
@@ -127,5 +127,12 @@ OFTEST_REGISTER(dcmdata_xferLookup_3);
 OFTEST_REGISTER(dcmdata_xferLookup_4);
 OFTEST_REGISTER(dcmdata_putOFStringAtPos);
 OFTEST_REGISTER(dcmdata_uncompressedFrameSize);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_exceeded);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_atLimit);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_wellBelow);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_customLimit);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_disabled);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_datasetAPI);
+OFTEST_REGISTER(dcmdata_nestingDepthLimit_fileFormatAPI);
 
 OFTEST_MAIN("dcmdata")
diff --git a/dcmdata/tests/tnesting.cc b/dcmdata/tests/tnesting.cc
new file mode 100644 (file)
index 0000000..5b89f32
--- /dev/null
@@ -0,0 +1,266 @@
+/*
+ *
+ *  Copyright (C) 2026, OFFIS e.V.
+ *  All rights reserved.  See COPYRIGHT file for details.
+ *
+ *  This software and supporting documentation were developed by
+ *
+ *    OFFIS e.V.
+ *    R&D Division Health
+ *    Escherweg 2
+ *    D-26121 Oldenburg, Germany
+ *
+ *
+ *  Module:  dcmdata
+ *
+ *  Author:  Michael Onken
+ *
+ *  Purpose: test program for sequence nesting depth limit
+ *
+ */
+
+
+#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
+
+#include "dcmtk/ofstd/oftest.h"
+#include "dcmtk/ofstd/oftempf.h"
+#include "dcmtk/dcmdata/dcdatset.h"
+#include "dcmtk/dcmdata/dcfilefo.h"
+#include "dcmtk/dcmdata/dcsequen.h"
+#include "dcmtk/dcmdata/dcistrmb.h"
+#include "dcmtk/dcmdata/dcostrmb.h"
+#include "dcmtk/dcmdata/dcerror.h"
+#include "dcmtk/dcmdata/dcdeftag.h"
+#include "dcmtk/dcmdata/dcwcache.h"
+
+
+/** Helper: build a DcmDataset containing a chain of nested sequences to the
+ *  specified depth using the high-level dcmdata API.  Each nesting level
+ *  consists of a DcmSequenceOfItems with one DcmItem, and the innermost
+ *  item is empty.
+ *  @param depth the number of nested sequence levels to generate
+ *  @param dset  output dataset (cleared before use)
+ */
+static void buildNestedDataset(Uint32 depth, DcmDataset& dset)
+{
+    dset.clear();
+    /* start with the dataset as the outermost item */
+    DcmItem* currentItem = &dset;
+    for (Uint32 i = 0; i < depth; ++i)
+    {
+        DcmSequenceOfItems* seq = new DcmSequenceOfItems(DCM_ReferencedSeriesSequence);
+        currentItem->insert(seq);
+        DcmItem* item = new DcmItem();
+        seq->insert(item);
+        currentItem = item;
+    }
+}
+
+
+/** Helper: serialize a dataset to a byte buffer and parse it back using a
+ *  fresh DcmInputStream, with an optional custom nesting depth limit.
+ *  @param srcDset        the dataset to serialize
+ *  @param dstDset        output dataset to receive the parsed result
+ *  @param maxNestingDepth nesting depth setting passed to setMaxNestingDepth():
+ *         0 (default) = compile-time default (DCMTK_MAX_SEQUENCE_NESTING), -1 = unlimited, > 0 = custom limit
+ *  @return the OFCondition returned by DcmDataset::read()
+ */
+static OFCondition serializeAndParse(DcmDataset& srcDset, DcmDataset& dstDset,
+                                     Sint32 maxNestingDepth = 0)
+{
+    /* write the dataset to a memory buffer */
+    const size_t bufLen = 1024 * 1024;
+    Uint8* buf = new Uint8[bufLen];
+
+    DcmOutputBufferStream outStream(buf, bufLen);
+    srcDset.transferInit();
+    DcmWriteCache wcache;
+    OFCondition cond = srcDset.write(outStream, EXS_LittleEndianExplicit, EET_UndefinedLength, &wcache);
+    srcDset.transferEnd();
+    if (cond.bad())
+    {
+        delete[] buf;
+        return cond;
+    }
+    offile_off_t bytesWritten = 0;
+    void* writtenBuf = NULL;
+    outStream.flushBuffer(writtenBuf, bytesWritten);
+
+    /* parse it back with nesting depth limit */
+    DcmInputBufferStream inStream;
+    inStream.setBuffer(buf, bytesWritten);
+    inStream.setEos();
+    inStream.setMaxNestingDepth(maxNestingDepth);
+
+    dstDset.clear();
+    dstDset.transferInit();
+    cond = dstDset.read(inStream, EXS_LittleEndianExplicit);
+    dstDset.transferEnd();
+
+    delete[] buf;
+    return cond;
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_exceeded)
+{
+    /* One level beyond the default limit (DCMTK_MAX_SEQUENCE_NESTING) must be rejected. */
+    DcmDataset srcDset, dstDset;
+    buildNestedDataset(DCMTK_MAX_SEQUENCE_NESTING + 1, srcDset);
+
+    OFCondition cond = serializeAndParse(srcDset, dstDset);
+    if (cond != EC_NestingDepthLimitExceeded)
+    {
+        OFCHECK_FAIL("Expected EC_NestingDepthLimitExceeded for " << (DCMTK_MAX_SEQUENCE_NESTING + 1)
+            << " levels of nesting, but got: " << cond.text());
+    }
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_atLimit)
+{
+    /* Exactly DCMTK_MAX_SEQUENCE_NESTING levels must still pass. */
+    DcmDataset srcDset, dstDset;
+    buildNestedDataset(DCMTK_MAX_SEQUENCE_NESTING, srcDset);
+
+    OFCondition cond = serializeAndParse(srcDset, dstDset);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("Parsing " << DCMTK_MAX_SEQUENCE_NESTING
+            << " levels of nesting should succeed, but got: " << cond.text());
+    }
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_wellBelow)
+{
+    /* 5 levels of nesting (typical real-world depth) must parse fine. */
+    DcmDataset srcDset, dstDset;
+    buildNestedDataset(5, srcDset);
+
+    OFCondition cond = serializeAndParse(srcDset, dstDset);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("Parsing 5 levels of nesting should succeed, but got: " << cond.text());
+    }
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_customLimit)
+{
+    /* Test setMaxNestingDepth(): set the limit to 10.
+     * 10 levels must succeed, 11 levels must fail. */
+    DcmDataset srcDset, dstDset;
+    OFCondition cond;
+
+    buildNestedDataset(10, srcDset);
+    cond = serializeAndParse(srcDset, dstDset, 10);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("Parsing 10 levels with maxNestingDepth=10 should succeed, but got: " << cond.text());
+    }
+
+    buildNestedDataset(11, srcDset);
+    cond = serializeAndParse(srcDset, dstDset, 10);
+    if (cond != EC_NestingDepthLimitExceeded)
+    {
+        OFCHECK_FAIL("Expected EC_NestingDepthLimitExceeded for 11 levels with maxNestingDepth=10, but got: " << cond.text());
+    }
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_disabled)
+{
+    /* Test that setMaxNestingDepth(-1) disables the limit entirely.
+     * Even 200 levels of nesting must succeed. */
+    DcmDataset srcDset, dstDset;
+    buildNestedDataset(200, srcDset);
+
+    OFCondition cond = serializeAndParse(srcDset, dstDset, -1);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("Parsing 200 levels with maxNestingDepth=-1 (unlimited) should succeed, but got: " << cond.text());
+    }
+}
+
+
+/** Helper: create a temporary file path.
+ *  @param tmpFile receives the temporary file path
+ *  @return EC_Normal if successful
+ */
+static OFCondition makeTempFile(OFString& tmpFile)
+{
+    return OFTempFile::createFile(tmpFile, NULL /* fd_out */, O_RDWR,
+        "" /* dir */, "" /* prefix */, ".dcm" /* postfix */);
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_datasetAPI)
+{
+    /* Test DcmDataset::setMaxNestingDepth() with loadFile(). */
+    DcmDataset srcDset;
+    OFString tmpFile;
+    OFCondition cond;
+
+    /* save as raw dataset (no meta header) */
+    buildNestedDataset(11, srcDset);
+    cond = makeTempFile(tmpFile);
+    OFCHECK(cond.good());
+    cond = srcDset.saveFile(tmpFile, EXS_LittleEndianExplicit);
+    OFCHECK(cond.good());
+
+    /* default limit (DCMTK_MAX_SEQUENCE_NESTING): 11 levels should succeed */
+    DcmDataset dset1;
+    cond = dset1.loadFile(tmpFile, EXS_LittleEndianExplicit);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("DcmDataset::loadFile() with 11 levels should succeed with default limit, but got: " << cond.text());
+    }
+
+    /* custom limit 10: 11 levels should fail */
+    DcmDataset dset2;
+    dset2.setMaxNestingDepth(10);
+    cond = dset2.loadFile(tmpFile, EXS_LittleEndianExplicit);
+    if (cond != EC_NestingDepthLimitExceeded)
+    {
+        OFCHECK_FAIL("DcmDataset::loadFile() with 11 levels and maxNestingDepth=10 should fail, but got: " << cond.text());
+    }
+
+    OFStandard::deleteFile(tmpFile);
+}
+
+
+OFTEST(dcmdata_nestingDepthLimit_fileFormatAPI)
+{
+    /* Test DcmFileFormat::setMaxNestingDepth() with loadFile(). */
+    DcmDataset srcDset;
+    OFString tmpFile;
+    OFCondition cond;
+
+    /* save as DcmFileFormat (with meta header) */
+    buildNestedDataset(11, srcDset);
+    cond = makeTempFile(tmpFile);
+    OFCHECK(cond.good());
+    DcmFileFormat srcFF(&srcDset);
+    cond = srcFF.saveFile(tmpFile, EXS_LittleEndianExplicit);
+    OFCHECK(cond.good());
+
+    /* default limit (DCMTK_MAX_SEQUENCE_NESTING): 11 levels should succeed */
+    DcmFileFormat ff1;
+    cond = ff1.loadFile(tmpFile);
+    if (cond.bad())
+    {
+        OFCHECK_FAIL("DcmFileFormat::loadFile() with 11 levels should succeed with default limit, but got: " << cond.text());
+    }
+
+    /* custom limit 10: 11 levels should fail */
+    DcmFileFormat ff2;
+    ff2.setMaxNestingDepth(10);
+    cond = ff2.loadFile(tmpFile);
+    if (cond != EC_NestingDepthLimitExceeded)
+    {
+        OFCHECK_FAIL("DcmFileFormat::loadFile() with 11 levels and maxNestingDepth=10 should fail, but got: " << cond.text());
+    }
+
+    OFStandard::deleteFile(tmpFile);
+}
index 316ec7b12eb22903972989ac60797edc18a55580..fdf73d3d1af998d080d87b1a7d5a6560bb9ddb03 100644 (file)
@@ -360,6 +360,17 @@ public:
      */
     void setAlwaysAcceptDefaultRole(const OFBool enabled);
 
+    /** Set the maximum permitted sequence nesting depth for parsing
+     *  datasets received over the network. This limit is applied to
+     *  each dataset received via receiveDIMSEDataset().
+     *  - Value 0 (default): apply the compile-time default
+     *    (DCMTK_MAX_SEQUENCE_NESTING)
+     *  - Value -1: disable the check (allow unlimited nesting)
+     *  - Value > 0: use this value as the maximum nesting depth
+     *  @param maxDepth maximum nesting depth setting
+     */
+    void setMaxNestingDepth(const Sint32 maxDepth);
+
     /* Get methods for SCP settings */
 
     /** Returns TCP/IP port number SCP listens for new connection requests
@@ -434,6 +445,14 @@ public:
      */
     OFBool getProgressNotificationMode() const;
 
+    /** Return the maximum permitted sequence nesting depth for
+     *  parsing datasets received over the network.
+     *  @return maximum nesting depth setting
+     *    (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING,
+     *    -1 = unlimited)
+     */
+    Sint32 getMaxNestingDepth() const;
+
     /** Get access to the configuration of the SCP. Note that the functionality
      *  on the configuration object is shadowed by other API functions of DcmSCP.
      *  The existing functions are provided in order to not break users of this
@@ -1157,6 +1176,11 @@ private:
     /// it, e.g. in the context of the DcmSCPPool class.
     DcmSharedSCPConfig m_cfg;
 
+    /// Maximum sequence nesting depth for parsing received datasets
+    /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING,
+    /// -1 = unlimited)
+    Sint32 m_maxNestingDepth;
+
     /** Drops association and clears internal structures to free memory
      */
     void dropAndDestroyAssociation();
index 5a83c25a8163fc85bbae7f70c88b6ced042dea35..8fcb17866cc04d67381dc131a2abe6abaa7ad48c 100644 (file)
@@ -739,6 +739,17 @@ public:
      */
     void setProgressNotificationMode(const OFBool mode);
 
+    /** Set the maximum permitted sequence nesting depth for parsing
+     *  datasets received over the network. This limit is applied to
+     *  each dataset received via receiveDIMSEDataset().
+     *  - Value 0 (default): apply the compile-time default
+     *    (DCMTK_MAX_SEQUENCE_NESTING)
+     *  - Value -1: disable the check (allow unlimited nesting)
+     *  - Value > 0: use this value as the maximum nesting depth
+     *  @param maxDepth maximum nesting depth setting
+     */
+    void setMaxNestingDepth(const Sint32 maxDepth);
+
     /* Get methods */
 
     /** Get current connection status
@@ -831,6 +842,14 @@ public:
      */
     OFBool getProgressNotificationMode() const;
 
+    /** Return the maximum permitted sequence nesting depth for
+     *  parsing datasets received over the network.
+     *  @return maximum nesting depth setting
+     *    (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING,
+     *    -1 = unlimited)
+     */
+    Sint32 getMaxNestingDepth() const;
+
     /** Returns whether SCU is configured to create a TLS connection with the SCP
      *  @return OFTrue if TLS mode has been enabled, OFFalse otherwise
      */
@@ -1136,6 +1155,11 @@ private:
     /// Progress notification mode (default: enabled)
     OFBool m_progressNotificationMode;
 
+    /// Maximum sequence nesting depth for parsing received datasets
+    /// (0 = compile-time default DCMTK_MAX_SEQUENCE_NESTING,
+    /// -1 = unlimited)
+    Sint32 m_maxNestingDepth;
+
     /// Flag indicating whether secure mode has been enabled (default: disabled)
     OFBool m_secureConnectionEnabled;
 
index 38d011ff5abaa2183baf2d603cc1c645af536966..28da484ae7b3c03985387dd013fd287d9a53caa6 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *
- *  Copyright (C) 1994-2025, OFFIS e.V.
+ *  Copyright (C) 1994-2026, OFFIS e.V.
  *  All rights reserved.  See COPYRIGHT file for details.
  *
  *  This software and supporting documentation were partly developed by
@@ -1536,7 +1536,8 @@ DIMSE_receiveDataSetInMemory(
         dset = *dataObject;
     }
 
-    /* check if there is still no DcmDataset object which can be used to store the data set. */
+    /* check if there is still no DcmDataset object (i.e. if allocation fails) */
+    /* which can be used to store the data set. */
     if (dset == NULL)
     {
         /* if this is the case, just go ahead an receive data, but do not store it anywhere */
index 0b84d00bac3c7a27a087edb3b0b77b88b5b217c1..bfb336db030b1c2e7281eb5ed86845ae8d096616 100644 (file)
@@ -32,6 +32,7 @@ DcmSCP::DcmSCP()
 : m_network(NULL)
 , m_assoc(NULL)
 , m_cfg()
+, m_maxNestingDepth(0)
 {
     OFStandard::initializeNetwork();
 }
@@ -1590,6 +1591,19 @@ OFCondition DcmSCP::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm
     if (m_assoc == NULL)
         return DIMSE_ILLEGALASSOCIATION;
 
+    /* if a custom nesting depth limit is configured, pre-allocate
+     * the dataset and apply the setting before parsing starts.
+     * DIMSE_receiveDataSetInMemory() will use this dataset instead
+     * of creating a new one internally. */
+    OFBool weAllocated = OFFalse;
+    if (dataObject != NULL && *dataObject == NULL
+        && m_maxNestingDepth != 0)
+    {
+        *dataObject = new DcmDataset();
+        (*dataObject)->setMaxNestingDepth(m_maxNestingDepth);
+        weAllocated = OFTrue;
+    }
+
     OFCondition cond;
     /* call the corresponding DIMSE function to receive the dataset */
     if (m_cfg->getProgressNotificationMode())
@@ -1619,6 +1633,14 @@ OFCondition DcmSCP::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm
     }
     else
     {
+        /* if we pre-allocated the dataset, clean up on error since
+         * DIMSE_receiveDataSetInMemory() won't delete a dataset
+         * that was passed in by the caller */
+        if (weAllocated && dataObject != NULL)
+        {
+            delete *dataObject;
+            *dataObject = NULL;
+        }
         OFString tempStr;
         DCMNET_ERROR("Unable to receive dataset on presentation context "
                      << OFstatic_cast(unsigned int, *presID) << ": " << DimseCondition::dump(tempStr, cond));
@@ -1910,6 +1932,13 @@ void DcmSCP::setAlwaysAcceptDefaultRole(const OFBool enabled)
 
 // ----------------------------------------------------------------------------
 
+void DcmSCP::setMaxNestingDepth(const Sint32 maxDepth)
+{
+    m_maxNestingDepth = maxDepth;
+}
+
+// ----------------------------------------------------------------------------
+
 /* Get methods for SCP settings and current association information */
 
 OFBool DcmSCP::getRefuseAssociation() const
@@ -2003,6 +2032,13 @@ OFBool DcmSCP::getProgressNotificationMode() const
 
 // ----------------------------------------------------------------------------
 
+Sint32 DcmSCP::getMaxNestingDepth() const
+{
+    return m_maxNestingDepth;
+}
+
+// ----------------------------------------------------------------------------
+
 OFBool DcmSCP::isConnected() const
 {
     return (m_assoc != NULL) && (m_assoc->DULassociation != NULL);
index fa3a863509a2aa75af777522bf1161c1beed0801..9fe002335035d68fd77c510d51ff100b290d84cd 100644 (file)
@@ -56,6 +56,7 @@ DcmSCU::DcmSCU()
     , m_verbosePCMode(OFFalse)
     , m_datasetConversionMode(OFFalse)
     , m_progressNotificationMode(OFTrue)
+    , m_maxNestingDepth(0)
     , m_secureConnectionEnabled(OFFalse)
     , m_protocolVersion(ASC_AF_Default)
 {
@@ -2500,6 +2501,19 @@ OFCondition DcmSCU::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm
     if (!isConnected())
         return DIMSE_ILLEGALASSOCIATION;
 
+    /* if a custom nesting depth limit is configured, pre-allocate
+     * the dataset and apply the setting before parsing starts.
+     * DIMSE_receiveDataSetInMemory() will use this dataset instead
+     * of creating a new one internally. */
+    OFBool weAllocated = OFFalse;
+    if (dataObject != NULL && *dataObject == NULL
+        && m_maxNestingDepth != 0)
+    {
+        *dataObject = new DcmDataset();
+        (*dataObject)->setMaxNestingDepth(m_maxNestingDepth);
+        weAllocated = OFTrue;
+    }
+
     OFCondition cond;
     /* call the corresponding DIMSE function to receive the dataset */
     if (m_progressNotificationMode)
@@ -2519,6 +2533,14 @@ OFCondition DcmSCU::receiveDIMSEDataset(T_ASC_PresentationContextID* presID, Dcm
     }
     else
     {
+        /* if we pre-allocated the dataset, clean up on error since
+         * DIMSE_receiveDataSetInMemory() won't delete a dataset
+         * that was passed in by the caller */
+        if (weAllocated && dataObject != NULL)
+        {
+            delete *dataObject;
+            *dataObject = NULL;
+        }
         OFString tempStr;
         DCMNET_ERROR("Unable to receive dataset on presentation context "
                      << OFstatic_cast(unsigned int, *presID) << ": " << DimseCondition::dump(tempStr, cond));
@@ -2607,6 +2629,11 @@ void DcmSCU::setProgressNotificationMode(const OFBool mode)
     m_progressNotificationMode = mode;
 }
 
+void DcmSCU::setMaxNestingDepth(const Sint32 maxDepth)
+{
+    m_maxNestingDepth = maxDepth;
+}
+
 void DcmSCU::setProtocolVersion(T_ASC_ProtocolFamily protocolVersion)
 {
     m_protocolVersion = protocolVersion;
@@ -2694,6 +2721,11 @@ OFBool DcmSCU::getProgressNotificationMode() const
     return m_progressNotificationMode;
 }
 
+Sint32 DcmSCU::getMaxNestingDepth() const
+{
+    return m_maxNestingDepth;
+}
+
 OFCondition DcmSCU::getDatasetInfo(DcmDataset* dataset,
                                    OFString& sopClassUID,
                                    OFString& sopInstanceUID,
index e7a2b438ba00b4b6d1766a8a15e8369992f11f7b..67aa12e05c2a08a5dea2d7411035eb2a5f2015e3 100644 (file)
@@ -51,6 +51,11 @@ OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_fails_when_requestedsopinstance_is_em
 OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_modifies_instance_when_scp_has_instance);
 OFTEST_REGISTER(dcmnet_scu_sendNSETRequest_succeeds_and_sets_responsestatuscode_from_scp_when_scp_sets_error_status);
 
+OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_getset);
+OFTEST_REGISTER(dcmnet_scu_maxNestingDepth_getset);
+OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_rejects_deep);
+OFTEST_REGISTER(dcmnet_scp_maxNestingDepth_accepts_shallow);
+
 #endif // WITH_THREADS
 
 OFTEST_MAIN("dcmnet")
index 3bf0b2609f88a43fcd9236f9e90cf06413a53f50..e5a83a02f4ee4fcefe4ab5af29207b3440dc2999 100644 (file)
@@ -1113,4 +1113,209 @@ OFTEST_FLAGS(dcmnet_scu_sendNSETRequest_succeeds_and_sets_responsestatuscode_fro
 }
 
 
+// Test that setMaxNestingDepth/getMaxNestingDepth work on DcmSCP
+OFTEST(dcmnet_scp_maxNestingDepth_getset)
+{
+    DcmSCP scp;
+    // Default must be 0 (use compile-time default)
+    OFCHECK_EQUAL(scp.getMaxNestingDepth(), 0);
+    scp.setMaxNestingDepth(10);
+    OFCHECK_EQUAL(scp.getMaxNestingDepth(), 10);
+    scp.setMaxNestingDepth(-1);
+    OFCHECK_EQUAL(scp.getMaxNestingDepth(), -1);
+    scp.setMaxNestingDepth(0);
+    OFCHECK_EQUAL(scp.getMaxNestingDepth(), 0);
+}
+
+
+// Test that setMaxNestingDepth/getMaxNestingDepth work on DcmSCU
+OFTEST(dcmnet_scu_maxNestingDepth_getset)
+{
+    DcmSCU scu;
+    // Default must be 0 (use compile-time default)
+    OFCHECK_EQUAL(scu.getMaxNestingDepth(), 0);
+    scu.setMaxNestingDepth(5);
+    OFCHECK_EQUAL(scu.getMaxNestingDepth(), 5);
+    scu.setMaxNestingDepth(-1);
+    OFCHECK_EQUAL(scu.getMaxNestingDepth(), -1);
+    scu.setMaxNestingDepth(0);
+    OFCHECK_EQUAL(scu.getMaxNestingDepth(), 0);
+}
+
+
+/** Helper: build a DcmDataset with nested sequences to a given depth.
+ *  @param depth number of nesting levels
+ *  @param dset  output dataset (cleared before use)
+ */
+static void buildNestedDataset(Uint32 depth, DcmDataset& dset)
+{
+    dset.clear();
+    DcmItem* currentItem = &dset;
+    for (Uint32 i = 0; i < depth; ++i)
+    {
+        DcmSequenceOfItems* sq =
+            new DcmSequenceOfItems(DCM_ReferencedSeriesSequence);
+        currentItem->insert(sq);
+        DcmItem* item = new DcmItem();
+        sq->insert(item);
+        currentItem = item;
+    }
+}
+
+
+/** SCP that applies a nesting depth limit and records whether
+ *  receiving the dataset succeeded or failed.
+ */
+struct NestingTestSCP : TestSCP
+{
+    NestingTestSCP(Sint32 maxNesting)
+        : TestSCP()
+        , m_receiveResult(EC_NotYetImplemented)
+    {
+        setMaxNestingDepth(maxNesting);
+        DcmSCPConfig& config = getConfig();
+        config.setAETitle("NESTING_SCP");
+        config.setConnectionBlockingMode(DUL_NOBLOCK);
+        config.setConnectionTimeout(10);
+        config.setHostLookupEnabled(OFFalse);
+        config.setPort(0);
+        OFList<OFString> xfers;
+        xfers.push_back(UID_LittleEndianImplicitTransferSyntax);
+        OFCHECK(config.addPresentationContext(
+            UID_ModalityPerformedProcedureStepSOPClass, xfers).good());
+        OFCHECK(openListenPort().good());
+        m_portNum = config.getPort();
+    }
+
+    OFCondition handleIncomingCommand(
+        T_DIMSE_Message* incomingMsg,
+        const DcmPresentationContextInfo& presInfo)
+    {
+        if (incomingMsg->CommandField == DIMSE_N_CREATE_RQ)
+        {
+            T_DIMSE_N_CreateRQ& req = incomingMsg->msg.NCreateRQ;
+            if (req.DataSetType != DIMSE_DATASET_NULL)
+            {
+                DcmDataset* dataset = OFnullptr;
+                T_ASC_PresentationContextID presIDdset;
+                m_receiveResult =
+                    receiveDIMSEDataset(&presIDdset, &dataset);
+                /* send a response regardless of receive outcome */
+                T_DIMSE_Message rsp;
+                memset(&rsp, 0, sizeof(rsp));
+                rsp.CommandField = DIMSE_N_CREATE_RSP;
+                T_DIMSE_N_CreateRSP& createRsp = rsp.msg.NCreateRSP;
+                createRsp.MessageIDBeingRespondedTo = req.MessageID;
+                createRsp.DimseStatus = m_receiveResult.good()
+                    ? STATUS_N_Success
+                    : STATUS_N_ProcessingFailure;
+                createRsp.DataSetType = DIMSE_DATASET_NULL;
+                OFStandard::strlcpy(
+                    createRsp.AffectedSOPClassUID,
+                    req.AffectedSOPClassUID,
+                    sizeof(createRsp.AffectedSOPClassUID));
+                createRsp.opts = O_NCREATE_AFFECTEDSOPCLASSUID;
+                sendDIMSEMessage(
+                    presInfo.presentationContextID, &rsp, NULL);
+                delete dataset;
+            }
+            return EC_Normal;
+        }
+        return DcmSCP::handleIncomingCommand(incomingMsg, presInfo);
+    }
+
+    /// Result of receiveDIMSEDataset() for the last request
+    OFCondition m_receiveResult;
+    /// Port the SCP is listening on
+    Uint16 m_portNum;
+};
+
+
+// Test that SCP's nesting depth limit rejects deeply nested data
+// received over the network
+OFTEST_FLAGS(dcmnet_scp_maxNestingDepth_rejects_deep, EF_Slow)
+{
+    NestingTestSCP scp(3);
+    scp.m_set_stop_after_assoc = OFTrue;
+    scp.start();
+    OFStandard::forceSleep(1);
+
+    // SCU sends a dataset with 4 levels of nesting (exceeds limit 3)
+    DcmSCU scu;
+    scu.setAETitle("NESTING_SCU");
+    scu.setPeerAETitle("NESTING_SCP");
+    scu.setPeerHostName("localhost");
+    scu.setPeerPort(scp.m_portNum);
+    OFList<OFString> xfers;
+    xfers.push_back(UID_LittleEndianImplicitTransferSyntax);
+    OFCHECK(scu.addPresentationContext(
+        UID_ModalityPerformedProcedureStepSOPClass, xfers).good());
+    OFCHECK(scu.initNetwork().good());
+    OFCHECK(scu.negotiateAssociation().good());
+
+    DcmDataset reqDataset;
+    buildNestedDataset(4, reqDataset);
+    T_ASC_PresentationContextID presID = scu.findPresentationContextID(
+        UID_ModalityPerformedProcedureStepSOPClass,
+        UID_LittleEndianImplicitTransferSyntax);
+    OFCHECK(presID != 0);
+    Uint16 rspStatus = 0;
+    DcmDataset* rspDataset = NULL;
+    scu.sendNCREATERequest(presID, "1.2.3.4.5",
+        &reqDataset, rspDataset, rspStatus);
+    delete rspDataset;
+    if (scu.isConnected())
+        scu.releaseAssociation();
+    OFStandard::forceSleep(2);
+    scp.join();
+
+    // Verify SCP saw the nesting depth error
+    OFCHECK(scp.m_receiveResult == DIMSE_RECEIVEFAILED);
+}
+
+
+// Test that SCP's nesting depth limit accepts data within the limit
+OFTEST_FLAGS(dcmnet_scp_maxNestingDepth_accepts_shallow, EF_Slow)
+{
+    NestingTestSCP scp(3);
+    scp.m_set_stop_after_assoc = OFTrue;
+    scp.start();
+    OFStandard::forceSleep(1);
+
+    // SCU sends a dataset with 3 levels of nesting (at the limit)
+    DcmSCU scu;
+    scu.setAETitle("NESTING_SCU");
+    scu.setPeerAETitle("NESTING_SCP");
+    scu.setPeerHostName("localhost");
+    scu.setPeerPort(scp.m_portNum);
+    OFList<OFString> xfers;
+    xfers.push_back(UID_LittleEndianImplicitTransferSyntax);
+    OFCHECK(scu.addPresentationContext(
+        UID_ModalityPerformedProcedureStepSOPClass, xfers).good());
+    OFCHECK(scu.initNetwork().good());
+    OFCHECK(scu.negotiateAssociation().good());
+
+    DcmDataset reqDataset;
+    buildNestedDataset(3, reqDataset);
+    T_ASC_PresentationContextID presID = scu.findPresentationContextID(
+        UID_ModalityPerformedProcedureStepSOPClass,
+        UID_LittleEndianImplicitTransferSyntax);
+    OFCHECK(presID != 0);
+    Uint16 rspStatus = 0;
+    DcmDataset* rspDataset = NULL;
+    OFCondition result = scu.sendNCREATERequest(presID, "1.2.3.4.5",
+        &reqDataset, rspDataset, rspStatus);
+    OFCHECK_MSG(result.good(), result.text());
+    OFCHECK(rspStatus == STATUS_N_Success);
+    delete rspDataset;
+    if (scu.isConnected())
+        OFCHECK(scu.releaseAssociation().good());
+    OFStandard::forceSleep(2);
+    scp.join();
+
+    // Verify SCP successfully received the dataset
+    OFCHECK(scp.m_receiveResult.good());
+}
+
+
 #endif // WITH_THREADS