CVE-2015-2313: Uncontrolled Resource Consumption (CWE-400)
value: Sandstorm Cap'n Proto before 0.4.1.1 and 0.5.x before 0.5.1.2, when an application invokes the totalSize method on an object reader, allows remote peers to cause a denial of service (CPU consumption) via a crafted small message, which triggers a "tight" for loop.  NOTE: this vulnerability exists because of an incomplete fix for CVE-2015-2312.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2017-08-09T18:29Z
Repository: https://github.com/capnproto/capnproto
Language: Other
Files changed: c++/src/capnp/encoding-test.c++, c++/src/capnp/layout.c++
Commit date: 2015-03-05 10:27:29 -0800
Commit message: SECURITY: Additional CPU amplification case.

Unfortunately, commit 104870608fde3c698483fdef6b97f093fc15685d missed a case of CPU amplification via struct lists with zero-sized elements.

See advisory: https://github.com/sandstorm-io/capnproto/blob/master/security-advisories/2015-03-05-0-c++-addl-cpu-amplification.md

Diff: diff --git a/c++/src/capnp/encoding-test.c++ b/c++/src/capnp/encoding-test.c++
index 6bf07adf..360d0e0f 100644
--- a/c++/src/capnp/encoding-test.c++
+++ b/c++/src/capnp/encoding-test.c++
@@ -1425,21 +1425,28 @@ TEST(Encoding, VoidListAmplification) {
   MallocMessageBuilder copy;
   EXPECT_NONFATAL_FAILURE(copy.setRoot(reader.getRoot<AnyPointer>()));
 }
 
 TEST(Encoding, EmptyStructListAmplification) {
-  MallocMessageBuilder builder;
-  builder.initRoot<test::TestAnyPointer>().getAnyPointerField()
-      .initAs<List<test::TestEmptyStruct>>(1u << 28);
+  MallocMessageBuilder builder(1024);
+  auto listList = builder.initRoot<test::TestAnyPointer>().getAnyPointerField()
+      .initAs<List<List<test::TestEmptyStruct>>>(500);
+
+  for (uint i = 0; i < listList.size(); i++) {
+    listList.init(i, 1u << 28);
+  }
 
   auto segments = builder.getSegmentsForOutput();
-  EXPECT_EQ(1, segments.size());
-  EXPECT_LT(segments[0].size(), 16);  // quite small for such a big list!
+  ASSERT_EQ(1, segments.size());
 
   SegmentArrayMessageReader reader(builder.getSegmentsForOutput());
-  auto root = reader.getRoot<test::TestAnyPointer>().getAnyPointerField();
-  EXPECT_NONFATAL_FAILURE(root.getAs<List<TestAllTypes>>());
+  auto root = reader.getRoot<test::TestAnyPointer>();
+  auto listListReader = root.getAnyPointerField().getAs<List<List<TestAllTypes>>>();
+  EXPECT_NONFATAL_FAILURE(listListReader[0]);
+  EXPECT_NONFATAL_FAILURE(listListReader[10]);
+
+  EXPECT_EQ(segments[0].size() - 1, root.totalSize().wordCount);
 }
 
 TEST(Encoding, Constants) {
   EXPECT_EQ(VOID, test::TestConstants::VOID_CONST);
   EXPECT_EQ(true, test::TestConstants::BOOL_CONST);
diff --git a/c++/src/capnp/layout.c++ b/c++/src/capnp/layout.c++
index de6ee5b3..2c7d593e 100644
--- a/c++/src/capnp/layout.c++
+++ b/c++/src/capnp/layout.c++
@@ -553,18 +553,20 @@ struct WireHelpers {
             KJ_ASSERT(elementTag->kind() == WirePointer::STRUCT,
                   "Don't know how to handle non-STRUCT inline composite.");
             WordCount dataSize = elementTag->structRef.dataSize.get();
             WirePointerCount pointerCount = elementTag->structRef.ptrCount.get();
 
-            word* pos = ptr + POINTER_SIZE_IN_WORDS;
             uint count = elementTag->inlineCompositeListElementCount() / ELEMENTS;
-            for (uint i = 0; i < count; i++) {
-              pos += dataSize;
-
-              for (uint j = 0; j < pointerCount / POINTERS; j++) {
-                zeroObject(segment, reinterpret_cast<WirePointer*>(pos));
-                pos += POINTER_SIZE_IN_WORDS;
+            if (pointerCount > 0 * POINTERS) {
+              word* pos = ptr + POINTER_SIZE_IN_WORDS;
+              for (uint i = 0; i < count; i++) {
+                pos += dataSize;
+
+                for (uint j = 0; j < pointerCount / POINTERS; j++) {
+                  zeroObject(segment, reinterpret_cast<WirePointer*>(pos));
+                  pos += POINTER_SIZE_IN_WORDS;
+                }
               }
             }
 
             memset(ptr, 0, (elementTag->structRef.wordSize() * count + POINTER_SIZE_IN_WORDS)
                            * BYTES_PER_WORD / BYTES);
@@ -678,37 +680,41 @@ struct WireHelpers {
             KJ_REQUIRE(boundsCheck(segment, ptr, ptr + wordCount + POINTER_SIZE_IN_WORDS),
                        "Message contained out-of-bounds list pointer.") {
               return result;
             }
 
-            result.wordCount += wordCount + POINTER_SIZE_IN_WORDS;
-
             const WirePointer* elementTag = reinterpret_cast<const WirePointer*>(ptr);
             ElementCount count = elementTag->inlineCompositeListElementCount();
 
             KJ_REQUIRE(elementTag->kind() == WirePointer::STRUCT,
                        "Don't know how to handle non-STRUCT inline composite.") {
               return result;
             }
 
-            KJ_REQUIRE(elementTag->structRef.wordSize() / ELEMENTS *
-                       ElementCount64(count) <= wordCount,
+            auto actualSize = elementTag->structRef.wordSize() / ELEMENTS * ElementCount64(count);
+            KJ_REQUIRE(actualSize <= wordCount,
                        "Struct list pointer's elements overran size.") {
               return result;
             }
 
+            // We count the actual size rather than the claimed word count because that's what
+            // we'll end up with if we make a copy.
+            result.wordCount += actualSize + POINTER_SIZE_IN_WORDS;
+
             WordCount dataSize = elementTag->structRef.dataSize.get();
             WirePointerCount pointerCount = elementTag->structRef.ptrCount.get();
 
-            const word* pos = ptr + POINTER_SIZE_IN_WORDS;
-            for (uint i = 0; i < count / ELEMENTS; i++) {
-              pos += dataSize;
+            if (pointerCount > 0 * POINTERS) {
+              const word* pos = ptr + POINTER_SIZE_IN_WORDS;
+              for (uint i = 0; i < count / ELEMENTS; i++) {
+                pos += dataSize;
 
-              for (uint j = 0; j < pointerCount / POINTERS; j++) {
-                result += totalSize(segment, reinterpret_cast<const WirePointer*>(pos),
-                                    nestingLimit);
-                pos += POINTER_SIZE_IN_WORDS;
+                for (uint j = 0; j < pointerCount / POINTERS; j++) {
+                  result += totalSize(segment, reinterpret_cast<const WirePointer*>(pos),
+                                      nestingLimit);
+                  pos += POINTER_SIZE_IN_WORDS;
+                }
               }
             }
             break;
           }
         }

Vulnerable code:   MallocMessageBuilder builder;
  builder.initRoot<test::TestAnyPointer>().getAnyPointerField()
      .initAs<List<test::TestEmptyStruct>>(1u << 28);
  EXPECT_EQ(1, segments.size());
  EXPECT_LT(segments[0].size(), 16);  // quite small for such a big list!
  auto root = reader.getRoot<test::TestAnyPointer>().getAnyPointerField();
  EXPECT_NONFATAL_FAILURE(root.getAs<List<TestAllTypes>>());
            word* pos = ptr + POINTER_SIZE_IN_WORDS;
            for (uint i = 0; i < count; i++) {
              pos += dataSize;

              for (uint j = 0; j < pointerCount / POINTERS; j++) {
                zeroObject(segment, reinterpret_cast<WirePointer*>(pos));
                pos += POINTER_SIZE_IN_WORDS;
            result.wordCount += wordCount + POINTER_SIZE_IN_WORDS;

            KJ_REQUIRE(elementTag->structRef.wordSize() / ELEMENTS *
                       ElementCount64(count) <= wordCount,
            const word* pos = ptr + POINTER_SIZE_IN_WORDS;
            for (uint i = 0; i < count / ELEMENTS; i++) {
              pos += dataSize;
              for (uint j = 0; j < pointerCount / POINTERS; j++) {
                result += totalSize(segment, reinterpret_cast<const WirePointer*>(pos),
                                    nestingLimit);
                pos += POINTER_SIZE_IN_WORDS;
Fixed code:   MallocMessageBuilder builder(1024);
  auto listList = builder.initRoot<test::TestAnyPointer>().getAnyPointerField()
      .initAs<List<List<test::TestEmptyStruct>>>(500);

  for (uint i = 0; i < listList.size(); i++) {
    listList.init(i, 1u << 28);
  }
  ASSERT_EQ(1, segments.size());
  auto root = reader.getRoot<test::TestAnyPointer>();
  auto listListReader = root.getAnyPointerField().getAs<List<List<TestAllTypes>>>();
  EXPECT_NONFATAL_FAILURE(listListReader[0]);
  EXPECT_NONFATAL_FAILURE(listListReader[10]);

  EXPECT_EQ(segments[0].size() - 1, root.totalSize().wordCount);
            if (pointerCount > 0 * POINTERS) {
              word* pos = ptr + POINTER_SIZE_IN_WORDS;
              for (uint i = 0; i < count; i++) {
                pos += dataSize;

                for (uint j = 0; j < pointerCount / POINTERS; j++) {
                  zeroObject(segment, reinterpret_cast<WirePointer*>(pos));
                  pos += POINTER_SIZE_IN_WORDS;
                }
            auto actualSize = elementTag->structRef.wordSize() / ELEMENTS * ElementCount64(count);
            KJ_REQUIRE(actualSize <= wordCount,
            // We count the actual size rather than the claimed word count because that's what
            // we'll end up with if we make a copy.
            result.wordCount += actualSize + POINTER_SIZE_IN_WORDS;

            if (pointerCount > 0 * POINTERS) {
              const word* pos = ptr + POINTER_SIZE_IN_WORDS;
              for (uint i = 0; i < count / ELEMENTS; i++) {
                pos += dataSize;
                for (uint j = 0; j < pointerCount / POINTERS; j++) {
                  result += totalSize(segment, reinterpret_cast<const WirePointer*>(pos),
                                      nestingLimit);
                  pos += POINTER_SIZE_IN_WORDS;
                }