|
|
|
@ -1,396 +0,0 @@
|
|
|
|
|
Description: Fix unbounded recursion CVEs
|
|
|
|
|
Origin: https://github.com/jbeder/yaml-cpp/pull/807
|
|
|
|
|
Forwarded: yes
|
|
|
|
|
Bug: https://github.com/jbeder/yaml-cpp/issues/459
|
|
|
|
|
Bug: https://github.com/jbeder/yaml-cpp/issues/655
|
|
|
|
|
Bug: https://github.com/jbeder/yaml-cpp/issues/654
|
|
|
|
|
Bug: https://github.com/jbeder/yaml-cpp/issues/660
|
|
|
|
|
|
|
|
|
|
This cherry-picks the (so-far-unmerged) upstream pull request.
|
|
|
|
|
The final 5 commits of that PR are not included; they only add tests
|
|
|
|
|
to a file not present in the 0.6.2 (or 0.6.3) release.
|
|
|
|
|
|
|
|
|
|
From d540476e31b080aa1f903ad20ec0426dd3838be7 Mon Sep 17 00:00:00 2001
|
|
|
|
|
From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= <anarcat@debian.org>
|
|
|
|
|
Date: Tue, 25 Apr 2017 20:10:20 -0400
|
|
|
|
|
Subject: [PATCH 1/9] fix stack overflow in HandleNode() (CVE-2017-5950)
|
|
|
|
|
|
|
|
|
|
simply set a hardcoded recursion limit to 2000 (inspired by Python's)
|
|
|
|
|
to avoid infinitely recursing into arbitrary data structures
|
|
|
|
|
|
|
|
|
|
assert() the depth. unsure if this is the right approach, but given
|
|
|
|
|
that HandleNode() is "void", I am not sure how else to return an
|
|
|
|
|
error. the problem with this approach of course is that it will still
|
|
|
|
|
crash the caller, unless they have proper exception handling in place.
|
|
|
|
|
|
|
|
|
|
Closes: #459
|
|
|
|
|
---
|
|
|
|
|
src/singledocparser.cpp | 2 ++
|
|
|
|
|
src/singledocparser.h | 2 ++
|
|
|
|
|
2 files changed, 4 insertions(+)
|
|
|
|
|
|
|
|
|
|
diff --git a/src/singledocparser.cpp b/src/singledocparser.cpp
|
|
|
|
|
index a27c1c3b..1b4262ee 100644
|
|
|
|
|
--- a/src/singledocparser.cpp
|
|
|
|
|
+++ b/src/singledocparser.cpp
|
|
|
|
|
@@ -46,6 +46,8 @@ void SingleDocParser::HandleDocument(EventHandler& eventHandler) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
+ assert(depth < depth_limit);
|
|
|
|
|
+ depth++;
|
|
|
|
|
// an empty node *is* a possibility
|
|
|
|
|
if (m_scanner.empty()) {
|
|
|
|
|
eventHandler.OnNull(m_scanner.mark(), NullAnchor);
|
|
|
|
|
diff --git a/src/singledocparser.h b/src/singledocparser.h
|
|
|
|
|
index 2b92067c..7046f1e2 100644
|
|
|
|
|
--- a/src/singledocparser.h
|
|
|
|
|
+++ b/src/singledocparser.h
|
|
|
|
|
@@ -51,6 +51,8 @@ class SingleDocParser : private noncopyable {
|
|
|
|
|
anchor_t LookupAnchor(const Mark& mark, const std::string& name) const;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
+ int depth = 0;
|
|
|
|
|
+ int depth_limit = 2000;
|
|
|
|
|
Scanner& m_scanner;
|
|
|
|
|
const Directives& m_directives;
|
|
|
|
|
std::unique_ptr<CollectionStack> m_pCollectionStack;
|
|
|
|
|
|
|
|
|
|
From ac00ef937702598aa27739c8c46be37ac5699039 Mon Sep 17 00:00:00 2001
|
|
|
|
|
From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= <anarcat@debian.org>
|
|
|
|
|
Date: Wed, 26 Apr 2017 10:25:43 -0400
|
|
|
|
|
Subject: [PATCH 2/9] throw an exception instead of using assert()
|
|
|
|
|
|
|
|
|
|
assert() may be compiled out in production and is clunkier to catch.
|
|
|
|
|
|
|
|
|
|
some ParserException are already thrown elsewhere in the code and it
|
|
|
|
|
seems to make sense to reuse the primitive, although it may still
|
|
|
|
|
crash improperly configured library consumers, those who do not handle
|
|
|
|
|
exceptions explicitly.
|
|
|
|
|
|
|
|
|
|
we use the BAD_FILE error message because at this point we do not
|
|
|
|
|
exactly know which specific data structure led to the recursion.
|
|
|
|
|
---
|
|
|
|
|
src/singledocparser.cpp | 4 +++-
|
|
|
|
|
1 file changed, 3 insertions(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
diff --git a/src/singledocparser.cpp b/src/singledocparser.cpp
|
|
|
|
|
index 1b4262ee..1af13f49 100644
|
|
|
|
|
--- a/src/singledocparser.cpp
|
|
|
|
|
+++ b/src/singledocparser.cpp
|
|
|
|
|
@@ -46,7 +46,9 @@ void SingleDocParser::HandleDocument(EventHandler& eventHandler) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
- assert(depth < depth_limit);
|
|
|
|
|
+ if (depth > depth_limit) {
|
|
|
|
|
+ throw ParserException(m_scanner.mark(), ErrorMsg::BAD_FILE);
|
|
|
|
|
+ }
|
|
|
|
|
depth++;
|
|
|
|
|
// an empty node *is* a possibility
|
|
|
|
|
if (m_scanner.empty()) {
|
|
|
|
|
|
|
|
|
|
From e78e3bf6a6d61ca321af90d213dc4435ed5cf602 Mon Sep 17 00:00:00 2001
|
|
|
|
|
From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= <anarcat@debian.org>
|
|
|
|
|
Date: Wed, 26 Apr 2017 10:39:45 -0400
|
|
|
|
|
Subject: [PATCH 3/9] increase and decrease depth properly on subhandlers
|
|
|
|
|
|
|
|
|
|
the original implementation couldn't parse a document with more than
|
|
|
|
|
depth_limit entries. now we explicitly increase *and* decrease the
|
|
|
|
|
depth on specific handlers like maps, sequences and so on - any
|
|
|
|
|
handler that may in turn callback into HandleNode().
|
|
|
|
|
|
|
|
|
|
this is a little clunky - I would have prefered to increment and
|
|
|
|
|
decrement the counter in only one place, but there are many different
|
|
|
|
|
return points and this is not Golang so I can't think of a better way
|
|
|
|
|
to to this.
|
|
|
|
|
---
|
|
|
|
|
src/singledocparser.cpp | 13 ++++++++++++-
|
|
|
|
|
1 file changed, 12 insertions(+), 1 deletion(-)
|
|
|
|
|
|
|
|
|
|
diff --git a/src/singledocparser.cpp b/src/singledocparser.cpp
|
|
|
|
|
index 1af13f49..89234867 100644
|
|
|
|
|
--- a/src/singledocparser.cpp
|
|
|
|
|
+++ b/src/singledocparser.cpp
|
|
|
|
|
@@ -49,7 +49,6 @@ void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
if (depth > depth_limit) {
|
|
|
|
|
throw ParserException(m_scanner.mark(), ErrorMsg::BAD_FILE);
|
|
|
|
|
}
|
|
|
|
|
- depth++;
|
|
|
|
|
// an empty node *is* a possibility
|
|
|
|
|
if (m_scanner.empty()) {
|
|
|
|
|
eventHandler.OnNull(m_scanner.mark(), NullAnchor);
|
|
|
|
|
@@ -61,9 +60,11 @@ void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
|
|
|
|
|
// special case: a value node by itself must be a map, with no header
|
|
|
|
|
if (m_scanner.peek().type == Token::VALUE) {
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, "?", NullAnchor, EmitterStyle::Default);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -98,32 +99,42 @@ void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
m_scanner.pop();
|
|
|
|
|
return;
|
|
|
|
|
case Token::FLOW_SEQ_START:
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnSequenceStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleSequence(eventHandler);
|
|
|
|
|
eventHandler.OnSequenceEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::BLOCK_SEQ_START:
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnSequenceStart(mark, tag, anchor, EmitterStyle::Block);
|
|
|
|
|
HandleSequence(eventHandler);
|
|
|
|
|
eventHandler.OnSequenceEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::FLOW_MAP_START:
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::BLOCK_MAP_START:
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Block);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::KEY:
|
|
|
|
|
// compact maps can only go in a flow sequence
|
|
|
|
|
if (m_pCollectionStack->GetCurCollectionType() ==
|
|
|
|
|
CollectionType::FlowSeq) {
|
|
|
|
|
+ depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
+ depth--;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
From 1690cacb3ff6d927286ded92b8fedd37b4045c7c Mon Sep 17 00:00:00 2001
|
|
|
|
|
From: Keith Bennett <keithb@genebygene.com>
|
|
|
|
|
Date: Thu, 29 Mar 2018 16:45:11 -0500
|
|
|
|
|
Subject: [PATCH 4/9] use RAII type class to guard against stack depth
|
|
|
|
|
recursion instead of error-prone manual increment/check/decrement
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
include/yaml-cpp/depthguard.h | 74 +++++++++++++++++++++++++++++++++++
|
|
|
|
|
src/depthguard.cpp | 14 +++++++
|
|
|
|
|
src/singledocparser.cpp | 18 ++-------
|
|
|
|
|
src/singledocparser.h | 4 +-
|
|
|
|
|
4 files changed, 94 insertions(+), 16 deletions(-)
|
|
|
|
|
create mode 100644 include/yaml-cpp/depthguard.h
|
|
|
|
|
create mode 100644 src/depthguard.cpp
|
|
|
|
|
|
|
|
|
|
diff --git a/include/yaml-cpp/depthguard.h b/include/yaml-cpp/depthguard.h
|
|
|
|
|
new file mode 100644
|
|
|
|
|
index 00000000..6aac81aa
|
|
|
|
|
--- /dev/null
|
|
|
|
|
+++ b/include/yaml-cpp/depthguard.h
|
|
|
|
|
@@ -0,0 +1,74 @@
|
|
|
|
|
+#ifndef DEPTH_GUARD_H_00000000000000000000000000000000000000000000000000000000
|
|
|
|
|
+#define DEPTH_GUARD_H_00000000000000000000000000000000000000000000000000000000
|
|
|
|
|
+
|
|
|
|
|
+#if defined(_MSC_VER) || \
|
|
|
|
|
+ (defined(__GNUC__) && (__GNUC__ == 3 && __GNUC_MINOR__ >= 4) || \
|
|
|
|
|
+ (__GNUC__ >= 4)) // GCC supports "pragma once" correctly since 3.4
|
|
|
|
|
+#pragma once
|
|
|
|
|
+#endif
|
|
|
|
|
+
|
|
|
|
|
+#include "exceptions.h"
|
|
|
|
|
+
|
|
|
|
|
+namespace YAML {
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @brief The DeepRecursion class
|
|
|
|
|
+ * An exception class which is thrown by DepthGuard. Ideally it should be
|
|
|
|
|
+ * a member of DepthGuard. However, DepthGuard is a templated class which means
|
|
|
|
|
+ * that any catch points would then need to know the template parameters. It is
|
|
|
|
|
+ * simpler for clients to not have to know at the catch point what was the
|
|
|
|
|
+ * maximum depth.
|
|
|
|
|
+ */
|
|
|
|
|
+class DeepRecursion : public ParserException {
|
|
|
|
|
+ int m_atDepth = 0;
|
|
|
|
|
+public:
|
|
|
|
|
+ // no custom dtor needed, but virtual dtor necessary to prevent slicing
|
|
|
|
|
+ virtual ~DeepRecursion() = default;
|
|
|
|
|
+
|
|
|
|
|
+ // construct an exception explaining how deep you were
|
|
|
|
|
+ DeepRecursion(int at_depth, const Mark& mark_, const std::string& msg_);
|
|
|
|
|
+
|
|
|
|
|
+ // query how deep you were when the exception was thrown
|
|
|
|
|
+ int AtDepth() const;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @brief The DepthGuard class
|
|
|
|
|
+ * DepthGuard takes a reference to an integer. It increments the integer upon
|
|
|
|
|
+ * construction of DepthGuard and decrements the integer upon destruction.
|
|
|
|
|
+ *
|
|
|
|
|
+ * If the integer would be incremented past max_depth, then an exception is
|
|
|
|
|
+ * thrown. This is ideally geared toward guarding against deep recursion.
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param max_depth
|
|
|
|
|
+ * compile-time configurable maximum depth.
|
|
|
|
|
+ */
|
|
|
|
|
+template <int max_depth = 2000>
|
|
|
|
|
+class DepthGuard final /* final because non-virtual dtor */ {
|
|
|
|
|
+ int & m_depth;
|
|
|
|
|
+public:
|
|
|
|
|
+ DepthGuard(int & depth_, const Mark& mark_, const std::string& msg_) : m_depth(depth_) {
|
|
|
|
|
+ ++m_depth;
|
|
|
|
|
+ if ( max_depth <= m_depth ) {
|
|
|
|
|
+ throw DeepRecursion{m_depth, mark_, msg_};
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // DepthGuard is neither copyable nor moveable.
|
|
|
|
|
+ DepthGuard(const DepthGuard & copy_ctor) = delete;
|
|
|
|
|
+ DepthGuard(DepthGuard && move_ctor) = delete;
|
|
|
|
|
+ DepthGuard & operator=(const DepthGuard & copy_assign) = delete;
|
|
|
|
|
+ DepthGuard & operator=(DepthGuard && move_assign) = delete;
|
|
|
|
|
+
|
|
|
|
|
+ ~DepthGuard() {
|
|
|
|
|
+ --m_depth;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int current_depth() const {
|
|
|
|
|
+ return m_depth;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+} // namespace YAML
|
|
|
|
|
+
|
|
|
|
|
+#endif // DEPTH_GUARD_H_00000000000000000000000000000000000000000000000000000000
|
|
|
|
|
diff --git a/src/depthguard.cpp b/src/depthguard.cpp
|
|
|
|
|
new file mode 100644
|
|
|
|
|
index 00000000..6d47eba3
|
|
|
|
|
--- /dev/null
|
|
|
|
|
+++ b/src/depthguard.cpp
|
|
|
|
|
@@ -0,0 +1,14 @@
|
|
|
|
|
+#include "yaml-cpp/depthguard.h"
|
|
|
|
|
+
|
|
|
|
|
+namespace YAML {
|
|
|
|
|
+
|
|
|
|
|
+DeepRecursion::DeepRecursion(int at_depth, const Mark& mark_, const std::string& msg_)
|
|
|
|
|
+ : ParserException(mark_, msg_),
|
|
|
|
|
+ m_atDepth(at_depth) {
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+int DeepRecursion::AtDepth() const {
|
|
|
|
|
+ return m_atDepth;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace YAML
|
|
|
|
|
diff --git a/src/singledocparser.cpp b/src/singledocparser.cpp
|
|
|
|
|
index 89234867..37cc1f51 100644
|
|
|
|
|
--- a/src/singledocparser.cpp
|
|
|
|
|
+++ b/src/singledocparser.cpp
|
|
|
|
|
@@ -7,6 +7,7 @@
|
|
|
|
|
#include "singledocparser.h"
|
|
|
|
|
#include "tag.h"
|
|
|
|
|
#include "token.h"
|
|
|
|
|
+#include "yaml-cpp/depthguard.h"
|
|
|
|
|
#include "yaml-cpp/emitterstyle.h"
|
|
|
|
|
#include "yaml-cpp/eventhandler.h"
|
|
|
|
|
#include "yaml-cpp/exceptions.h" // IWYU pragma: keep
|
|
|
|
|
@@ -46,9 +47,8 @@ void SingleDocParser::HandleDocument(EventHandler& eventHandler) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
- if (depth > depth_limit) {
|
|
|
|
|
- throw ParserException(m_scanner.mark(), ErrorMsg::BAD_FILE);
|
|
|
|
|
- }
|
|
|
|
|
+ DepthGuard depthguard(depth, m_scanner.mark(), ErrorMsg::BAD_FILE);
|
|
|
|
|
+
|
|
|
|
|
// an empty node *is* a possibility
|
|
|
|
|
if (m_scanner.empty()) {
|
|
|
|
|
eventHandler.OnNull(m_scanner.mark(), NullAnchor);
|
|
|
|
|
@@ -60,11 +60,9 @@ void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
|
|
|
|
|
// special case: a value node by itself must be a map, with no header
|
|
|
|
|
if (m_scanner.peek().type == Token::VALUE) {
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, "?", NullAnchor, EmitterStyle::Default);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -99,42 +97,32 @@ void SingleDocParser::HandleNode(EventHandler& eventHandler) {
|
|
|
|
|
m_scanner.pop();
|
|
|
|
|
return;
|
|
|
|
|
case Token::FLOW_SEQ_START:
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnSequenceStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleSequence(eventHandler);
|
|
|
|
|
eventHandler.OnSequenceEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::BLOCK_SEQ_START:
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnSequenceStart(mark, tag, anchor, EmitterStyle::Block);
|
|
|
|
|
HandleSequence(eventHandler);
|
|
|
|
|
eventHandler.OnSequenceEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::FLOW_MAP_START:
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::BLOCK_MAP_START:
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Block);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
case Token::KEY:
|
|
|
|
|
// compact maps can only go in a flow sequence
|
|
|
|
|
if (m_pCollectionStack->GetCurCollectionType() ==
|
|
|
|
|
CollectionType::FlowSeq) {
|
|
|
|
|
- depth++;
|
|
|
|
|
eventHandler.OnMapStart(mark, tag, anchor, EmitterStyle::Flow);
|
|
|
|
|
HandleMap(eventHandler);
|
|
|
|
|
eventHandler.OnMapEnd();
|
|
|
|
|
- depth--;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
diff --git a/src/singledocparser.h b/src/singledocparser.h
|
|
|
|
|
index 7046f1e2..f1676c43 100644
|
|
|
|
|
--- a/src/singledocparser.h
|
|
|
|
|
+++ b/src/singledocparser.h
|
|
|
|
|
@@ -16,6 +16,8 @@
|
|
|
|
|
|
|
|
|
|
namespace YAML {
|
|
|
|
|
class CollectionStack;
|
|
|
|
|
+template <int> class DepthGuard; // depthguard.h
|
|
|
|
|
+class DeepRecursion; // an exception which may be thrown from excessive call stack recursion, see depthguard.h
|
|
|
|
|
class EventHandler;
|
|
|
|
|
class Node;
|
|
|
|
|
class Scanner;
|
|
|
|
|
@@ -51,8 +53,8 @@ class SingleDocParser : private noncopyable {
|
|
|
|
|
anchor_t LookupAnchor(const Mark& mark, const std::string& name) const;
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
+ using DepthGuard = YAML::DepthGuard<2000>;
|
|
|
|
|
int depth = 0;
|
|
|
|
|
- int depth_limit = 2000;
|
|
|
|
|
Scanner& m_scanner;
|
|
|
|
|
const Directives& m_directives;
|
|
|
|
|
std::unique_ptr<CollectionStack> m_pCollectionStack;
|
|
|
|
|
|