Read BSON documents without recursing per nesting level

An embedded document (record type 0x03) or array (0x04) was read by calling
back into the document reader, which read its element list, which called the
element reader again for the next embedded one. The native call stack
therefore grew with the nesting depth of the input, and about seven bytes buy
a level, so a document of a few hundred kilobytes crashes the process
(#5104). This is the last of the four binary formats to still do that.

Apply the same shape as the other three: open_bson_document() reads the size
prefix and opens the document, parse_bson_element_internal() calls it for both
record types instead of recursing, and parse_bson_internal() loops over the
element list of whichever document is innermost, closing it when its
terminator is reached and resuming the one below.

check_bson_document_size() is unchanged, and so is when it runs: a document is
still measured from the byte before its size prefix to the byte after its
terminator, and still reported before the end event. The frame carries those
two values, which is what a per-document check needs once the reads are
interleaved rather than nested. Nothing else about the element reader changes.

unit-bson passes unchanged. Round trips through to_bson of nested objects,
arrays, arrays of objects and mixed nesting are identical to the previous
commit, as are the errors for a truncated document, an unsupported record
type, a negative size and a size that does not match, including their byte
offsets. A 30,000-level document built by to_bson is now read to completion
where it used to crash.

Note for sequencing: #5185 changes parse_bson_internal(), the element list and
the array reader, which are the functions this commit restructures. It should
land first; this commit then keeps its checks and moves them onto the loop.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann
2026-09-06 18:34:00 +02:00
parent 7dc308d952
commit 89994acc2c
3 changed files with 236 additions and 170 deletions
+83 -85
View File
@@ -202,9 +202,14 @@ class binary_reader
/// number of elements that have not been read yet, or npos when the /// number of elements that have not been read yet, or npos when the
/// container is not sized and ends at a marker instead /// container is not sized and ends at a marker instead
std::size_t remaining = 0; std::size_t remaining = 0;
/// BSON: value of chars_read before this document's size prefix, which
/// check_bson_document_size() needs once the document has been read
std::size_t start_position = 0;
/// UBJSON/BJData: the type marker of an optimized container, so that /// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise /// its elements are read without one of their own; 0 otherwise
char_int_type type_marker = 0; char_int_type type_marker = 0;
/// BSON: the size this document declares, in bytes
std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array() /// whether to close this container with end_object() or end_array()
bool is_object = false; bool is_object = false;
}; };
@@ -271,8 +276,10 @@ class binary_reader
@brief Reads in a BSON-object and passes it to the SAX-parser. @brief Reads in a BSON-object and passes it to the SAX-parser.
@return whether a valid BSON-value was passed to the SAX parser @return whether a valid BSON-value was passed to the SAX parser
*/ */
bool parse_bson_internal() bool open_bson_document(const bool is_object)
{ {
// recorded before the size prefix is read, because
// check_bson_document_size() measures the document from here
const std::size_t document_start = chars_read; const std::size_t document_start = chars_read;
std::int32_t document_size{}; std::int32_t document_size{};
if (!get_number<std::int32_t, true>(input_format_t::bson, document_size)) if (!get_number<std::int32_t, true>(input_format_t::bson, document_size))
@@ -280,22 +287,88 @@ class binary_reader
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size())))
{ {
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/false))) container_stack.back().start_position = document_start;
container_stack.back().declared_size = document_size;
return true;
}
/*!
@brief read a BSON document and everything nested inside it
Reads elements until the document that was begun here is complete,
resuming the enclosing document each time an embedded one ends, so that
the nesting depth of the input costs heap rather than native stack
(see #5104).
@return whether reading the document succeeded
*/
bool parse_bson_internal()
{
if (JSON_HEDLEY_UNLIKELY(!open_bson_document(/*is_object*/true)))
{ {
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
return false; string_t key;
}
return sax->end_object(); while (true)
{
const auto element_type = get();
if (element_type == 0) // end of the innermost document
{
const container_frame& top = container_stack.back();
const bool is_object = top.is_object;
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(top.start_position, top.declared_size)))
{
return false;
}
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the document begun here is complete once it is not inside one
if (container_stack.empty())
{
return true;
}
continue;
}
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list")))
{
return false;
}
const std::size_t element_type_parse_position = chars_read;
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
{
return false;
}
// an array's elements are named "0", "1", ... in the wire format,
// and those names are not passed on
if (container_stack.back().is_object && !sax->key(key))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position)))
{
return false;
}
}
} }
/*! /*!
@@ -407,12 +480,12 @@ class binary_reader
case 0x03: // object case 0x03: // object
{ {
return parse_bson_internal(); return open_bson_document(/*is_object*/true);
} }
case 0x04: // array case 0x04: // array
{ {
return parse_bson_array(); return open_bson_document(/*is_object*/false);
} }
case 0x05: // binary case 0x05: // binary
@@ -462,82 +535,7 @@ class binary_reader
} }
} }
/*!
@brief Read a BSON element list (as specified in the BSON-spec)
The same binary layout is used for objects and arrays, hence it must be
indicated with the argument @a is_array which one is expected
(true --> array, false --> object).
@param[in] is_array Determines if the element list being read is to be
treated as an object (@a is_array == false), or as an
array (@a is_array == true).
@return whether a valid BSON-object/array was passed to the SAX parser
*/
bool parse_bson_element_list(const bool is_array)
{
string_t key;
while (auto element_type = get())
{
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list")))
{
return false;
}
const std::size_t element_type_parse_position = chars_read;
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
{
return false;
}
if (!is_array && !sax->key(key))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position)))
{
return false;
}
// get_bson_cstr only appends
key.clear();
}
return true;
}
/*!
@brief Reads an array from the BSON input and passes it to the SAX-parser.
@return whether a valid BSON-array was passed to the SAX parser
*/
bool parse_bson_array()
{
const std::size_t document_start = chars_read;
std::int32_t document_size{};
if (!get_number<std::int32_t, true>(input_format_t::bson, document_size))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size())))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/true)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size)))
{
return false;
}
return sax->end_array();
}
////////// //////////
// CBOR // // CBOR //
+83 -85
View File
@@ -10889,9 +10889,14 @@ class binary_reader
/// number of elements that have not been read yet, or npos when the /// number of elements that have not been read yet, or npos when the
/// container is not sized and ends at a marker instead /// container is not sized and ends at a marker instead
std::size_t remaining = 0; std::size_t remaining = 0;
/// BSON: value of chars_read before this document's size prefix, which
/// check_bson_document_size() needs once the document has been read
std::size_t start_position = 0;
/// UBJSON/BJData: the type marker of an optimized container, so that /// UBJSON/BJData: the type marker of an optimized container, so that
/// its elements are read without one of their own; 0 otherwise /// its elements are read without one of their own; 0 otherwise
char_int_type type_marker = 0; char_int_type type_marker = 0;
/// BSON: the size this document declares, in bytes
std::int32_t declared_size = 0;
/// whether to close this container with end_object() or end_array() /// whether to close this container with end_object() or end_array()
bool is_object = false; bool is_object = false;
}; };
@@ -10958,8 +10963,10 @@ class binary_reader
@brief Reads in a BSON-object and passes it to the SAX-parser. @brief Reads in a BSON-object and passes it to the SAX-parser.
@return whether a valid BSON-value was passed to the SAX parser @return whether a valid BSON-value was passed to the SAX parser
*/ */
bool parse_bson_internal() bool open_bson_document(const bool is_object)
{ {
// recorded before the size prefix is read, because
// check_bson_document_size() measures the document from here
const std::size_t document_start = chars_read; const std::size_t document_start = chars_read;
std::int32_t document_size{}; std::int32_t document_size{};
if (!get_number<std::int32_t, true>(input_format_t::bson, document_size)) if (!get_number<std::int32_t, true>(input_format_t::bson, document_size))
@@ -10967,22 +10974,88 @@ class binary_reader
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size()))) if (JSON_HEDLEY_UNLIKELY(!enter_container(is_object, detail::unknown_size())))
{ {
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/false))) container_stack.back().start_position = document_start;
container_stack.back().declared_size = document_size;
return true;
}
/*!
@brief read a BSON document and everything nested inside it
Reads elements until the document that was begun here is complete,
resuming the enclosing document each time an embedded one ends, so that
the nesting depth of the input costs heap rather than native stack
(see #5104).
@return whether reading the document succeeded
*/
bool parse_bson_internal()
{
if (JSON_HEDLEY_UNLIKELY(!open_bson_document(/*is_object*/true)))
{ {
return false; return false;
} }
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size))) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
return false; string_t key;
}
return sax->end_object(); while (true)
{
const auto element_type = get();
if (element_type == 0) // end of the innermost document
{
const container_frame& top = container_stack.back();
const bool is_object = top.is_object;
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(top.start_position, top.declared_size)))
{
return false;
}
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the document begun here is complete once it is not inside one
if (container_stack.empty())
{
return true;
}
continue;
}
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list")))
{
return false;
}
const std::size_t element_type_parse_position = chars_read;
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
{
return false;
}
// an array's elements are named "0", "1", ... in the wire format,
// and those names are not passed on
if (container_stack.back().is_object && !sax->key(key))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position)))
{
return false;
}
}
} }
/*! /*!
@@ -11094,12 +11167,12 @@ class binary_reader
case 0x03: // object case 0x03: // object
{ {
return parse_bson_internal(); return open_bson_document(/*is_object*/true);
} }
case 0x04: // array case 0x04: // array
{ {
return parse_bson_array(); return open_bson_document(/*is_object*/false);
} }
case 0x05: // binary case 0x05: // binary
@@ -11149,82 +11222,7 @@ class binary_reader
} }
} }
/*!
@brief Read a BSON element list (as specified in the BSON-spec)
The same binary layout is used for objects and arrays, hence it must be
indicated with the argument @a is_array which one is expected
(true --> array, false --> object).
@param[in] is_array Determines if the element list being read is to be
treated as an object (@a is_array == false), or as an
array (@a is_array == true).
@return whether a valid BSON-object/array was passed to the SAX parser
*/
bool parse_bson_element_list(const bool is_array)
{
string_t key;
while (auto element_type = get())
{
if (JSON_HEDLEY_UNLIKELY(!unexpect_eof(input_format_t::bson, "element list")))
{
return false;
}
const std::size_t element_type_parse_position = chars_read;
if (JSON_HEDLEY_UNLIKELY(!get_bson_cstr(key)))
{
return false;
}
if (!is_array && !sax->key(key))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_internal(element_type, element_type_parse_position)))
{
return false;
}
// get_bson_cstr only appends
key.clear();
}
return true;
}
/*!
@brief Reads an array from the BSON input and passes it to the SAX-parser.
@return whether a valid BSON-array was passed to the SAX parser
*/
bool parse_bson_array()
{
const std::size_t document_start = chars_read;
std::int32_t document_size{};
if (!get_number<std::int32_t, true>(input_format_t::bson, document_size))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size())))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/true)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size)))
{
return false;
}
return sax->end_array();
}
////////// //////////
// CBOR // // CBOR //
+70
View File
@@ -1011,6 +1011,76 @@ TEST_CASE("BSON document size mismatch")
} }
} }
TEST_CASE("BSON nesting does not consume the call stack")
{
// An embedded document or array used to be read by calling back into the
// document reader, so the native call stack grew with the nesting depth of
// the input (#5104). The open documents are kept on a heap stack now.
//
// Deeply nested values must not be compared, copied or dumped here: those
// operations are still recursive and would reintroduce the crash.
// a document nested deeply enough to have crashed, built by to_bson so
// that every one of its size prefixes is correct
const std::size_t depth = 30000;
json deep = json::object();
json* p = &deep;
for (std::size_t i = 0; i < depth; ++i)
{
(*p)["a"] = json::object();
p = &(*p)["a"];
}
const std::vector<uint8_t> input = json::to_bson(deep);
SECTION("a well-formed deep document is read through the SAX interface")
{
SaxCountdown accept_all(1000000);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::bson));
}
SECTION("a well-formed deep document is read into a value")
{
json j = json::from_bson(input);
std::size_t measured = 0;
const json* q = &j;
while (q->is_object() && !q->empty())
{
q = &q->begin().value();
++measured;
}
CHECK(measured == depth);
}
SECTION("embedded documents and arrays are still read the same way")
{
const json values = {{"a", {{"b", {{"c", 1}}}}}};
CHECK(json::from_bson(json::to_bson(values)) == values);
const json array = {{"a", {1, 2, 3}}};
CHECK(json::from_bson(json::to_bson(array)) == array);
const json mixed = {{"a", {json{{"x", 1}}, json{{"y", 2}}}}};
CHECK(json::from_bson(json::to_bson(mixed)) == mixed);
CHECK(json::from_bson(json::to_bson(json::object())) == json::object());
}
SECTION("a size that does not match is still reported per document")
{
// the embedded document claims one byte too many
std::vector<uint8_t> const bad =
{
0x15, 0x00, 0x00, 0x00, 0x03, 'a', 0x00,
0x0D, 0x00, 0x00, 0x00, 0x08, 'b', 0x00, 0x01, 0x00,
0x00
};
json _;
CHECK_THROWS_AS(_ = json::from_bson(bad), json::parse_error&);
CHECK(json::from_bson(bad, true, false).is_discarded());
}
}
TEST_CASE("BSON numerical data") TEST_CASE("BSON numerical data")
{ {
SECTION("number") SECTION("number")