bej_decoder: Add a limit on number of operations

This is to prevent the decoder from taking on a huge number of
operations from a single input, that could cause the main while loop for
bejDecode to run for a long time.

Tested: Unit test passes

Change-Id: Idb68b706d3634d1874d1814f17420076672d21ee
Signed-off-by: Brandon Kim <brandonkim@google.com>
diff --git a/src/bej_decoder_core.c b/src/bej_decoder_core.c
index a9bb02c..b500fcf 100644
--- a/src/bej_decoder_core.c
+++ b/src/bej_decoder_core.c
@@ -719,8 +719,16 @@
         .stackDataPtr = stackDataPtr,
     };
 
+    uint64_t maxOperations = 1000000;
+    uint64_t operationCount = 0;
+
     while (params.state.encodedStreamOffset < streamLen)
     {
+        if (++operationCount > maxOperations)
+        {
+            fprintf(stderr, "BEJ decoding exceeded max operations\n");
+            return bejErrorNotSuppoted;
+        }
         // Go to the next encoded segment in the encoded stream.
         params.state.encodedSubStream =
             enStream + params.state.encodedStreamOffset;
diff --git a/test/bej_decoder_test.cpp b/test/bej_decoder_test.cpp
index 55b4358..b229043 100644
--- a/test/bej_decoder_test.cpp
+++ b/test/bej_decoder_test.cpp
@@ -1,8 +1,10 @@
 #include "bej_common_test.hpp"
 #include "bej_decoder_json.hpp"
+#include "bej_encoder_json.hpp"
 
 #include <memory>
 #include <string_view>
+#include <vector>
 
 #include <gmock/gmock-matchers.h>
 #include <gmock/gmock.h>
@@ -80,6 +82,65 @@
     EXPECT_TRUE(jsonDecoded.dump() == inputsOrErr->expectedJson.dump());
 }
 
+TEST(BejDecoderSecurityTest, MaxOperationsLimit)
+{
+    auto inputsOrErr = loadInputs(dummySimpleTestFiles);
+    ASSERT_TRUE(inputsOrErr);
+
+    BejDictionaries dictionaries = {
+        .schemaDictionary = inputsOrErr->schemaDictionary,
+        .annotationDictionary = inputsOrErr->annotationDictionary,
+        .errorDictionary = inputsOrErr->errorDictionary,
+    };
+
+    // Each array element below consists of a set and two properties, resulting
+    // in 3 operations. 400,000 elements will result in 1,200,000 operations,
+    // which should exceed the limit of 1,000,000.
+    constexpr int numElements = 400000;
+
+    auto root = std::make_unique<RedfishPropertyParent>();
+    bejTreeInitSet(root.get(), "DummySimple");
+
+    auto childArray = std::make_unique<RedfishPropertyParent>();
+    bejTreeInitArray(childArray.get(), "ChildArrayProperty");
+    bejTreeLinkChildToParent(root.get(), childArray.get());
+
+    std::vector<std::unique_ptr<RedfishPropertyParent>> sets;
+    std::vector<std::unique_ptr<RedfishPropertyLeafBool>> bools;
+    std::vector<std::unique_ptr<RedfishPropertyLeafEnum>> enums;
+    sets.reserve(numElements);
+    bools.reserve(numElements);
+    enums.reserve(numElements);
+
+    for (int i = 0; i < numElements; ++i)
+    {
+        auto chArraySet = std::make_unique<RedfishPropertyParent>();
+        bejTreeInitSet(chArraySet.get(), nullptr);
+
+        auto chArraySetBool = std::make_unique<RedfishPropertyLeafBool>();
+        bejTreeAddBool(chArraySet.get(), chArraySetBool.get(), "AnotherBoolean",
+                       true);
+
+        auto chArraySetLs = std::make_unique<RedfishPropertyLeafEnum>();
+        bejTreeAddEnum(chArraySet.get(), chArraySetLs.get(), "LinkStatus",
+                       "NoLink");
+
+        bejTreeLinkChildToParent(childArray.get(), chArraySet.get());
+
+        sets.push_back(std::move(chArraySet));
+        bools.push_back(std::move(chArraySetBool));
+        enums.push_back(std::move(chArraySetLs));
+    }
+
+    libbej::BejEncoderJson encoder;
+    encoder.encode(&dictionaries, bejMajorSchemaClass, root.get());
+    std::vector<uint8_t> outputBuffer = encoder.getOutput();
+
+    BejDecoderJson decoder;
+    EXPECT_THAT(decoder.decode(dictionaries, std::span(outputBuffer)),
+                bejErrorNotSuppoted);
+}
+
 /**
  * TODO: Add more test cases.
  * - Test Enums inside array elemets