Stop the BON8 writer overflowing the stack on deep values (#5806)

* Stop the BON8 writer overflowing the stack on deep values

to_bon8() recursed once per nesting level, so a value the iterative
BON8 reader accepts (e.g. ~24k nested one-element arrays) crashed on
the way back out. #5781 bounded the CBOR, MessagePack, and UBJSON/BJData
writers, but BON8 was merged before it and was not covered.

Apply the same scheme: recurse for the first recursion_depth_limit()
levels, then finish the value with write_bon8_iterative, which keeps
the open containers on a heap stack (reusing binary_container_frame)
and writes the 0xFE closer when it leaves a container with more than
four elements. The output is byte-for-byte unchanged.

Fixes https://issues.oss-fuzz.com/issues/572238015

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Merge the array and object branches of the BON8 iterative writer

write_bon8_iterative and write_bon8_value_or_push handled arrays and
objects in separate branches that repeated the end-of-container check,
the 0xFE closer and the marker computation. Share those parts and branch
only where arrays and objects really differ. The output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hz7VJi1FTKr6gpseLErbbS
Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Niels LohmannandClaude Opus 5.5 authored and GitHub committed 2026-10-11 08:24:35 +02:00
1 parent eb8899a29c
commit b38a2c9177
2 files changed
+163 -7

No files matched your search

@@ -868,9 +868,9 @@ class binary_writer
}
}
/// @brief a CBOR or MessagePack array or object whose elements
/// @ref write_cbor_iterative or @ref write_msgpack_iterative is
/// still writing
/// @brief a CBOR, MessagePack, or BON8 array or object whose elements
/// @ref write_cbor_iterative, @ref write_msgpack_iterative, or
/// @ref write_bon8_iterative is still writing
struct binary_container_frame
{
explicit binary_container_frame(const BasicJsonType* value_) noexcept
@@ -2609,11 +2609,27 @@ class binary_writer
@param[in] j JSON value to serialize
@param[in,out] string_open whether the output ends with a non-empty
string that has not been terminated with 0xFF
@param[in] depth nesting level of @a j, counted from the
top-level value passed to @ref basic_json::to_bon8
@throw type_error.316 if a string value or an object key is not valid
UTF-8
@throw out_of_range.407 if an unsigned integer does not fit int64
@throw type_error.321 if @a j or a value nested in it is discarded
Values nested deeper than @ref recursion_depth_limit are written by
@ref write_bon8_iterative without the call stack.
@sa @ref write_cbor
@sa https://github.com/nlohmann/json/issues/5392
*/
void write_bon8_value(const BasicJsonType& j, bool& string_open)
void write_bon8_value(const BasicJsonType& j, bool& string_open, const std::size_t depth = 0)
{
if (JSON_HEDLEY_UNLIKELY(depth >= recursion_depth_limit()) && (j.is_array() || j.is_object()))
{
write_bon8_iterative(j, string_open);
return;
}
switch (j.type())
{
case value_t::null:
@@ -2667,7 +2683,7 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.array)
{
write_bon8_value(el, string_open);
write_bon8_value(el, string_open, depth + 1);
}
if (N > 4)
@@ -2686,7 +2702,7 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.object)
{
write_bon8_string(el.first, string_open, j);
write_bon8_value(el.second, string_open);
write_bon8_value(el.second, string_open, depth + 1);
}
if (N > 4)
@@ -2723,6 +2739,83 @@ class binary_writer
}
}
/*!
@brief write @a j with @ref write_bon8_value, or write its marker and push
a frame for @ref write_bon8_iterative to continue with its elements
A scalar, and an empty array or object, are written out in full and not
pushed.
@sa @ref write_cbor_value_or_push
*/
void write_bon8_value_or_push(const BasicJsonType& j, bool& string_open, std::vector<binary_container_frame>& stack)
{
if (j.is_array() || j.is_object())
{
// arrays use the markers 0x80..0x85, objects 0x86..0x8B; the last
// one stands for more than four elements, closed later with 0xFE
const auto N = j.size();
const std::size_t base = j.is_array() ? 0x80 : 0x86;
write_bon8_marker(static_cast<std::uint8_t>(base + (N <= 4 ? N : 5)), string_open);
if (N != 0)
{
stack.emplace_back(&j);
}
return;
}
write_bon8_value(j, string_open);
}
/*!
@brief write out @a root and everything below it without the call stack
A container with more than four elements is closed with 0xFE once its
last element is written, as in @ref write_bon8_value.
@sa @ref write_cbor_iterative
*/
void write_bon8_iterative(const BasicJsonType& root, bool& string_open)
{
// only a container with elements is ever pushed; see write_bon8_value_or_push
std::vector<binary_container_frame> stack;
write_bon8_value_or_push(root, string_open, stack);
while (!stack.empty())
{
const binary_container_frame current = stack.back();
const bool is_array = current.value->is_array();
const bool at_end = is_array
? current.array_it == current.value->m_data.m_value.array->cend()
: current.object_it == current.value->m_data.m_value.object->cend();
if (at_end)
{
if (current.value->size() > 4)
{
write_bon8_marker(0xFE, string_open);
}
stack.pop_back();
continue;
}
// read the child before pushing: entering it can move every frame
const BasicJsonType* child = nullptr;
if (is_array)
{
child = &(*current.array_it);
++stack.back().array_it;
}
else
{
write_bon8_string(current.object_it->first, string_open, *current.value);
child = &(current.object_it->second);
++stack.back().object_it;
}
write_bon8_value_or_push(*child, string_open, stack);
}
}
/*!
@brief write a single byte that is not part of a string