mirror of
https://github.com/nlohmann/json.git
synced 2026-09-07 08:47:57 +00:00
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:
@@ -201,14 +201,20 @@ class binary_reader
|
||||
{
|
||||
container_frame(const std::size_t remaining_, const bool is_object_,
|
||||
const char_int_type type_marker_ = 0) noexcept
|
||||
: remaining(remaining_), type_marker(type_marker_), is_object(is_object_) {}
|
||||
: remaining(remaining_), start_position(0), type_marker(type_marker_),
|
||||
declared_size(0), is_object(is_object_) {}
|
||||
|
||||
/// number of elements that have not been read yet, or npos when the
|
||||
/// container is not sized and ends at a marker instead
|
||||
std::size_t remaining;
|
||||
/// 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;
|
||||
/// UBJSON/BJData: the type marker of an optimized container, so that
|
||||
/// its elements are read without one of their own; 0 otherwise
|
||||
char_int_type type_marker;
|
||||
/// BSON: the size this document declares, in bytes
|
||||
std::int32_t declared_size;
|
||||
/// whether to close this container with end_object() or end_array()
|
||||
bool is_object;
|
||||
};
|
||||
@@ -283,8 +289,10 @@ class binary_reader
|
||||
@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
|
||||
*/
|
||||
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;
|
||||
std::int32_t document_size{};
|
||||
if (!get_number<std::int32_t, true>(input_format_t::bson, document_size))
|
||||
@@ -292,22 +300,89 @@ class binary_reader
|
||||
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;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!parse_bson_element_list(/*is_array*/false)))
|
||||
container_frame& frame = container_stack.back();
|
||||
frame.start_position = document_start;
|
||||
frame.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;
|
||||
}
|
||||
|
||||
if (JSON_HEDLEY_UNLIKELY(!check_bson_document_size(document_start, document_size)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// the key currently being read; hoisted out of the loop so that its
|
||||
// capacity is reused across elements and across nesting levels
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -419,12 +494,12 @@ class binary_reader
|
||||
|
||||
case 0x03: // object
|
||||
{
|
||||
return parse_bson_internal();
|
||||
return open_bson_document(/*is_object*/true);
|
||||
}
|
||||
|
||||
case 0x04: // array
|
||||
{
|
||||
return parse_bson_array();
|
||||
return open_bson_document(/*is_object*/false);
|
||||
}
|
||||
|
||||
case 0x05: // binary
|
||||
@@ -474,82 +549,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 //
|
||||
|
||||
Reference in New Issue
Block a user