This is an automated email from the ASF dual-hosted git repository.

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new c74c9576f7d GH-51757: [C++] Limit max JSON nesting depth in JSON 
parser (#51758)
c74c9576f7d is described below

commit c74c9576f7d37439fcb23ce0724b501f0c71cb9e
Author: Antoine Pitrou <[email protected]>
AuthorDate: Wed Oct 7 10:14:05 2026 +0200

    GH-51757: [C++] Limit max JSON nesting depth in JSON parser (#51758)
    
    ### Rationale for this change
    
    Our JSON parser is inherently recursive, and a crafted JSON document with 
huge nesting can send it into a stack overflow.
    
    Given that real-world JSON files will not have thousands levels of nesting, 
we limit the JSON nesting to an arbitrary value of 300.
    
    ### Are these changes tested?
    
    Yes, by new tests.
    
    ### Are there any user-facing changes?
    
    **This PR contains a "Critical Fix".** A crafted JSON document with a large 
nesting depth could trigger a denial of service through stack overflow.
    
    ### Was AI used for this PR?
    
    **PR code and description written by:**
    
    - [x] Human
    - [ ] AI
    
    **Reviewed before submission by:**
    
    - [x] Human (self-reviewed)
    - [ ] AI
    - [ ] Not reviewed
    
    * GitHub Issue: #51757
    
    Authored-by: Antoine Pitrou <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/json/parser.cc                     | 109 +++++++++++++----------
 cpp/src/arrow/json/parser_test.cc                |  58 ++++++++++++
 cpp/src/arrow/util/simdjson_internal.cc          |  48 ++++++----
 cpp/src/arrow/util/simdjson_internal.h           |   9 +-
 cpp/src/parquet/geospatial/util_json_internal.cc |   8 +-
 cpp/src/parquet/reader_test.cc                   |   3 +-
 python/pyarrow/tests/test_json.py                |  27 ++++++
 7 files changed, 188 insertions(+), 74 deletions(-)

diff --git a/cpp/src/arrow/json/parser.cc b/cpp/src/arrow/json/parser.cc
index 2d2363fe431..69373d07fb6 100644
--- a/cpp/src/arrow/json/parser.cc
+++ b/cpp/src/arrow/json/parser.cc
@@ -17,7 +17,6 @@
 
 #include "arrow/json/parser.h"
 
-#include <functional>
 #include <limits>
 #include <memory>
 #include <string_view>
@@ -47,11 +46,6 @@ namespace json {
 
 namespace sj = simdjson::ondemand;
 
-template <typename... T>
-static Status ParseError(T&&... t) {
-  return Status::Invalid("JSON parse error: ", std::forward<T>(t)...);
-}
-
 const std::string& Kind::Name(Kind::type kind) {
   static const std::string names[] = {
       "null", "boolean", "number", "string", "array", "object", 
"number_or_string",
@@ -121,6 +115,18 @@ Status Kind::ForType(const DataType& type, Kind::type* 
kind) {
   return VisitTypeInline(type, &visitor);
 }
 
+namespace {
+
+// Tuned to not crash on any of our CI platforms in release mode.
+// This may seem low but some platforms have very low thread stack sizes
+// (128 kiB on musllinux).
+constexpr int kMaxNestingDepth = 100;
+
+template <typename... T>
+Status ParseError(T&&... t) {
+  return Status::Invalid("JSON parse error: ", std::forward<T>(t)...);
+}
+
 /// \brief ArrayBuilder for parsed but unconverted arrays
 template <Kind::type>
 class RawArrayBuilder;
@@ -305,20 +311,17 @@ class RawArrayBuilder<Kind::kArray> {
     return null_bitmap_builder_.Append(count, false);
   }
 
-  Status Finish(std::function<Status(BuilderPtr, std::shared_ptr<Array>*)> 
finish_child,
-                std::shared_ptr<Array>* out) {
+  Status Finish(std::shared_ptr<Array> child_values, std::shared_ptr<Array>* 
out) {
     RETURN_NOT_OK(offset_builder_.Append(offset_));
     auto size = length();
     auto null_count = null_bitmap_builder_.false_count();
     std::shared_ptr<Buffer> offsets, null_bitmap;
     RETURN_NOT_OK(offset_builder_.Finish(&offsets));
     RETURN_NOT_OK(null_bitmap_builder_.Finish(&null_bitmap));
-    std::shared_ptr<Array> values;
-    RETURN_NOT_OK(finish_child(value_builder_, &values));
-    auto type = list(field("item", values->type(), value_builder_.nullable,
+    auto type = list(field("item", child_values->type(), 
value_builder_.nullable,
                            Kind::Tag(value_builder_.kind)));
-    *out = MakeArray(ArrayData::Make(type, size, {null_bitmap, offsets}, 
{values->data()},
-                                     null_count));
+    *out = MakeArray(ArrayData::Make(type, size, {null_bitmap, offsets},
+                                     {child_values->data()}, null_count));
     return Status::OK();
   }
 
@@ -404,21 +407,18 @@ class RawArrayBuilder<Kind::kObject> {
     field_infos_[index].builder = builder;
   }
 
-  Status Finish(std::function<Status(BuilderPtr, std::shared_ptr<Array>*)> 
finish_child,
+  Status Finish(std::vector<std::shared_ptr<ArrayData>> child_data,
                 std::shared_ptr<Array>* out) {
     auto size = length();
     auto null_count = null_bitmap_builder_.false_count();
     std::shared_ptr<Buffer> null_bitmap;
     RETURN_NOT_OK(null_bitmap_builder_.Finish(&null_bitmap));
 
+    DCHECK_EQ(child_data.size(), static_cast<size_t>(num_fields()));
     std::vector<std::shared_ptr<Field>> fields(num_fields());
-    std::vector<std::shared_ptr<ArrayData>> child_data(num_fields());
     for (int i = 0; i < num_fields(); ++i) {
       const auto& info = field_infos_[i];
-      std::shared_ptr<Array> field_values;
-      RETURN_NOT_OK(finish_child(info.builder, &field_values));
-      child_data[i] = field_values->data();
-      fields[i] = field(std::string(info.name), field_values->type(),
+      fields[i] = field(std::string(info.name), child_data[i]->type,
                         info.builder.nullable, Kind::Tag(info.builder.kind));
     }
 
@@ -581,10 +581,6 @@ class RawBuilderSet {
 
   Status Finish(const std::shared_ptr<Array>& scalar_values, BuilderPtr 
builder,
                 std::shared_ptr<Array>* out) {
-    auto finish_children = [this, &scalar_values](BuilderPtr child,
-                                                  std::shared_ptr<Array>* out) 
{
-      return Finish(scalar_values, child, out);
-    };
     switch (builder.kind) {
       case Kind::kNull: {
         auto length = static_cast<int64_t>(builder.index);
@@ -603,11 +599,25 @@ class RawBuilderSet {
       case Kind::kNumberOrString:
         return FinishScalar(scalar_values, 
Cast<Kind::kNumberOrString>(builder), out);
 
-      case Kind::kArray:
-        return Cast<Kind::kArray>(builder)->Finish(std::move(finish_children), 
out);
+      case Kind::kArray: {
+        auto array_builder = Cast<Kind::kArray>(builder);
+        auto child_builder = array_builder->value_builder();
+        std::shared_ptr<Array> child_values;
+        RETURN_NOT_OK(Finish(scalar_values, child_builder, &child_values));
+        return array_builder->Finish(std::move(child_values), out);
+      }
 
-      case Kind::kObject:
-        return 
Cast<Kind::kObject>(builder)->Finish(std::move(finish_children), out);
+      case Kind::kObject: {
+        auto object_builder = Cast<Kind::kObject>(builder);
+        std::vector<std::shared_ptr<ArrayData>> 
child_data(object_builder->num_fields());
+        for (int i = 0; i < object_builder->num_fields(); ++i) {
+          auto child_builder = object_builder->field_builder(i);
+          std::shared_ptr<Array> child_values;
+          RETURN_NOT_OK(Finish(scalar_values, child_builder, &child_values));
+          child_data[i] = child_values->data();
+        }
+        return object_builder->Finish(std::move(child_data), out);
+      }
 
       default:
         return Status::NotImplemented("invalid builder kind");
@@ -662,13 +672,15 @@ class ParseImpl : public BlockParser {
     return builder_set_.AppendNull(builder_stack_.back(), field_index_, 
builder_);
   }
 
-  Status HandleUnexpectedField(std::string_view key, sj::value value) {
+  Status HandleUnexpectedField(std::string_view key, sj::value& value) {
     switch (unexpected_field_behavior_) {
       case UnexpectedFieldBehavior::Error:
         return ParseError("unexpected field");
 
       case UnexpectedFieldBehavior::Ignore:
-        return internal::ConsumeJsonValue(value);
+        return internal::ValidateJsonValue(
+            std::move(value), kMaxNestingDepth,
+            /*depth=*/static_cast<int>(builder_stack_.size() - 1));
 
       case UnexpectedFieldBehavior::InferType: {
         // If an unexpected field is encountered, add a NullBuilder with 
leading nulls.
@@ -833,7 +845,7 @@ class ParseImpl : public BlockParser {
     return Status::OK();
   }
 
-  Status ParseValue(sj::value value) {
+  Status ParseValue(sj::value& value) {
     ARROW_ASSIGN_OR_RAISE(auto type, arrow::internal::ResolveSimdjsonResult(
                                          value.type(), "Failed to determine 
JSON type"));
 
@@ -887,25 +899,22 @@ class ParseImpl : public BlockParser {
     return Status::OK();
   }
 
-  Status ParseArray(sj::value value) {
+  Status ParseArray(sj::value& value) {
     constexpr auto kind = Kind::kArray;
     if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
       return IllegallyChangedTo(kind);
     }
 
-    StartNested();
-
+    RETURN_NOT_OK(StartNested());
     builder_ = Cast<kind>(builder_)->value_builder();
 
     ARROW_ASSIGN_OR_RAISE(auto array, arrow::internal::ResolveSimdjsonResult(
                                           value.get_array(), "Failed to get 
JSON array"));
-
     int64_t size = 0;
-
     for (auto element_result : array) {
-      ARROW_ASSIGN_OR_RAISE(auto element,
-                            arrow::internal::ResolveSimdjsonResult(
-                                element_result, "Failed to iterate JSON 
array"));
+      ARROW_ASSIGN_OR_RAISE(
+          auto element, arrow::internal::ResolveSimdjsonResult(
+                            std::move(element_result), "Failed to iterate JSON 
array"));
 
       RETURN_NOT_OK(ParseValue(element));
       ++size;
@@ -918,7 +927,7 @@ class ParseImpl : public BlockParser {
     return list_builder->Append(static_cast<int32_t>(size));
   }
 
-  Status ParseObject(sj::value value) {
+  Status ParseObject(sj::value& value) {
     constexpr auto kind = Kind::kObject;
     if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
       return IllegallyChangedTo(kind);
@@ -926,7 +935,7 @@ class ParseImpl : public BlockParser {
 
     auto struct_builder = Cast<kind>(builder_);
     absent_fields_stack_.Push(struct_builder->num_fields(), true);
-    StartNested();
+    RETURN_NOT_OK(StartNested());
     RETURN_NOT_OK(struct_builder->Append());
 
     ARROW_ASSIGN_OR_RAISE(
@@ -934,10 +943,10 @@ class ParseImpl : public BlockParser {
                                                             "Failed to get 
JSON object"));
 
     for (auto field_result : object) {
-      ARROW_ASSIGN_OR_RAISE(
-          auto field,
-          arrow::internal::ResolveSimdjsonResult(
-              field_result, "JSON parse error: Failed to iterate JSON 
object"));
+      ARROW_ASSIGN_OR_RAISE(auto field,
+                            arrow::internal::ResolveSimdjsonResult(
+                                std::move(field_result),
+                                "JSON parse error: Failed to iterate JSON 
object"));
 
       ARROW_ASSIGN_OR_RAISE(auto key,
                             arrow::internal::ResolveSimdjsonResult(
@@ -964,7 +973,7 @@ class ParseImpl : public BlockParser {
     return Status::OK();
   }
 
-  Status ParseObjectField(std::string_view key, sj::value value) {
+  Status ParseObjectField(std::string_view key, sj::value& value) {
     auto parent = Cast<Kind::kObject>(builder_stack_.back());
     field_index_ = parent->GetFieldIndex(key);
     if (ARROW_PREDICT_FALSE(field_index_ == -1)) {
@@ -1002,10 +1011,16 @@ class ParseImpl : public BlockParser {
   /// helper method for ParseArray and ParseObject
   /// adds the current builder to a stack so its
   /// children can be visited and parsed.
-  void StartNested() {
+  Status StartNested() {
+    if (ARROW_PREDICT_FALSE(builder_stack_.size() >=
+                            static_cast<size_t>(kMaxNestingDepth))) {
+      return Status::Invalid("JSON too deeply nested: max nesting depth is ",
+                             kMaxNestingDepth);
+    }
     field_index_stack_.push_back(field_index_);
     field_index_ = -1;
     builder_stack_.push_back(builder_);
+    return Status::OK();
   }
 
   /// helper method for EndArray and EndObject
@@ -1049,6 +1064,8 @@ class ParseImpl : public BlockParser {
   sj::parser parser_;
 };
 
+}  // namespace
+
 Status BlockParser::Make(MemoryPool* pool, const ParseOptions& options,
                          std::unique_ptr<BlockParser>* out) {
   DCHECK(options.unexpected_field_behavior == 
UnexpectedFieldBehavior::InferType ||
diff --git a/cpp/src/arrow/json/parser_test.cc 
b/cpp/src/arrow/json/parser_test.cc
index 28c54b3c5cd..6c32fd7cab3 100644
--- a/cpp/src/arrow/json/parser_test.cc
+++ b/cpp/src/arrow/json/parser_test.cc
@@ -20,6 +20,7 @@
 #include <gmock/gmock-matchers.h>
 #include <gtest/gtest.h>
 
+#include <sstream>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -355,5 +356,62 @@ TEST(BlockParser, NullTypeRejectsNonNullUnderError) {
   ASSERT_RAISES(Invalid, ParseFromString(options, R"({"a": 5})", &parsed));
 }
 
+TEST(BlockParser, NestingDepth) {
+  auto deeply_nested_json_object = [](int depth) {
+    std::stringstream ss;
+    for (int i = 0; i < depth; ++i) {
+      ss << "{\"a\":";
+    }
+    ss << "1";
+    for (int i = 0; i < depth; ++i) {
+      ss << "}";
+    }
+    return std::move(ss).str();
+  };
+
+  auto deeply_nested_json_array = [](int depth) {
+    std::stringstream ss;
+    ss << "{\"a\":";
+    for (int i = 0; i < depth - 1; ++i) {
+      ss << "[";
+    }
+    ss << "1";
+    for (int i = 0; i < depth - 1; ++i) {
+      ss << "]";
+    }
+    ss << "}";
+    return std::move(ss).str();
+  };
+
+  const int kMaxDepth = 100;  // hard-coded in parser.cc
+  ParseOptions options = ParseOptions::Defaults();
+  std::shared_ptr<Array> parsed;
+
+  for (UnexpectedFieldBehavior unexpected_field_behavior :
+       {UnexpectedFieldBehavior::Ignore, UnexpectedFieldBehavior::InferType}) {
+    options.unexpected_field_behavior = unexpected_field_behavior;
+    options.explicit_schema = schema({{"not_here", int32()}});
+    ASSERT_OK(ParseFromString(options, deeply_nested_json_object(kMaxDepth), 
&parsed));
+    ASSERT_OK(parsed->ValidateFull());
+    ASSERT_OK(ParseFromString(options, deeply_nested_json_array(kMaxDepth), 
&parsed));
+    ASSERT_OK(parsed->ValidateFull());
+
+    // `kMaxDepth + 1` is the first value that triggers an error (but wouldn't 
trigger
+    // a stack overflow otherwise).
+    // 100'000 would definitely trigger a stack overflow, validate that the 
error is
+    // detected before that would happen.
+    for (int depth : {kMaxDepth + 1, 100'000}) {
+      EXPECT_RAISES_WITH_MESSAGE_THAT(
+          Invalid,
+          ::testing::HasSubstr("JSON too deeply nested: max nesting depth is 
100"),
+          ParseFromString(options, deeply_nested_json_object(depth), &parsed));
+      EXPECT_RAISES_WITH_MESSAGE_THAT(
+          Invalid,
+          ::testing::HasSubstr("JSON too deeply nested: max nesting depth is 
100"),
+          ParseFromString(options, deeply_nested_json_array(depth), &parsed));
+    }
+  }
+}
+
 }  // namespace json
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/simdjson_internal.cc 
b/cpp/src/arrow/util/simdjson_internal.cc
index d845eab0d47..d8cea1a5536 100644
--- a/cpp/src/arrow/util/simdjson_internal.cc
+++ b/cpp/src/arrow/util/simdjson_internal.cc
@@ -538,47 +538,57 @@ Result<std::string> MinifyJson(std::string_view json) {
   return minified;
 }
 
-Status ConsumeJsonValue(simdjson::ondemand::value value) {
-  return VisitJsonValue(
-      value, ValidateJsonObject, ValidateJsonArray,
-      [](std::string_view) { return Status::OK(); }, [](bool) { return 
Status::OK(); },
-      []() { return Status::OK(); }, [](int64_t) { return Status::OK(); },
-      [](uint64_t) { return Status::OK(); }, [](double) { return Status::OK(); 
},
-      [](simdjson::ondemand::value) { return Status::OK(); });
-}
-
-Status ValidateJsonObject(simdjson::ondemand::object object) {
+static Status ValidateJsonObject(simdjson::ondemand::object object, int 
max_depth,
+                                 int depth) {
+  if (ARROW_PREDICT_FALSE(depth >= max_depth)) {
+    return Status::Invalid("JSON too deeply nested: max nesting depth is ", 
max_depth);
+  }
   for (auto field_result : object) {
     ARROW_ASSIGN_OR_RAISE(
         auto field, ResolveSimdjsonResult(field_result, "Failed to iterate 
JSON object"));
-
-    RETURN_NOT_OK(ConsumeJsonValue(field.value()));
+    RETURN_NOT_OK(ValidateJsonValue(field.value(), max_depth, depth));
   }
-
   return Status::OK();
 }
 
-Status ValidateJsonArray(simdjson::ondemand::array array) {
+static Status ValidateJsonArray(simdjson::ondemand::array array, int max_depth,
+                                int depth) {
+  if (ARROW_PREDICT_FALSE(depth >= max_depth)) {
+    return Status::Invalid("JSON too deeply nested: max nesting depth is ", 
max_depth);
+  }
   for (auto element_result : array) {
     ARROW_ASSIGN_OR_RAISE(
         auto value,
         ResolveSimdjsonResult(element_result, "Failed to iterate JSON array"));
-
-    RETURN_NOT_OK(ConsumeJsonValue(value));
+    RETURN_NOT_OK(ValidateJsonValue(value, max_depth, depth));
   }
-
   return Status::OK();
 }
 
+Status ValidateJsonValue(simdjson::ondemand::value value, int max_depth, int 
depth) {
+  return VisitJsonValue(
+      value,
+      [depth, max_depth](simdjson::ondemand::object object) {
+        return ValidateJsonObject(object, max_depth, depth + 1);
+      },
+      [depth, max_depth](simdjson::ondemand::array array) {
+        return ValidateJsonArray(array, max_depth, depth + 1);
+      },
+      [](std::string_view) { return Status::OK(); }, [](bool) { return 
Status::OK(); },
+      []() { return Status::OK(); }, [](int64_t) { return Status::OK(); },
+      [](uint64_t) { return Status::OK(); }, [](double) { return Status::OK(); 
},
+      [](simdjson::ondemand::value) { return Status::OK(); });
+}
+
 Status ValidateJsonDocument(simdjson::ondemand::parser& parser,
-                            simdjson::padded_string& json) {
+                            simdjson::padded_string& json, int max_depth) {
   ARROW_ASSIGN_OR_RAISE(
       auto document, ResolveSimdjsonResult(parser.iterate(json), "Failed to 
parse JSON"));
 
   ARROW_ASSIGN_OR_RAISE(auto value, ResolveSimdjsonResult(document.get_value(),
                                                           "Failed to get JSON 
value"));
 
-  return ConsumeJsonValue(value);
+  return ValidateJsonValue(value, max_depth);
 }
 
 /// Returns the position of the first non-whitespace character when trailing 
is false,
diff --git a/cpp/src/arrow/util/simdjson_internal.h 
b/cpp/src/arrow/util/simdjson_internal.h
index d5816522bee..fbdf8639dad 100644
--- a/cpp/src/arrow/util/simdjson_internal.h
+++ b/cpp/src/arrow/util/simdjson_internal.h
@@ -341,14 +341,11 @@ Result<T> GetJsonField(simdjson::ondemand::object& 
object, std::string_view key)
 
 ARROW_EXPORT Result<std::string> MinifyJson(std::string_view json);
 
-ARROW_EXPORT Status ValidateJsonObject(simdjson::ondemand::object object);
-
-ARROW_EXPORT Status ValidateJsonArray(simdjson::ondemand::array array);
-
-ARROW_EXPORT Status ConsumeJsonValue(simdjson::ondemand::value value);
+ARROW_EXPORT Status ValidateJsonValue(simdjson::ondemand::value value, int 
max_depth,
+                                      int depth = 0);
 
 ARROW_EXPORT Status ValidateJsonDocument(simdjson::ondemand::parser& parser,
-                                         simdjson::padded_string& json);
+                                         simdjson::padded_string& json, int 
max_depth);
 
 ARROW_EXPORT int64_t ConsumeJsonWhitespace(std::string_view view, bool 
trailing);
 
diff --git a/cpp/src/parquet/geospatial/util_json_internal.cc 
b/cpp/src/parquet/geospatial/util_json_internal.cc
index 6933d692795..308c882fddc 100644
--- a/cpp/src/parquet/geospatial/util_json_internal.cc
+++ b/cpp/src/parquet/geospatial/util_json_internal.cc
@@ -30,6 +30,10 @@
 namespace parquet {
 
 namespace {
+
+// An upper bound on the expected nesting depth of a geospatial JSON metadata 
object
+constexpr int kMaxJsonDepth = 20;
+
 ::arrow::Result<std::string> GeospatialGeoArrowCrsToParquetCrs(
     simdjson::ondemand::object object) {
   auto json_crs_result =
@@ -195,7 +199,7 @@ namespace {
   simdjson::ondemand::parser parser;
   simdjson::padded_string json(crs);
 
-  if (!::arrow::internal::ValidateJsonDocument(parser, json).ok()) {
+  if (!::arrow::internal::ValidateJsonDocument(parser, json, 
kMaxJsonDepth).ok()) {
     return EscapeJsonString(crs);
   }
 
@@ -215,7 +219,7 @@ LogicalTypeFromGeoArrowMetadata(std::string_view 
serialized_data) {
   simdjson::ondemand::parser parser;
   simdjson::padded_string json(serialized_data);
 
-  RETURN_NOT_OK(::arrow::internal::ValidateJsonDocument(parser, json));
+  RETURN_NOT_OK(::arrow::internal::ValidateJsonDocument(parser, json, 
kMaxJsonDepth));
 
   // Reparse because validation consumes the On-Demand document.
   ARROW_ASSIGN_OR_RAISE(auto document, 
::arrow::internal::ResolveSimdjsonResult(
diff --git a/cpp/src/parquet/reader_test.cc b/cpp/src/parquet/reader_test.cc
index b5487fa4689..05ce2984873 100644
--- a/cpp/src/parquet/reader_test.cc
+++ b/cpp/src/parquet/reader_test.cc
@@ -1445,7 +1445,8 @@ namespace {
   simdjson::ondemand::parser parser;
   auto padded_json = simdjson::padded_string(json_string);
 
-  RETURN_NOT_OK(::arrow::internal::ValidateJsonDocument(parser, padded_json));
+  RETURN_NOT_OK(
+      ::arrow::internal::ValidateJsonDocument(parser, padded_json, 
/*max_depth=*/10));
 
   return ::arrow::Status::OK();
 }
diff --git a/python/pyarrow/tests/test_json.py 
b/python/pyarrow/tests/test_json.py
index e7ab574ead4..2239c2f1bb8 100644
--- a/python/pyarrow/tests/test_json.py
+++ b/python/pyarrow/tests/test_json.py
@@ -22,6 +22,7 @@ import io
 import itertools
 import json
 import string
+import sys
 import unittest
 
 try:
@@ -344,6 +345,32 @@ class BaseTestJSON(abc.ABC):
                             # Better error output
                             assert table.to_pydict() == expected.to_pydict()
 
+    def test_max_nesting_depth(self):
+        if pa.build_info.build_type == 'debug' and sys.platform == 'darwin':
+            pytest.skip('test crashes in debug mode on macOS '
+                        'due to small default thread stack size')
+
+        def deeply_nested_json_object(depth):
+            return b'{"a":' * depth + b'1' + b'}' * depth
+
+        def deeply_nested_json_array(depth):
+            return b'{"a":' + b'[' * (depth - 1) + b'1' + b']' * (depth - 1) + 
b'}'
+
+        max_depth = 100  # Hard-coded in arrow/json/parser.cc
+        table = self.read_bytes(deeply_nested_json_object(max_depth))
+        table.validate(full=True)
+        table = self.read_bytes(deeply_nested_json_array(max_depth))
+        table.validate(full=True)
+
+        with pytest.raises(
+                ValueError,
+                match=f"JSON too deeply nested: max nesting depth is 
{max_depth}"):
+            self.read_bytes(deeply_nested_json_object(max_depth + 1))
+        with pytest.raises(
+                ValueError,
+                match=f"JSON too deeply nested: max nesting depth is 
{max_depth}"):
+            self.read_bytes(deeply_nested_json_array(max_depth + 1))
+
 
 class BaseTestJSONRead(BaseTestJSON):
 

Reply via email to