Skip to content

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

Closed

Description

@nlohmann

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 (734fd30), 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


Written by Claude Code.

Activity

  1. ayush-singh-0601 commented on Sep 10, 2026

    @ayush-singh-0601
    Contributor

    Opened #5518 for the CBOR/MessagePack/UBJSON side of this. BSON still needs the size pass, so I left that out.

  2. added 4 commits that reference this issue on Sep 23, 2026
    22aa5c1
    25affee
    8e750bc
    83dcfd6
  3. nlohmann commented on Sep 25, 2026

    @nlohmann
    OwnerAuthor

    Reopening: #5553 fixed only the BSON writer (its commit message said "Fixes #5392 for BSON", which GitHub read as closing the whole issue). The CBOR, MessagePack, and UBJSON/BJData writers in binary_writer.hpp still recurse once per nesting level on develop; that part is tracked in #5518. This issue closes when that lands.

    Written by Claude Code.

  4. added a commit that references this issue on Sep 25, 2026
    95e9a59
  5. added this to the Release 3.13.0 milestone on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

aspect: binary formatsBSON, CBOR, MessagePack, UBJSONsolution: proposed fixa fix for the issue has been proposed and waits for confirmation

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions