Compare commits

..
Author SHA1 Message Date
Niels Lohmann 1df654d19d Read CBOR containers and tags without recursing per nesting level
get_cbor_array() and get_cbor_object() read their elements by calling back
into the value reader, which called them again for a nested container, and a
tag was handled by reading the tagged value the same way. All three cost
native stack, and all three cost a single byte to encode: 0x9F opens an
indefinite-length array, 0x81 a one-element array, and 0xC2 is a tag. Half a
million of any of them crashes the process before the input runs out (#5104).

Apply the shape the MessagePack reader already uses: the open containers live
on the heap stack, parse_cbor_value() reads a single value and only opens a
container rather than reading it to its end, and parse_cbor_internal() loops,
resuming the innermost container after each element.

Two things are specific to CBOR. An indefinite-length container ends at a
break marker rather than at a count, and testing for that marker consumes a
byte which is the first byte of the next element when it is not one; the
frame's count is npos for those, and the driver tracks whether the next value
starts at a fresh byte. And a tag is not a value of its own: instead of
reading the tagged value by recursing, the value reader reports that a tag was
read and the driver reads on, so a chain of tags costs no stack at all.

The switch that decodes a value is unchanged apart from the twelve container
cases and the two tag sites. Verified against the previous commit over
definite and indefinite arrays and maps, all four counted forms, empty
containers, nesting of the forms inside each other, truncated inputs, and all
three tag handlers: identical values, error codes, messages and byte offsets.
500,000 levels of each of the three vectors now report parse_error.110 instead
of crashing, and a well-formed 200,000-level value is read to completion.

On performance: the driver does per element what a counted loop used to do
per container, and CBOR pays for it more than MessagePack because the value
reader also has to be told whether to fetch a byte. Parsing 60,000 small
objects and one array of a million integers is 3 to 4 % slower than the
recursive reader, measured over five alternating runs. Against develop the
same two inputs are about 44 % faster, because the entry point no longer
copies the value it parsed; the earlier commit in this series is what pays
for that.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-07 15:45:33 +02:00
Niels Lohmann 90bd0f7fec Read MessagePack containers without recursing per nesting level
get_msgpack_array() and get_msgpack_object() read their elements by calling
back into parse_msgpack_internal(), which calls them again for a nested
container. The native call stack therefore grew with the nesting depth of the
input, and each level costs only one byte to encode: 0x91 is a one-element
array, so a few hundred thousand of them crash the process before any of the
input is rejected (#5104).

Keep the open containers on a heap stack instead, the way
parser::sax_parse_internal() has always done for JSON text. A frame records
how many elements are left and whether to close with end_object() or
end_array(); parse_msgpack_value() reads a single value and, for a container,
only opens it; and parse_msgpack_internal() loops, resuming the innermost
container after each element and closing it when its count runs out. Whether
the value that was begun is complete is answered by the stack being empty, so
no separate bookkeeping is needed.

The switch that decodes a value is untouched apart from the six container
cases, which now call enter_container() rather than a reader that loops. That
keeps this diff to the control flow and leaves the decoding of every other
type byte-identical.

enter_container() is the only place a binary reader emits start_object() or
start_array(), so a check that rejects a container can be added there once and
is guaranteed to run before the start event. The frame type and the stack are
shared, ready for the other three formats.

Verified against develop over empty, nested, counted (array 16/32, map 16/32)
and truncated inputs: identical values, error codes, messages and byte
offsets. 300,000 levels now report parse_error.110 instead of crashing, and a
well-formed 300,000-level value is read to completion through the SAX
interface, where develop crashes.

Reading such a value into a basic_json needs the return-by-move change as
well, without which the recursive copy constructor overflows on the way out;
that is the parent commit, and the test for the value path covers the two
together. Timing is unchanged: parsing 60,000 small objects and one array of
a million integers is within run-to-run noise of develop either way.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-07 14:08:26 +02:00
4 changed files with 636 additions and 260 deletions
+238 -124
View File
@@ -130,6 +130,7 @@ class binary_reader
const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
{ {
sax = sax_; sax = sax_;
container_stack.clear();
bool result = false; bool result = false;
switch (format) switch (format)
@@ -179,6 +180,69 @@ class binary_reader
} }
private: private:
////////////////////////
// nested containers //
////////////////////////
/*!
@brief a container that has been opened and not closed yet
The binary readers do not call themselves once per nesting level. Like
@ref parser::sax_parse_internal, which does the same for JSON text, they
keep the containers they are inside of on a heap-allocated stack, so that
the native call stack does not grow with the nesting depth of the input
and a deeply nested value is bounded by memory rather than by the stack
(see #5104).
The members are ordered by decreasing alignment, which is the ordering that
keeps a struct from growing as members are added to it.
*/
struct container_frame
{
container_frame(const std::size_t remaining_, const bool is_object_) noexcept
: remaining(remaining_), is_object(is_object_) {}
/// number of elements that have not been read yet
std::size_t remaining;
/// whether to close this container with end_object() or end_array()
bool is_object;
};
/*!
@brief open a nested array or object
Emits the SAX start event and records the container. This is the only
place the binary readers start a container, so a check that rejects one
can be made here and is then guaranteed to run before the start event.
@param[in] is_object whether an object (true) or an array (false) begins
@param[in] len number of elements the container declares
@return whether the SAX parser accepted the start event
*/
bool enter_container(const bool is_object, const std::size_t len)
{
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->start_object(len) : !sax->start_array(len)))
{
return false;
}
container_stack.emplace_back(len, is_object);
return true;
}
/// @copydoc enter_container
bool enter_array(const std::size_t len)
{
return enter_container(/*is_object*/false, len);
}
/// @copydoc enter_container
bool enter_object(const std::size_t len)
{
return enter_container(/*is_object*/true, len);
}
////////// //////////
// BSON // // BSON //
////////// //////////
@@ -511,9 +575,12 @@ class binary_reader
@return whether a valid CBOR value was passed to the SAX parser @return whether a valid CBOR value was passed to the SAX parser
*/ */
bool parse_cbor_internal(const bool get_char, bool parse_cbor_value(const bool get_char,
const cbor_tag_handler_t tag_handler) const cbor_tag_handler_t tag_handler,
bool& tag_pending)
{ {
tag_pending = false;
switch (get_char ? get() : current) switch (get_char ? get() : current)
{ {
// EOF // EOF
@@ -705,37 +772,36 @@ class binary_reader
case 0x95: case 0x95:
case 0x96: case 0x96:
case 0x97: case 0x97:
return get_cbor_array( return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
case 0x98: // array (one-byte uint8_t for n follows) case 0x98: // array (one-byte uint8_t for n follows)
{ {
std::uint8_t len{}; std::uint8_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0x99: // array (two-byte uint16_t for n follow) case 0x99: // array (two-byte uint16_t for n follow)
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0x9A: // array (four-byte uint32_t for n follow) case 0x9A: // array (four-byte uint32_t for n follow)
{ {
std::uint32_t len{}; std::uint32_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && enter_array(size);
} }
case 0x9B: // array (eight-byte uint64_t for n follow) case 0x9B: // array (eight-byte uint64_t for n follow)
{ {
std::uint64_t len{}; std::uint64_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && enter_array(size);
} }
case 0x9F: // array (indefinite length) case 0x9F: // array (indefinite length)
return get_cbor_array(detail::unknown_size(), tag_handler); return enter_array(detail::unknown_size());
// map (0x00..0x17 pairs of data items follow) // map (0x00..0x17 pairs of data items follow)
case 0xA0: case 0xA0:
@@ -762,36 +828,36 @@ class binary_reader
case 0xB5: case 0xB5:
case 0xB6: case 0xB6:
case 0xB7: case 0xB7:
return get_cbor_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler); return enter_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
case 0xB8: // map (one-byte uint8_t for n follows) case 0xB8: // map (one-byte uint8_t for n follows)
{ {
std::uint8_t len{}; std::uint8_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xB9: // map (two-byte uint16_t for n follow) case 0xB9: // map (two-byte uint16_t for n follow)
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xBA: // map (four-byte uint32_t for n follow) case 0xBA: // map (four-byte uint32_t for n follow)
{ {
std::uint32_t len{}; std::uint32_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && enter_object(size);
} }
case 0xBB: // map (eight-byte uint64_t for n follow) case 0xBB: // map (eight-byte uint64_t for n follow)
{ {
std::uint64_t len{}; std::uint64_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && enter_object(size);
} }
case 0xBF: // map (indefinite length) case 0xBF: // map (indefinite length)
return get_cbor_object(detail::unknown_size(), tag_handler); return enter_object(detail::unknown_size());
case 0xC0: // tagged item case 0xC0: // tagged item
case 0xC1: case 0xC1:
@@ -875,7 +941,10 @@ class binary_reader
default: default:
break; break;
} }
return parse_cbor_internal(true, tag_handler); // the tagged value follows; it is read by the loop in
// parse_cbor_internal() rather than by recursing here
tag_pending = true;
return true;
} }
case cbor_tag_handler_t::store: case cbor_tag_handler_t::store:
@@ -925,7 +994,11 @@ class binary_reader
break; break;
} }
default: default:
return parse_cbor_internal(true, tag_handler); {
// as above, the tagged value is read by the caller
tag_pending = true;
return true;
}
} }
get(); get();
return get_cbor_binary(b) && sax->binary(b); return get_cbor_binary(b) && sax->binary(b);
@@ -1323,96 +1396,110 @@ class binary_reader
} }
/*! /*!
@param[in] len the length of the array or detail::unknown_size() for an @brief read a CBOR value and everything nested inside it
array of indefinite size
Reads values until the one that was begun here is complete, resuming the
enclosing container after each element, so that the nesting depth of the
input costs heap rather than native stack (see #5104).
@param[in] get_char whether a new character should be retrieved from the
input (true) or whether the last read character
@a current should be considered instead
@param[in] tag_handler how CBOR tags should be treated @param[in] tag_handler how CBOR tags should be treated
@return whether array creation completed
@return whether reading the value succeeded
*/ */
bool get_cbor_array(const std::size_t len, bool parse_cbor_internal(const bool get_char,
const cbor_tag_handler_t tag_handler) const cbor_tag_handler_t tag_handler)
{ {
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) // whether the next value starts at a fresh byte or at the one already
{ // read into `current`
return false; bool fetch = get_char;
}
if (len != detail::unknown_size()) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
for (std::size_t i = 0; i < len; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
{
return false;
}
}
}
else
{
while (get() != 0xFF)
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(false, tag_handler)))
{
return false;
}
}
}
return sax->end_array();
}
/*!
@param[in] len the length of the object or detail::unknown_size() for an
object of indefinite size
@param[in] tag_handler how CBOR tags should be treated
@return whether object creation completed
*/
bool get_cbor_object(const std::size_t len,
const cbor_tag_handler_t tag_handler)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
{
return false;
}
if (len != 0)
{
string_t key; string_t key;
if (len != detail::unknown_size())
while (true)
{ {
for (std::size_t i = 0; i < len; ++i) if (!container_stack.empty())
{
// the reference is not held across parse_cbor_value() below,
// which can push onto the stack and reallocate it
container_frame& top = container_stack.back();
bool at_end = false;
if (top.remaining != npos)
{
// definite length: the container ends once its elements
// have been read
at_end = (top.remaining == 0);
if (!at_end)
{
// claim the element about to be read
--top.remaining;
if (top.is_object)
{ {
get(); get();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
{
return false;
} }
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
{
return false;
}
key.clear();
} }
fetch = true;
} }
else else
{ {
while (get() != 0xFF) // indefinite length: the container ends at a break marker.
// Testing for it consumes a byte, which is the first byte
// of the next element when it is not one.
at_end = (get() == 0xFF);
fetch = top.is_object;
}
if (at_end)
{ {
const bool is_object = top.is_object;
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the value begun here is complete once its container is
if (container_stack.empty())
{
return true;
}
continue;
}
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key))) if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
{ {
return false; return false;
} }
fetch = true;
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler))) // a tag is not a value of its own: read on until the tagged value
bool tag_pending = false;
do
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending)))
{ {
return false; return false;
} }
key.clear(); fetch = true;
}
}
} }
while (tag_pending);
return sax->end_object(); // a value that opened a container left it on the stack; one that
// did not, and that was not inside a container, was the whole value
if (container_stack.empty())
{
return true;
}
}
} }
///////////// /////////////
@@ -1422,7 +1509,17 @@ class binary_reader
/*! /*!
@return whether a valid MessagePack value was passed to the SAX parser @return whether a valid MessagePack value was passed to the SAX parser
*/ */
bool parse_msgpack_internal() /*!
@brief read one MessagePack value
Reads a single value and passes it to the SAX parser. A value that begins
a container is not read to its end: the container is opened with
@ref enter_container and its elements are read by
@ref parse_msgpack_internal, so that nesting does not consume native stack.
@return whether reading the value succeeded
*/
bool parse_msgpack_value()
{ {
switch (get()) switch (get())
{ {
@@ -1578,7 +1675,7 @@ class binary_reader
case 0x8D: case 0x8D:
case 0x8E: case 0x8E:
case 0x8F: case 0x8F:
return get_msgpack_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu)); return enter_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
// fixarray // fixarray
case 0x90: case 0x90:
@@ -1597,7 +1694,7 @@ class binary_reader
case 0x9D: case 0x9D:
case 0x9E: case 0x9E:
case 0x9F: case 0x9F:
return get_msgpack_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu)); return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
// fixstr // fixstr
case 0xA0: case 0xA0:
@@ -1728,25 +1825,25 @@ class binary_reader
case 0xDC: // array 16 case 0xDC: // array 16
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0xDD: // array 32 case 0xDD: // array 32
{ {
std::uint32_t len{}; std::uint32_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_array(conditional_static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_array(conditional_static_cast<std::size_t>(len));
} }
case 0xDE: // map 16 case 0xDE: // map 16
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xDF: // map 32 case 0xDF: // map 32
{ {
std::uint32_t len{}; std::uint32_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_object(conditional_static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_object(conditional_static_cast<std::size_t>(len));
} }
// negative fixint // negative fixint
@@ -1994,55 +2091,69 @@ class binary_reader
} }
/*! /*!
@param[in] len the length of the array @brief read a MessagePack value and everything nested inside it
@return whether array creation completed
Reads values until the one that was begun here is complete, resuming the
enclosing container each time an element ends, so that the nesting depth
of the input costs heap rather than native stack (see #5104).
@return whether reading the value succeeded
*/ */
bool get_msgpack_array(const std::size_t len) bool parse_msgpack_internal()
{ {
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
return false;
}
for (std::size_t i = 0; i < len; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
{
return false;
}
}
return sax->end_array();
}
/*!
@param[in] len the length of the object
@return whether object creation completed
*/
bool get_msgpack_object(const std::size_t len)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
{
return false;
}
string_t key; string_t key;
for (std::size_t i = 0; i < len; ++i)
while (true)
{
if (!container_stack.empty())
{
// copied out before anything can push onto the stack and
// invalidate a reference into it
const bool is_object = container_stack.back().is_object;
if (container_stack.back().remaining == 0)
{
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the value begun here is complete once its container is
if (container_stack.empty())
{
return true;
}
continue;
}
// claim the element about to be read
--container_stack.back().remaining;
if (is_object)
{ {
get(); get();
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key))) if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key)))
{ {
return false; return false;
} }
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal())) if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_value()))
{ {
return false; return false;
} }
key.clear();
}
return sax->end_object(); // a value that opened a container left it on the stack; one that
// did not, and that was not inside a container, was the whole value
if (container_stack.empty())
{
return true;
}
}
} }
//////////// ////////////
@@ -3347,6 +3458,9 @@ class binary_reader
/// the SAX parser /// the SAX parser
json_sax_t* sax = nullptr; json_sax_t* sax = nullptr;
/// the containers that have been opened and not closed yet; see @ref container_frame
std::vector<container_frame> container_stack{};
// excluded markers in bjdata optimized type // excluded markers in bjdata optimized type
#define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \ #define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \
make_array<char_int_type>('F', 'H', 'N', 'S', 'T', 'Z', '[', '{') make_array<char_int_type>('F', 'H', 'N', 'S', 'T', 'Z', '[', '{')
+238 -124
View File
@@ -10817,6 +10817,7 @@ class binary_reader
const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error) const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
{ {
sax = sax_; sax = sax_;
container_stack.clear();
bool result = false; bool result = false;
switch (format) switch (format)
@@ -10866,6 +10867,69 @@ class binary_reader
} }
private: private:
////////////////////////
// nested containers //
////////////////////////
/*!
@brief a container that has been opened and not closed yet
The binary readers do not call themselves once per nesting level. Like
@ref parser::sax_parse_internal, which does the same for JSON text, they
keep the containers they are inside of on a heap-allocated stack, so that
the native call stack does not grow with the nesting depth of the input
and a deeply nested value is bounded by memory rather than by the stack
(see #5104).
The members are ordered by decreasing alignment, which is the ordering that
keeps a struct from growing as members are added to it.
*/
struct container_frame
{
container_frame(const std::size_t remaining_, const bool is_object_) noexcept
: remaining(remaining_), is_object(is_object_) {}
/// number of elements that have not been read yet
std::size_t remaining;
/// whether to close this container with end_object() or end_array()
bool is_object;
};
/*!
@brief open a nested array or object
Emits the SAX start event and records the container. This is the only
place the binary readers start a container, so a check that rejects one
can be made here and is then guaranteed to run before the start event.
@param[in] is_object whether an object (true) or an array (false) begins
@param[in] len number of elements the container declares
@return whether the SAX parser accepted the start event
*/
bool enter_container(const bool is_object, const std::size_t len)
{
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->start_object(len) : !sax->start_array(len)))
{
return false;
}
container_stack.emplace_back(len, is_object);
return true;
}
/// @copydoc enter_container
bool enter_array(const std::size_t len)
{
return enter_container(/*is_object*/false, len);
}
/// @copydoc enter_container
bool enter_object(const std::size_t len)
{
return enter_container(/*is_object*/true, len);
}
////////// //////////
// BSON // // BSON //
////////// //////////
@@ -11198,9 +11262,12 @@ class binary_reader
@return whether a valid CBOR value was passed to the SAX parser @return whether a valid CBOR value was passed to the SAX parser
*/ */
bool parse_cbor_internal(const bool get_char, bool parse_cbor_value(const bool get_char,
const cbor_tag_handler_t tag_handler) const cbor_tag_handler_t tag_handler,
bool& tag_pending)
{ {
tag_pending = false;
switch (get_char ? get() : current) switch (get_char ? get() : current)
{ {
// EOF // EOF
@@ -11392,37 +11459,36 @@ class binary_reader
case 0x95: case 0x95:
case 0x96: case 0x96:
case 0x97: case 0x97:
return get_cbor_array( return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
case 0x98: // array (one-byte uint8_t for n follows) case 0x98: // array (one-byte uint8_t for n follows)
{ {
std::uint8_t len{}; std::uint8_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0x99: // array (two-byte uint16_t for n follow) case 0x99: // array (two-byte uint16_t for n follow)
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_array(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0x9A: // array (four-byte uint32_t for n follow) case 0x9A: // array (four-byte uint32_t for n follow)
{ {
std::uint32_t len{}; std::uint32_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && enter_array(size);
} }
case 0x9B: // array (eight-byte uint64_t for n follow) case 0x9B: // array (eight-byte uint64_t for n follow)
{ {
std::uint64_t len{}; std::uint64_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && get_cbor_array(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "array") && enter_array(size);
} }
case 0x9F: // array (indefinite length) case 0x9F: // array (indefinite length)
return get_cbor_array(detail::unknown_size(), tag_handler); return enter_array(detail::unknown_size());
// map (0x00..0x17 pairs of data items follow) // map (0x00..0x17 pairs of data items follow)
case 0xA0: case 0xA0:
@@ -11449,36 +11515,36 @@ class binary_reader
case 0xB5: case 0xB5:
case 0xB6: case 0xB6:
case 0xB7: case 0xB7:
return get_cbor_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler); return enter_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
case 0xB8: // map (one-byte uint8_t for n follows) case 0xB8: // map (one-byte uint8_t for n follows)
{ {
std::uint8_t len{}; std::uint8_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xB9: // map (two-byte uint16_t for n follow) case 0xB9: // map (two-byte uint16_t for n follow)
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::cbor, len) && get_cbor_object(static_cast<std::size_t>(len), tag_handler); return get_number(input_format_t::cbor, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xBA: // map (four-byte uint32_t for n follow) case 0xBA: // map (four-byte uint32_t for n follow)
{ {
std::uint32_t len{}; std::uint32_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && enter_object(size);
} }
case 0xBB: // map (eight-byte uint64_t for n follow) case 0xBB: // map (eight-byte uint64_t for n follow)
{ {
std::uint64_t len{}; std::uint64_t len{};
std::size_t size{}; std::size_t size{};
return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && get_cbor_object(size, tag_handler); return get_number(input_format_t::cbor, len) && get_cbor_container_size(len, size, "map") && enter_object(size);
} }
case 0xBF: // map (indefinite length) case 0xBF: // map (indefinite length)
return get_cbor_object(detail::unknown_size(), tag_handler); return enter_object(detail::unknown_size());
case 0xC0: // tagged item case 0xC0: // tagged item
case 0xC1: case 0xC1:
@@ -11562,7 +11628,10 @@ class binary_reader
default: default:
break; break;
} }
return parse_cbor_internal(true, tag_handler); // the tagged value follows; it is read by the loop in
// parse_cbor_internal() rather than by recursing here
tag_pending = true;
return true;
} }
case cbor_tag_handler_t::store: case cbor_tag_handler_t::store:
@@ -11612,7 +11681,11 @@ class binary_reader
break; break;
} }
default: default:
return parse_cbor_internal(true, tag_handler); {
// as above, the tagged value is read by the caller
tag_pending = true;
return true;
}
} }
get(); get();
return get_cbor_binary(b) && sax->binary(b); return get_cbor_binary(b) && sax->binary(b);
@@ -12010,96 +12083,110 @@ class binary_reader
} }
/*! /*!
@param[in] len the length of the array or detail::unknown_size() for an @brief read a CBOR value and everything nested inside it
array of indefinite size
Reads values until the one that was begun here is complete, resuming the
enclosing container after each element, so that the nesting depth of the
input costs heap rather than native stack (see #5104).
@param[in] get_char whether a new character should be retrieved from the
input (true) or whether the last read character
@a current should be considered instead
@param[in] tag_handler how CBOR tags should be treated @param[in] tag_handler how CBOR tags should be treated
@return whether array creation completed
@return whether reading the value succeeded
*/ */
bool get_cbor_array(const std::size_t len, bool parse_cbor_internal(const bool get_char,
const cbor_tag_handler_t tag_handler) const cbor_tag_handler_t tag_handler)
{ {
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) // whether the next value starts at a fresh byte or at the one already
{ // read into `current`
return false; bool fetch = get_char;
}
if (len != detail::unknown_size()) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
for (std::size_t i = 0; i < len; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
{
return false;
}
}
}
else
{
while (get() != 0xFF)
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(false, tag_handler)))
{
return false;
}
}
}
return sax->end_array();
}
/*!
@param[in] len the length of the object or detail::unknown_size() for an
object of indefinite size
@param[in] tag_handler how CBOR tags should be treated
@return whether object creation completed
*/
bool get_cbor_object(const std::size_t len,
const cbor_tag_handler_t tag_handler)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
{
return false;
}
if (len != 0)
{
string_t key; string_t key;
if (len != detail::unknown_size())
while (true)
{ {
for (std::size_t i = 0; i < len; ++i) if (!container_stack.empty())
{
// the reference is not held across parse_cbor_value() below,
// which can push onto the stack and reallocate it
container_frame& top = container_stack.back();
bool at_end = false;
if (top.remaining != npos)
{
// definite length: the container ends once its elements
// have been read
at_end = (top.remaining == 0);
if (!at_end)
{
// claim the element about to be read
--top.remaining;
if (top.is_object)
{ {
get(); get();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
{
return false;
} }
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler)))
{
return false;
}
key.clear();
} }
fetch = true;
} }
else else
{ {
while (get() != 0xFF) // indefinite length: the container ends at a break marker.
// Testing for it consumes a byte, which is the first byte
// of the next element when it is not one.
at_end = (get() == 0xFF);
fetch = top.is_object;
}
if (at_end)
{ {
const bool is_object = top.is_object;
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the value begun here is complete once its container is
if (container_stack.empty())
{
return true;
}
continue;
}
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key))) if (JSON_HEDLEY_UNLIKELY(!get_cbor_string(key) || !sax->key(key)))
{ {
return false; return false;
} }
fetch = true;
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_internal(true, tag_handler))) // a tag is not a value of its own: read on until the tagged value
bool tag_pending = false;
do
{
if (JSON_HEDLEY_UNLIKELY(!parse_cbor_value(fetch, tag_handler, tag_pending)))
{ {
return false; return false;
} }
key.clear(); fetch = true;
}
}
} }
while (tag_pending);
return sax->end_object(); // a value that opened a container left it on the stack; one that
// did not, and that was not inside a container, was the whole value
if (container_stack.empty())
{
return true;
}
}
} }
///////////// /////////////
@@ -12109,7 +12196,17 @@ class binary_reader
/*! /*!
@return whether a valid MessagePack value was passed to the SAX parser @return whether a valid MessagePack value was passed to the SAX parser
*/ */
bool parse_msgpack_internal() /*!
@brief read one MessagePack value
Reads a single value and passes it to the SAX parser. A value that begins
a container is not read to its end: the container is opened with
@ref enter_container and its elements are read by
@ref parse_msgpack_internal, so that nesting does not consume native stack.
@return whether reading the value succeeded
*/
bool parse_msgpack_value()
{ {
switch (get()) switch (get())
{ {
@@ -12265,7 +12362,7 @@ class binary_reader
case 0x8D: case 0x8D:
case 0x8E: case 0x8E:
case 0x8F: case 0x8F:
return get_msgpack_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu)); return enter_object(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
// fixarray // fixarray
case 0x90: case 0x90:
@@ -12284,7 +12381,7 @@ class binary_reader
case 0x9D: case 0x9D:
case 0x9E: case 0x9E:
case 0x9F: case 0x9F:
return get_msgpack_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu)); return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x0Fu));
// fixstr // fixstr
case 0xA0: case 0xA0:
@@ -12415,25 +12512,25 @@ class binary_reader
case 0xDC: // array 16 case 0xDC: // array 16
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_array(static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_array(static_cast<std::size_t>(len));
} }
case 0xDD: // array 32 case 0xDD: // array 32
{ {
std::uint32_t len{}; std::uint32_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_array(conditional_static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_array(conditional_static_cast<std::size_t>(len));
} }
case 0xDE: // map 16 case 0xDE: // map 16
{ {
std::uint16_t len{}; std::uint16_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_object(static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_object(static_cast<std::size_t>(len));
} }
case 0xDF: // map 32 case 0xDF: // map 32
{ {
std::uint32_t len{}; std::uint32_t len{};
return get_number(input_format_t::msgpack, len) && get_msgpack_object(conditional_static_cast<std::size_t>(len)); return get_number(input_format_t::msgpack, len) && enter_object(conditional_static_cast<std::size_t>(len));
} }
// negative fixint // negative fixint
@@ -12681,55 +12778,69 @@ class binary_reader
} }
/*! /*!
@param[in] len the length of the array @brief read a MessagePack value and everything nested inside it
@return whether array creation completed
Reads values until the one that was begun here is complete, resuming the
enclosing container each time an element ends, so that the nesting depth
of the input costs heap rather than native stack (see #5104).
@return whether reading the value succeeded
*/ */
bool get_msgpack_array(const std::size_t len) bool parse_msgpack_internal()
{ {
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len))) // the key currently being read; hoisted out of the loop so that its
{ // capacity is reused across elements and across nesting levels
return false;
}
for (std::size_t i = 0; i < len; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
{
return false;
}
}
return sax->end_array();
}
/*!
@param[in] len the length of the object
@return whether object creation completed
*/
bool get_msgpack_object(const std::size_t len)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(len)))
{
return false;
}
string_t key; string_t key;
for (std::size_t i = 0; i < len; ++i)
while (true)
{
if (!container_stack.empty())
{
// copied out before anything can push onto the stack and
// invalidate a reference into it
const bool is_object = container_stack.back().is_object;
if (container_stack.back().remaining == 0)
{
container_stack.pop_back();
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->end_object() : !sax->end_array()))
{
return false;
}
// the value begun here is complete once its container is
if (container_stack.empty())
{
return true;
}
continue;
}
// claim the element about to be read
--container_stack.back().remaining;
if (is_object)
{ {
get(); get();
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key))) if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key)))
{ {
return false; return false;
} }
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal())) if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_value()))
{ {
return false; return false;
} }
key.clear();
}
return sax->end_object(); // a value that opened a container left it on the stack; one that
// did not, and that was not inside a container, was the whole value
if (container_stack.empty())
{
return true;
}
}
} }
//////////// ////////////
@@ -14034,6 +14145,9 @@ class binary_reader
/// the SAX parser /// the SAX parser
json_sax_t* sax = nullptr; json_sax_t* sax = nullptr;
/// the containers that have been opened and not closed yet; see @ref container_frame
std::vector<container_frame> container_stack{};
// excluded markers in bjdata optimized type // excluded markers in bjdata optimized type
#define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \ #define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \
make_array<char_int_type>('F', 'H', 'N', 'S', 'T', 'Z', '[', '{') make_array<char_int_type>('F', 'H', 'N', 'S', 'T', 'Z', '[', '{')
+87
View File
@@ -2035,6 +2035,93 @@ TEST_CASE("CBOR definite length equal to the indefinite-length sentinel")
} }
} }
TEST_CASE("CBOR nesting does not consume the call stack")
{
// Containers used to be read by calling back into the value reader once
// per element, and a tag by calling it for the tagged value, so the native
// call stack grew with the nesting depth of the input. Each of the three
// costs a single byte to encode -- 0x9F, 0x81 and 0xC2 -- so a payload of
// repeated bytes crashed the process (#5104). The containers are kept on a
// heap stack now, and a tag is read in a loop.
//
// Deeply nested values must not be compared, copied or dumped here: those
// operations are still recursive and would reintroduce the crash.
json _;
SECTION("indefinite-length containers")
{
const std::vector<uint8_t> input(500000, 0x9F);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 500001: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false).is_discarded());
}
SECTION("definite-length containers")
{
const std::vector<uint8_t> input(500000, 0x81);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input), "[json.exception.parse_error.110] parse error at byte 500001: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false).is_discarded());
}
SECTION("tags")
{
// a tag is not a value of its own, so a chain of them used to recurse
const std::vector<uint8_t> input(500000, 0xC2);
CHECK_THROWS_WITH_AS(_ = json::from_cbor(input, true, true, json::cbor_tag_handler_t::ignore), "[json.exception.parse_error.110] parse error at byte 500001: syntax error while parsing CBOR value: unexpected end of input", json::parse_error&);
CHECK(json::from_cbor(input, true, false, json::cbor_tag_handler_t::ignore).is_discarded());
}
SECTION("a well-formed deep value is read through the SAX interface")
{
std::vector<uint8_t> input(200000, 0x9F);
input.insert(input.end(), 200000, 0xFF);
SaxCountdown accept_all(1000000);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::cbor));
}
SECTION("a well-formed deep value is read into a value")
{
const std::size_t depth = 10000;
std::vector<uint8_t> input(depth, 0x81);
input.push_back(0x00);
json j = json::from_cbor(input);
std::size_t measured = 0;
const json* p = &j;
while (p->is_array() && !p->empty())
{
p = &p->front();
++measured;
}
CHECK(measured == depth);
CHECK(p->is_number());
}
SECTION("containers are still read the same way")
{
CHECK(json::from_cbor(std::vector<uint8_t>({0x80})) == json::array());
CHECK(json::from_cbor(std::vector<uint8_t>({0xA0})) == json::object());
CHECK(json::from_cbor(std::vector<uint8_t>({0x9F, 0xFF})) == json::array());
CHECK(json::from_cbor(std::vector<uint8_t>({0xBF, 0xFF})) == json::object());
CHECK(json::from_cbor(std::vector<uint8_t>({0x9F, 0x01, 0x02, 0xFF})) == json({1, 2}));
CHECK(json::from_cbor(std::vector<uint8_t>({0xBF, 0x61, 'a', 0x01, 0xFF})) == json({{"a", 1}}));
// definite and indefinite forms nested inside each other
CHECK(json::from_cbor(std::vector<uint8_t>({0x9F, 0x82, 0x01, 0x02, 0xA1, 0x61, 'k', 0xBF, 0xFF, 0xFF})) == json({{1, 2}, {{"k", json::object()}}}));
}
SECTION("tagged values are still read the same way")
{
const auto ignore = json::cbor_tag_handler_t::ignore;
CHECK(json::from_cbor(std::vector<uint8_t>({0xC2, 0x01}), true, true, ignore) == json(1));
// a chain of tags resolves to the value that follows it
CHECK(json::from_cbor(std::vector<uint8_t>({0xC2, 0xC2, 0xC2, 0x01}), true, true, ignore) == json(1));
// a tag inside a container, and one in front of a container
CHECK(json::from_cbor(std::vector<uint8_t>({0x82, 0xC2, 0x01, 0x02}), true, true, ignore) == json({1, 2}));
CHECK(json::from_cbor(std::vector<uint8_t>({0xC2, 0x82, 0x01, 0x02}), true, true, ignore) == json({1, 2}));
}
}
TEST_CASE("CBOR indefinite-length strings do not recurse per chunk") TEST_CASE("CBOR indefinite-length strings do not recurse per chunk")
{ {
// Reading an indefinite-length string or byte array used to call itself // Reading an indefinite-length string or byte array used to call itself
+61
View File
@@ -1598,6 +1598,67 @@ TEST_CASE("MessagePack")
} }
// use this testcase outside [hide] to run it with Valgrind // use this testcase outside [hide] to run it with Valgrind
TEST_CASE("MessagePack nesting does not consume the call stack")
{
// Reading a container used to call back into the value reader once per
// element, so the native call stack grew with the nesting depth of the
// input: one frame per byte for repeated 0x91 (a one-element array), which
// crashes the process long before the input is exhausted (#5104). The
// containers are kept on a heap stack now.
//
// Note that deeply nested values must not be compared, copied or dumped
// here: those operations are still recursive, and would reintroduce the
// very crash this checks for. Depth is measured by descending instead.
SECTION("an unterminated chain is reported, not crashed on")
{
json _;
const std::vector<uint8_t> input(300000, 0x91);
CHECK_THROWS_WITH_AS(_ = json::from_msgpack(input), "[json.exception.parse_error.110] parse error at byte 300001: syntax error while parsing MessagePack value: unexpected end of input", json::parse_error&);
CHECK(json::from_msgpack(input, true, false).is_discarded());
}
SECTION("a well-formed deep value is read through the SAX interface")
{
std::vector<uint8_t> input(300000, 0x91);
input.push_back(0x01); // innermost value
SaxCountdown accept_all(600001);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::msgpack));
}
SECTION("a well-formed deep value is read into a value")
{
const std::size_t depth = 10000;
std::vector<uint8_t> input(depth, 0x91);
input.push_back(0x01);
json j = json::from_msgpack(input);
std::size_t measured = 0;
const json* p = &j;
while (p->is_array() && !p->empty())
{
p = &p->front();
++measured;
}
CHECK(measured == depth);
CHECK(p->is_number());
}
SECTION("containers are still read the same way")
{
CHECK(json::from_msgpack(std::vector<uint8_t>({0x90})) == json::array());
CHECK(json::from_msgpack(std::vector<uint8_t>({0x80})) == json::object());
CHECK(json::from_msgpack(std::vector<uint8_t>({0x92, 0x90, 0x80})) == json({json::array(), json::object()}));
CHECK(json::from_msgpack(std::vector<uint8_t>({0x91, 0x91, 0x91, 0x90})) == json({{{json::array()}}}));
CHECK(json::from_msgpack(std::vector<uint8_t>({0x81, 0xA1, 'a', 0x81, 0xA1, 'b', 0x92, 0x01, 0x02})) == json({{"a", {{"b", {1, 2}}}}}));
// array 16 and map 32, i.e. the counted forms
CHECK(json::from_msgpack(std::vector<uint8_t>({0xDC, 0x00, 0x02, 0x01, 0x02})) == json({1, 2}));
CHECK(json::from_msgpack(std::vector<uint8_t>({0xDF, 0x00, 0x00, 0x00, 0x01, 0xA1, 'k', 0xC3})) == json({{"k", true}}));
}
}
TEST_CASE("single MessagePack roundtrip") TEST_CASE("single MessagePack roundtrip")
{ {
SECTION("sample.json") SECTION("sample.json")