#5392·json

Stack overflow in binary format writers (CBOR/MessagePack/UBJSON/BSON) due to unbounded recursion

Author: nlohmannCreated Aug 21, 2026Updated Sep 10, 2026

Summary

The binary format writers (binary_writer.hpp) recurse once per nesting level with no depth limit, so serializing a deeply nested value crashes the process. This is the counterpart of #5104, which covers the binary readers; both directions are affected, and neither is fixed.

The text parser is iterative, so json::parse accepts input of any depth. A value that the library happily builds can therefore crash it on the way back out.

Affected functions

  • json::to_cbor()
  • json::to_msgpack()
  • json::to_ubjson() (and BJData)
  • json::to_bson()

Reproduction

Against develop (734fd305a), on macOS/arm64 with the default 8 MB stack:

#include <nlohmann/json.hpp>
#include <string>

int main()
{
    const std::size_t depth = 200000;
    const nlohmann::json j = nlohmann::json::parse(
        std::string(depth, '[') + "0" + std::string(depth, ']'));

    const auto v = nlohmann::json::to_cbor(j);   // SIGSEGV
    return static_cast<int>(v.size());
}
function -O0 -O2
to_cbor crashes at depth 100,000 crashes at depth 200,000
to_msgpack crashes at depth 100,000 crashes at depth 200,000
to_ubjson crashes at depth 100,000 crashes at depth 100,000
to_bson crashes at depth 100,000 crashes at depth 100,000

The depth needed varies with build and stack size, as usual for stack exhaustion; CBOR and MessagePack survive depth 100,000 at -O2 on this machine and fail at 200,000. BSON is measured with a nested object, since BSON requires an object at the root.

Root cause

Each writer calls itself for every element:

  • CBOR: write_cbor() (binary_writer.hpp:96) → binary_writer.hpp:303 (array), :405/:406 (object)
  • MessagePack: write_msgpack() (:420) → :614 (array), :727/:728 (object)
  • UBJSON/BJData: write_ubjson() (:747) → :842 (array), :944 (object)
  • BSON: write_bson_element() (:1238) → write_bson_object() (:1302) / write_bson_array() (:1160) → write_bson_element() (:1306/:1166)

BSON recurses twice over the same value. Besides writing, it computes each document's length up front through a second, independent recursive walk: calc_bson_element_size() (:1190) → calc_bson_object_size() (:1285) / calc_bson_array_size() (:1133) → calc_bson_element_size() (:1290/:1139). Both walks need bounding, which makes BSON the most involved of the four.

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. That keeps the fast path for ordinary values and removes the limit on depth, without a new exception or a new macro.

CBOR, MessagePack and UBJSON are structurally similar and could share an approach. BSON's size pass is separate work and may be worth its own change.

Related

  • #5104 — the same problem in the binary readers
  • #5387 — the same problem in the copy constructor and dump()
  • #1436 — where the destructor was fixed this way

Written by Claude Code.