Stack overflow in diff(), merge_patch(), and flatten() due to unbounded recursion
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 50.6k
- Forks
- 7.5k
- Avg merge
- 4d 17h
- Merged PRs (30d)
- 58
Description
Summary
Three value algorithms recurse once per nesting level with no depth limit, so each crashes the process on a deeply nested value: json::diff(), basic_json::merge_patch() and json_pointer::flatten() (reached through basic_json::flatten()).
The text parser is iterative, so json::parse accepts input of any depth. Any of these three called on such a value is a crash, which matters wherever the value came from somewhere untrusted — JSON Patch and JSON Merge Patch are typically applied to exactly that.
unflatten() is not affected: it iterates the object and lets get_and_create walk each pointer, and it handles a 500,000-level pointer without trouble. I checked, so that nobody has to.
Reproduction
Against develop (734fd305a), -O2, macOS/arm64 with the default 8 MB stack. All three crash at depth 100,000:
#include <nlohmann/json.hpp>
#include <string>
int main()
{
const std::size_t depth = 100000;
const auto deep = [&](const char* leaf)
{
return nlohmann::json::parse(std::string(depth, '[') + leaf + std::string(depth, ']'));
};
const nlohmann::json a = deep("0");
const nlohmann::json b = deep("1");
const auto patch = nlohmann::json::diff(a, b); // SIGSEGV
return static_cast<int>(patch.size());
}
| call | result |
|---|---|
json::diff(a, b) |
SIGSEGV |
target.merge_patch(patch) |
SIGSEGV |
j.flatten() |
SIGSEGV |
j.unflatten() |
fine — iterative |
merge_patch is measured with nested objects, since it only descends into objects.
Root cause
diff—basic_json::diff()(json.hpp:5089) calls itself per element: json.hpp:5120 (arrays), :5168 (objects)merge_patch—basic_json::merge_patch()(json.hpp:5231) calls itself per member: json.hpp:5247flatten—json_pointer::flatten()(json_pointer.hpp:861) calls itself per element: json_pointer.hpp:879 (arrays), :898 (objects)
Suggested fix
The same shape used for the destructor in #1436 and for copying, serializing and comparing in #5389 / #5285 / #5390: descend a bounded number of levels, then finish the rest on an explicit stack.
flatten looks the most straightforward of the three, since it only accumulates into result and carries a prefix string down. diff returns a value built from each level's result, and merge_patch mutates in place, so both need a little more care about what the explicit stack has to carry.
Related
- #5387 — the same problem in the copy constructor and
dump() - #5104 — the same problem in the binary readers
- #1436 — where the destructor was fixed this way
Written by Claude Code.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the 100,000-level reproducer against develop, then inspect basic_json::diff() and basic_json::merge_patch() in json.hpp alongside json_pointer::flatten() in json_pointer.hpp. Compare their recursive call sites with the iterative approach referenced in #1436 and #5389. Done means all three operations handle deeply nested values without stack overflow while preserving their existing results.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend-api-design, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100