Skip to content

GH-51757: [C++] Limit max JSON nesting depth in JSON parser - #51758

Open
pitrou wants to merge 5 commits into
apache:mainfrom
pitrou:gh51757-json-max-nesting-depth
Open

pitrou wants to merge 5 commits into
apache:mainfrom
pitrou:gh51757-json-max-nesting-depth

Conversation

@pitrou

@pitrou pitrou commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

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:

  • Human
  • AI

Reviewed before submission by:

  • Human (self-reviewed)
  • AI
  • Not reviewed

@pitrou pitrou added Critical Fix Bugfixes for security vulnerabilities, crashes, or invalid data. backport-candidate labels Oct 5, 2026
@pitrou
pitrou force-pushed the gh51757-json-max-nesting-depth branch from e3608fb to 3a59361 Compare October 5, 2026 14:18
@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Hmm, the macOS Python builds don't like our new PyArrow tests. I think it's because the default stack size for new threads on macOS is quite smaller (512 kiB by default), debug builds might add some stack overhead, and some of our JSON reading tests are multi-threaded.

It's a bit unexpected that 512 kiB of stack isn't enough for 300 levels of nesting (the current limit). I'm now trying to make stack consumption a bit smaller still, by removing the functional overhead in RawBuilderSet::Finish.

@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Hmm, the macOS Python builds don't like our new PyArrow tests. I think it's because the default stack size for new threads on macOS is quite smaller (512 kiB by default), debug builds might add some stack overhead, and some of our JSON reading tests are multi-threaded.

It's a bit unexpected that 512 kiB of stack isn't enough for 300 levels of nesting (the current limit). I'm now trying to make stack consumption a bit smaller still, by removing the functional overhead in RawBuilderSet::Finish.

Hmm, still getting crashes on macOS Python. Does anyone have a macOS machine so as to get a stack trace?

@taepper @Reranko05

@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

That said, it seems that stack consumption in debug mode on Linux can be huge as well (each nesting level in StructType::~StructType is about 1kB of stack space!). It's just that default stack size on Linux is much larger, 8 MB IIRC.

Perhaps we should just disable these PyArrow tests in debug mode on macOS.

@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

@github-actions crossbow submit wheelcp314*

@github-actions

This comment was marked as outdated.

@pitrou pitrou added the CI: Extra: C++ Run extra C++ CI label Oct 5, 2026
@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Ok, the PyArrow tests passed on the macOS wheel builds (in release mode), so regular users will get stack overflow protection.

@pitrou
pitrou marked this pull request as ready for review October 5, 2026 17:20
@pitrou
pitrou requested review from AlenkaF, raulcd and rok as code owners October 5, 2026 17:20
@pitrou

pitrou commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

@taepper Do you want to review this?

Comment thread python/pyarrow/tests/test_json.py
Comment thread cpp/src/arrow/json/parser.cc Outdated
@github-actions github-actions Bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Oct 6, 2026
@Reranko05

Copy link
Copy Markdown
Collaborator

Should we also add an array variant in the C++ test, like the Python test has for both object and array? Right now the C++ test only covers nested objects.

@pitrou
pitrou force-pushed the gh51757-json-max-nesting-depth branch from 3e4a96c to 9ddab7c Compare October 6, 2026 07:02
@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Should we also add an array variant in the C++ test, like the Python test has for both object and array? Right now the C++ test only covers nested objects.

Good point, I've added a C++ test for this.

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 1000.
@pitrou
pitrou force-pushed the gh51757-json-max-nesting-depth branch from 9ddab7c to 422930f Compare October 6, 2026 12:35
@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

I've rebased after PR #51038 (migrating the parser to simdjson) was merged.

@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@github-actions crossbow submit wheelcp314*

@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@github-actions crossbow submit -g cpp

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Revision: 422930f

Submitted crossbow builds: ursacomputing/crossbow @ actions-af8e1837a0

Task Status
wheel-macos-monterey-cp314-cp314-amd64 GitHub Actions
wheel-macos-monterey-cp314-cp314-arm64 GitHub Actions
wheel-macos-monterey-cp314-cp314t-amd64 GitHub Actions
wheel-macos-monterey-cp314-cp314t-arm64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314-amd64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314-arm64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314t-amd64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314t-arm64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314-amd64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314-arm64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314t-amd64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314t-arm64 GitHub Actions
wheel-windows-cp314-cp314-amd64 GitHub Actions
wheel-windows-cp314-cp314t-amd64 GitHub Actions

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Revision: 422930f

Submitted crossbow builds: ursacomputing/crossbow @ actions-c29d6a353c

Task Status
example-cpp-minimal-build-static GitHub Actions
example-cpp-minimal-build-static-system-dependency GitHub Actions
example-cpp-tutorial GitHub Actions
test-build-cpp-fuzz GitHub Actions
test-conda-cpp GitHub Actions
test-conda-cpp-valgrind GitHub Actions
test-debian-13-cpp-amd64 GitHub Actions
test-debian-13-cpp-i386 GitHub Actions
test-debian-experimental-cpp-gcc-15 GitHub Actions
test-fedora-42-cpp GitHub Actions
test-ubuntu-22.04-cpp GitHub Actions
test-ubuntu-22.04-cpp-bundled GitHub Actions
test-ubuntu-22.04-cpp-emscripten GitHub Actions
test-ubuntu-22.04-cpp-no-threading GitHub Actions
test-ubuntu-24.04-cpp GitHub Actions
test-ubuntu-24.04-cpp-gcc-13-bundled GitHub Actions
test-ubuntu-24.04-cpp-gcc-14 GitHub Actions
test-ubuntu-24.04-cpp-minimal-with-formats GitHub Actions
test-ubuntu-24.04-cpp-thread-sanitizer GitHub Actions

@taepper taepper left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks very good! I really like the refactoring of lambdas to explicitly destructing the child data at call sites.

One gap I found is the ConsumeJsonValue method, which has the same recursive behavior as the parser had.

Comment thread cpp/src/arrow/json/parser.cc Outdated
Comment on lines 678 to 679

@taepper taepper Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we still call a recursive method without any depth limit. Ideally, we want to pass the current depth to ConsumeJsonValue.

This PR definitely already fixes many cases, so whether we want to change ConsumeJsonValue to accept a depth parameter in this same PR is a different question.

Alternatively, we can add a follow-up issue

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll tackle this here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could solve this like in this case:
#51472

where we let simdjson handle the skip by the regular advancing of the iterator

But this would avoid the json validation (beyond structural errors regarding braces [,],{,} which will still be checked), which is probably required at this point

Comment on lines +34 to +35
// An upper bound on the expected nesting depth of a geospatial JSON metadata object
constexpr int kMaxJsonDepth = 20;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paleolimbot Does this limit look ok?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that makes sense. Thank you!

Comment on lines +401 to +402
// 100'000 would definitely trigger a stack overflow, validate that the error is
// detected before that would happen.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: in debug builds we probably did not run into stack overflows, but simdjson assertions.

simdjson::ondemand::parser also has a depth limit (default 1024), and when development checks are enabled, it will assert and thus possibly abort the program, if we consume objects deeper than that limit:

https://github.com/simdjson/simdjson/blob/7fa77b1e4ba2a21c97d97adc48e4cd8b9d1e7faa/doc/basics.md?plain=1#L3985-L4032

I did not quite understand the purpose of this limit (other than aborting debug code using the parser). I will open an issue in simdjson to find out a bit more.

Not any change request, but just for context, which we should maybe document somewhere?

Comment thread cpp/src/arrow/util/simdjson_internal.cc Outdated
@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

Ok, so musllinux crashes after ~152 levels of nesting. It seems the thread stack size is 128kiB there, and each level of nesting in ParseImpl::ParseValue takes 792 bytes of stack (!).

@pitrou

pitrou commented Oct 6, 2026

Copy link
Copy Markdown
Member Author

@github-actions crossbow submit wheelcp314*

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Revision: e71ab61

Submitted crossbow builds: ursacomputing/crossbow @ actions-379dcb9fd6

Task Status
wheel-macos-monterey-cp314-cp314-amd64 GitHub Actions
wheel-macos-monterey-cp314-cp314-arm64 GitHub Actions
wheel-macos-monterey-cp314-cp314t-amd64 GitHub Actions
wheel-macos-monterey-cp314-cp314t-arm64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314-amd64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314-arm64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314t-amd64 GitHub Actions
wheel-manylinux-2-28-cp314-cp314t-arm64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314-amd64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314-arm64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314t-amd64 GitHub Actions
wheel-musllinux-1-2-cp314-cp314t-arm64 GitHub Actions
wheel-windows-cp314-cp314-amd64 GitHub Actions
wheel-windows-cp314-cp314t-amd64 GitHub Actions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting changes Awaiting changes backport-candidate CI: Extra: C++ Run extra C++ CI Component: C++ Component: Parquet Component: Python Critical Fix Bugfixes for security vulnerabilities, crashes, or invalid data.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants