Compare commits

...
Author SHA1 Message Date
Niels Lohmann 1cba195509 Read UBJSON and BJData containers without recursing per nesting level
get_ubjson_array() and get_ubjson_object() read their elements by calling
back into the value reader, which called them again for a nested container,
so the native call stack grew with the nesting depth of the input. '[' alone
opens a container, so half a million of them crashes the process before the
input runs out (#5104). The optimized forms reach the same path through a
size or type annotation, and in plain UBJSON '[' and '{' are permitted as the
type of an optimized container, so "[$[#i\x01" repeated nests just as deeply
at six bytes a level.

Both readers now only open their container, and parse_ubjson_internal() loops:
it closes the containers that have ended, claims the next element of the
innermost one, reads its key when it is an object, and works out the marker
of the value to read next. That last part is where the formats differ, and
the loop follows what the four element loops used to do:

  - a sized, typed container gives its elements no marker of their own
  - a sized, untyped container reads one for each element
  - a container that ends at a marker has the byte already, from the test
    against ']' or '}'; for an object it is the first byte of the key

The ND-array wrapper and the 'B' binary shortcut stay as they are. Both read
a complete value rather than opening a container, and their elements are
always scalars: BJData does not permit '[' or '{' as an optimized type, which
is also why only plain UBJSON needed the type-marker case above.

A container of no-ops keeps its behaviour of holding no elements while still
announcing its declared size to the SAX parser, by opening it and then
setting its count to zero.

unit-ubjson and unit-bjdata pass unchanged, 1.39 million assertions between
them, and a behaviour comparison against the previous commit over every
container form -- sized, unsized, typed, untyped, empty, no-op, ND-array,
binary, and the forms nested inside one another -- gives identical values,
error codes, messages and byte offsets. 500,000 levels of each vector now
report a parse error instead of crashing, and a well-formed 100,000-level
value is read to completion.

The driver costs about 3 % on parsing 60,000 small objects and one array of a
million integers, for the reason given in the previous commit; reading the
frame once per element rather than per branch halved what it cost before.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-07 15:45:43 +02:00
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
5 changed files with 965 additions and 454 deletions
+350 -221
View File
@@ -130,6 +130,7 @@ class binary_reader
const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
{
sax = sax_;
container_stack.clear();
bool result = false;
switch (format)
@@ -179,6 +180,75 @@ class binary_reader
}
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_,
const char_int_type type_marker_ = 0) noexcept
: remaining(remaining_), type_marker(type_marker_), 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;
/// 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;
/// 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,
const char_int_type type_marker = 0)
{
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->start_object(len) : !sax->start_array(len)))
{
return false;
}
container_stack.emplace_back(len, is_object, type_marker);
return true;
}
/// @copydoc enter_container
bool enter_array(const std::size_t len, const char_int_type type_marker = 0)
{
return enter_container(/*is_object*/false, len, type_marker);
}
/// @copydoc enter_container
bool enter_object(const std::size_t len, const char_int_type type_marker = 0)
{
return enter_container(/*is_object*/true, len, type_marker);
}
//////////
// BSON //
//////////
@@ -511,9 +581,12 @@ class binary_reader
@return whether a valid CBOR value was passed to the SAX parser
*/
bool parse_cbor_internal(const bool get_char,
const cbor_tag_handler_t tag_handler)
bool parse_cbor_value(const bool get_char,
const cbor_tag_handler_t tag_handler,
bool& tag_pending)
{
tag_pending = false;
switch (get_char ? get() : current)
{
// EOF
@@ -705,37 +778,36 @@ class binary_reader
case 0x95:
case 0x96:
case 0x97:
return get_cbor_array(
conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
case 0x98: // array (one-byte uint8_t for n follows)
{
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)
{
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)
{
std::uint32_t len{};
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)
{
std::uint64_t len{};
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)
return get_cbor_array(detail::unknown_size(), tag_handler);
return enter_array(detail::unknown_size());
// map (0x00..0x17 pairs of data items follow)
case 0xA0:
@@ -762,36 +834,36 @@ class binary_reader
case 0xB5:
case 0xB6:
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)
{
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)
{
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)
{
std::uint32_t len{};
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)
{
std::uint64_t len{};
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)
return get_cbor_object(detail::unknown_size(), tag_handler);
return enter_object(detail::unknown_size());
case 0xC0: // tagged item
case 0xC1:
@@ -875,7 +947,10 @@ class binary_reader
default:
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:
@@ -925,7 +1000,11 @@ class binary_reader
break;
}
default:
return parse_cbor_internal(true, tag_handler);
{
// as above, the tagged value is read by the caller
tag_pending = true;
return true;
}
}
get();
return get_cbor_binary(b) && sax->binary(b);
@@ -1323,96 +1402,110 @@ class binary_reader
}
/*!
@param[in] len the length of the array or detail::unknown_size() for an
array of indefinite size
@brief read a CBOR value and everything nested inside it
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
@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)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len)))
{
return false;
}
// whether the next value starts at a fresh byte or at the one already
// read into `current`
bool fetch = get_char;
if (len != detail::unknown_size())
{
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)
{
// 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;
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();
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
{
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)))
{
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;
}
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 +1515,17 @@ class binary_reader
/*!
@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())
{
@@ -1578,7 +1681,7 @@ class binary_reader
case 0x8D:
case 0x8E:
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
case 0x90:
@@ -1597,7 +1700,7 @@ class binary_reader
case 0x9D:
case 0x9E:
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
case 0xA0:
@@ -1728,25 +1831,25 @@ class binary_reader
case 0xDC: // array 16
{
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
{
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
{
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
{
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
@@ -1994,55 +2097,69 @@ class binary_reader
}
/*!
@param[in] len the length of the array
@return whether array creation completed
@brief read a MessagePack value and everything nested inside it
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)))
{
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;
}
// 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;
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();
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key)))
{
return false;
}
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_value()))
{
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;
}
}
}
////////////
@@ -2058,7 +2175,103 @@ class binary_reader
*/
bool parse_ubjson_internal(const bool get_char = true)
{
return get_ubjson_value(get_char ? get_ignore_noop() : current);
// 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;
// the type marker of the value to read next
char_int_type prefix = get_char ? get_ignore_noop() : current;
while (true)
{
const std::size_t depth = container_stack.size();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(prefix)))
{
return false;
}
// the value begun here is complete once it is not inside anything
if (container_stack.empty())
{
return true;
}
// a value was completed rather than a container opened; a
// container that ends at a marker needs the next byte to test
if (container_stack.size() == depth && container_stack.back().remaining == npos)
{
get_ignore_noop();
}
// advance to the next element, closing the containers that ended.
// The reference is not held across get_ubjson_value() above, which
// can push onto the stack and reallocate it.
for (;;)
{
container_frame& top = container_stack.back();
if (top.remaining != npos)
{
if (top.remaining != 0)
{
--top.remaining;
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
}
// an optimized container gives its elements no marker
prefix = (top.type_marker != 0) ? top.type_marker : get_ignore_noop();
break;
}
}
// the end marker is compared against a literal rather than
// against a conditional expression, because char_int_type is
// unsigned for some input adapters and MSVC then reports the
// comparison as a signed/unsigned mismatch
else if (top.is_object ? (current != '}') : (current != ']'))
{
// a container that ends at a marker is never optimized, so
// every element carries its own marker; for an object the
// byte tested above is the first byte of the key
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key)))
{
return false;
}
prefix = get_ignore_noop();
}
else
{
prefix = current;
}
break;
}
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;
}
if (container_stack.empty())
{
return true;
}
// the container that just ended was an element of the one
// below it, which may need the next byte for its own test
if (container_stack.back().remaining == npos)
{
get_ignore_noop();
}
}
}
}
/*!
@@ -2830,53 +3043,22 @@ class binary_reader
exception_message(input_format, "excessive array size", "size"), nullptr));
}
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(size_and_type.first)))
if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
{
return false;
}
if (size_and_type.second != 0)
if (size_and_type.second == 'N')
{
if (size_and_type.second != 'N')
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
}
}
}
}
else
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
}
}
}
else
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size())))
{
return false;
// a no-op is not a value, so a container of them holds none;
// the declared size has already been passed to the SAX parser
container_stack.back().remaining = 0;
}
while (current != ']')
{
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal(false)))
{
return false;
}
get_ignore_noop();
}
return true;
}
return sax->end_array();
return enter_array(detail::unknown_size());
}
/*!
@@ -2898,68 +3080,12 @@ class binary_reader
exception_message(input_format, "BJData object does not support ND-array size in optimized format", "object"), nullptr));
}
string_t key;
if (size_and_type.first != npos)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(size_and_type.first)))
{
return false;
return enter_object(size_and_type.first, size_and_type.second);
}
if (size_and_type.second != 0)
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
}
key.clear();
}
}
else
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
key.clear();
}
}
}
else
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size())))
{
return false;
}
while (current != '}')
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
get_ignore_noop();
key.clear();
}
}
return sax->end_object();
return enter_object(detail::unknown_size());
}
// Note, no reader for UBJSON binary types is implemented because they do
@@ -3347,6 +3473,9 @@ class binary_reader
/// the SAX parser
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
#define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \
make_array<char_int_type>('F', 'H', 'N', 'S', 'T', 'Z', '[', '{')
+350 -221
View File
@@ -10817,6 +10817,7 @@ class binary_reader
const cbor_tag_handler_t tag_handler = cbor_tag_handler_t::error)
{
sax = sax_;
container_stack.clear();
bool result = false;
switch (format)
@@ -10866,6 +10867,75 @@ class binary_reader
}
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_,
const char_int_type type_marker_ = 0) noexcept
: remaining(remaining_), type_marker(type_marker_), 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;
/// 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;
/// 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,
const char_int_type type_marker = 0)
{
if (JSON_HEDLEY_UNLIKELY(is_object ? !sax->start_object(len) : !sax->start_array(len)))
{
return false;
}
container_stack.emplace_back(len, is_object, type_marker);
return true;
}
/// @copydoc enter_container
bool enter_array(const std::size_t len, const char_int_type type_marker = 0)
{
return enter_container(/*is_object*/false, len, type_marker);
}
/// @copydoc enter_container
bool enter_object(const std::size_t len, const char_int_type type_marker = 0)
{
return enter_container(/*is_object*/true, len, type_marker);
}
//////////
// BSON //
//////////
@@ -11198,9 +11268,12 @@ class binary_reader
@return whether a valid CBOR value was passed to the SAX parser
*/
bool parse_cbor_internal(const bool get_char,
const cbor_tag_handler_t tag_handler)
bool parse_cbor_value(const bool get_char,
const cbor_tag_handler_t tag_handler,
bool& tag_pending)
{
tag_pending = false;
switch (get_char ? get() : current)
{
// EOF
@@ -11392,37 +11465,36 @@ class binary_reader
case 0x95:
case 0x96:
case 0x97:
return get_cbor_array(
conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu), tag_handler);
return enter_array(conditional_static_cast<std::size_t>(static_cast<unsigned int>(current) & 0x1Fu));
case 0x98: // array (one-byte uint8_t for n follows)
{
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)
{
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)
{
std::uint32_t len{};
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)
{
std::uint64_t len{};
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)
return get_cbor_array(detail::unknown_size(), tag_handler);
return enter_array(detail::unknown_size());
// map (0x00..0x17 pairs of data items follow)
case 0xA0:
@@ -11449,36 +11521,36 @@ class binary_reader
case 0xB5:
case 0xB6:
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)
{
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)
{
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)
{
std::uint32_t len{};
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)
{
std::uint64_t len{};
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)
return get_cbor_object(detail::unknown_size(), tag_handler);
return enter_object(detail::unknown_size());
case 0xC0: // tagged item
case 0xC1:
@@ -11562,7 +11634,10 @@ class binary_reader
default:
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:
@@ -11612,7 +11687,11 @@ class binary_reader
break;
}
default:
return parse_cbor_internal(true, tag_handler);
{
// as above, the tagged value is read by the caller
tag_pending = true;
return true;
}
}
get();
return get_cbor_binary(b) && sax->binary(b);
@@ -12010,96 +12089,110 @@ class binary_reader
}
/*!
@param[in] len the length of the array or detail::unknown_size() for an
array of indefinite size
@brief read a CBOR value and everything nested inside it
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
@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)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(len)))
{
return false;
}
// whether the next value starts at a fresh byte or at the one already
// read into `current`
bool fetch = get_char;
if (len != detail::unknown_size())
{
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)
{
// 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;
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();
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
{
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)))
{
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;
}
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 +12202,17 @@ class binary_reader
/*!
@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())
{
@@ -12265,7 +12368,7 @@ class binary_reader
case 0x8D:
case 0x8E:
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
case 0x90:
@@ -12284,7 +12387,7 @@ class binary_reader
case 0x9D:
case 0x9E:
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
case 0xA0:
@@ -12415,25 +12518,25 @@ class binary_reader
case 0xDC: // array 16
{
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
{
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
{
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
{
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
@@ -12681,55 +12784,69 @@ class binary_reader
}
/*!
@param[in] len the length of the array
@return whether array creation completed
@brief read a MessagePack value and everything nested inside it
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)))
{
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;
}
// 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;
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();
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_msgpack_string(key) || !sax->key(key)))
{
return false;
}
}
}
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_internal()))
if (JSON_HEDLEY_UNLIKELY(!parse_msgpack_value()))
{
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;
}
}
}
////////////
@@ -12745,7 +12862,103 @@ class binary_reader
*/
bool parse_ubjson_internal(const bool get_char = true)
{
return get_ubjson_value(get_char ? get_ignore_noop() : current);
// 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;
// the type marker of the value to read next
char_int_type prefix = get_char ? get_ignore_noop() : current;
while (true)
{
const std::size_t depth = container_stack.size();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(prefix)))
{
return false;
}
// the value begun here is complete once it is not inside anything
if (container_stack.empty())
{
return true;
}
// a value was completed rather than a container opened; a
// container that ends at a marker needs the next byte to test
if (container_stack.size() == depth && container_stack.back().remaining == npos)
{
get_ignore_noop();
}
// advance to the next element, closing the containers that ended.
// The reference is not held across get_ubjson_value() above, which
// can push onto the stack and reallocate it.
for (;;)
{
container_frame& top = container_stack.back();
if (top.remaining != npos)
{
if (top.remaining != 0)
{
--top.remaining;
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
}
// an optimized container gives its elements no marker
prefix = (top.type_marker != 0) ? top.type_marker : get_ignore_noop();
break;
}
}
// the end marker is compared against a literal rather than
// against a conditional expression, because char_int_type is
// unsigned for some input adapters and MSVC then reports the
// comparison as a signed/unsigned mismatch
else if (top.is_object ? (current != '}') : (current != ']'))
{
// a container that ends at a marker is never optimized, so
// every element carries its own marker; for an object the
// byte tested above is the first byte of the key
if (top.is_object)
{
key.clear();
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key)))
{
return false;
}
prefix = get_ignore_noop();
}
else
{
prefix = current;
}
break;
}
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;
}
if (container_stack.empty())
{
return true;
}
// the container that just ended was an element of the one
// below it, which may need the next byte for its own test
if (container_stack.back().remaining == npos)
{
get_ignore_noop();
}
}
}
}
/*!
@@ -13517,53 +13730,22 @@ class binary_reader
exception_message(input_format, "excessive array size", "size"), nullptr));
}
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(size_and_type.first)))
if (JSON_HEDLEY_UNLIKELY(!enter_array(size_and_type.first, size_and_type.second)))
{
return false;
}
if (size_and_type.second != 0)
if (size_and_type.second == 'N')
{
if (size_and_type.second != 'N')
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
}
}
}
}
else
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
}
}
}
else
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_array(detail::unknown_size())))
{
return false;
// a no-op is not a value, so a container of them holds none;
// the declared size has already been passed to the SAX parser
container_stack.back().remaining = 0;
}
while (current != ']')
{
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal(false)))
{
return false;
}
get_ignore_noop();
}
return true;
}
return sax->end_array();
return enter_array(detail::unknown_size());
}
/*!
@@ -13585,68 +13767,12 @@ class binary_reader
exception_message(input_format, "BJData object does not support ND-array size in optimized format", "object"), nullptr));
}
string_t key;
if (size_and_type.first != npos)
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(size_and_type.first)))
{
return false;
return enter_object(size_and_type.first, size_and_type.second);
}
if (size_and_type.second != 0)
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_value(size_and_type.second)))
{
return false;
}
key.clear();
}
}
else
{
for (std::size_t i = 0; i < size_and_type.first; ++i)
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
key.clear();
}
}
}
else
{
if (JSON_HEDLEY_UNLIKELY(!sax->start_object(detail::unknown_size())))
{
return false;
}
while (current != '}')
{
if (JSON_HEDLEY_UNLIKELY(!get_ubjson_string(key, false) || !sax->key(key)))
{
return false;
}
if (JSON_HEDLEY_UNLIKELY(!parse_ubjson_internal()))
{
return false;
}
get_ignore_noop();
key.clear();
}
}
return sax->end_object();
return enter_object(detail::unknown_size());
}
// Note, no reader for UBJSON binary types is implemented because they do
@@ -14034,6 +14160,9 @@ class binary_reader
/// the SAX parser
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
#define JSON_BINARY_READER_MAKE_BJD_OPTIMIZED_TYPE_MARKERS_ \
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")
{
// 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
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")
{
SECTION("sample.json")
+105
View File
@@ -2149,6 +2149,111 @@ TEST_CASE("UBJSON")
}
}
TEST_CASE("UBJSON nesting does not consume the call stack")
{
// Containers used to be read by calling back into the value reader once
// per element, so the native call stack grew with the nesting depth of the
// input. '[' alone opens a container, so a payload of repeated '[' crashed
// the process (#5104), as did the optimized forms, which reach the same
// path through a type or size annotation. The containers 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.
json _;
SECTION("containers that end at a marker")
{
const std::vector<uint8_t> input(500000, '[');
CHECK_THROWS_WITH_AS(_ = json::from_ubjson(input), "[json.exception.parse_error.110] parse error at byte 500001: syntax error while parsing UBJSON value: unexpected end of input", json::parse_error&);
CHECK(json::from_ubjson(input, true, false).is_discarded());
}
SECTION("containers with a size")
{
std::vector<uint8_t> input;
for (std::size_t i = 0; i < 100000; ++i)
{
input.push_back('[');
input.push_back('#');
input.push_back('i');
input.push_back(1);
}
CHECK_THROWS_AS(_ = json::from_ubjson(input), json::parse_error&);
CHECK(json::from_ubjson(input, true, false).is_discarded());
}
SECTION("containers with a type and a size")
{
// '[' is a permitted optimized type in UBJSON, so each element of such
// a container is itself a container, read without a marker of its own
std::vector<uint8_t> input;
for (std::size_t i = 0; i < 100000; ++i)
{
const std::vector<uint8_t> level = {'[', '$', '[', '#', 'i', 1};
input.insert(input.end(), level.begin(), level.end());
}
CHECK_THROWS_AS(_ = json::from_ubjson(input), json::parse_error&);
CHECK(json::from_ubjson(input, true, false).is_discarded());
}
SECTION("a well-formed deep value is read through the SAX interface")
{
std::vector<uint8_t> input(100000, '[');
input.insert(input.end(), 100000, ']');
SaxCountdown accept_all(1000000);
CHECK(json::sax_parse(input, &accept_all, json::input_format_t::ubjson));
}
SECTION("a well-formed deep value is read into a value")
{
const std::size_t depth = 10000;
std::vector<uint8_t> input(depth, '[');
input.insert(input.end(), depth, ']');
json j = json::from_ubjson(input);
std::size_t measured = 0;
const json* p = &j;
while (p->is_array() && !p->empty())
{
p = &p->front();
++measured;
}
// the innermost array is empty, so the descent stops one level short
CHECK(measured == depth - 1);
}
SECTION("containers are still read the same way")
{
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', ']'})) == json::array());
CHECK(json::from_ubjson(std::vector<uint8_t>({'{', '}'})) == json::object());
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '#', 'i', 0})) == json::array());
CHECK(json::from_ubjson(std::vector<uint8_t>({'{', '#', 'i', 0})) == json::object());
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '$', 'i', '#', 'i', 2, 1, 2})) == json({1, 2}));
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '#', 'i', 2, 'i', 1, 'i', 2})) == json({1, 2}));
CHECK(json::from_ubjson(std::vector<uint8_t>({'{', '$', 'i', '#', 'i', 1, 'i', 1, 'a', 1})) == json({{"a", 1}}));
// a no-op is not a value, so a container of them holds none
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '$', 'N', '#', 'i', 2})) == json::array());
// sized and unsized forms nested inside one another
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '[', '#', 'i', 2, 'i', 1, 'i', 2, ']'})) == json({{1, 2}}));
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '#', 'i', 1, '[', 'i', 1, ']'})) == json({{1}}));
// an optimized container of containers
CHECK(json::from_ubjson(std::vector<uint8_t>({'[', '$', '[', '#', 'i', 2, 'i', 1, ']', 'i', 2, ']'})) == json({{1}, {2}}));
}
SECTION("BJData containers are still read the same way")
{
// the ND-array wrapper and the binary shortcut are complete values,
// not containers the reader descends into
CHECK(json::from_bjdata(std::vector<uint8_t>({'[', '$', 'U', '#', '[', '$', 'i', '#', 'i', 2, 2, 3, 1, 2, 3, 4, 5, 6})) ==
json({{"_ArrayType_", "uint8"}, {"_ArraySize_", {2, 3}}, {"_ArrayData_", {1, 2, 3, 4, 5, 6}}}));
CHECK(json::from_bjdata(std::vector<uint8_t>({'[', '$', 'i', '#', 'i', 2, 1, 2})) == json({1, 2}));
CHECK(json::from_bjdata(std::vector<uint8_t>({'[', '[', 'i', 1, ']', ']'})) == json({{1}}));
}
}
TEST_CASE("UBJSON optimized arrays of a valueless type are bounded")
{
// An element of type 'Z', 'T' or 'F' is encoded by its marker alone, so an